diff --git a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts index 309fe21f8bf17..3f3dce55e1236 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts @@ -435,6 +435,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D avcResults: `https://www.elastic.co/blog/elastic-security-av-comparatives-business-test`, bidirectionalIntegrations: `${ELASTIC_DOCS}solutions/security/endpoint-response-actions/third-party-response-actions`, trustedApps: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/trusted-applications`, + elasticAiFeatures: `${ELASTIC_DOCS}solutions/security/ai`, eventFilters: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/event-filters`, blocklist: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/blocklist`, threatIntelInt: `${ELASTIC_DOCS}solutions/security/get-started/enable-threat-intelligence-integrations`, @@ -462,6 +463,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D : `https://www.elastic.co/blog/security-prebuilt-rules-editing`, createEsqlRuleType: `${ELASTIC_DOCS}solutions/security/detect-and-alert/create-detection-rule#create-esql-rule`, ruleUiAdvancedParams: `${ELASTIC_DOCS}solutions/security/detect-and-alert/create-detection-rule#rule-ui-advanced-params`, + thirdPartyLlmProviders: `${ELASTIC_DOCS}solutions/security/ai/set-up-connectors-for-large-language-models-llm`, entityAnalytics: { riskScorePrerequisites: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/entity-risk-scoring-requirements`, entityRiskScoring: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/entity-risk-scoring`, diff --git a/src/platform/packages/shared/kbn-doc-links/src/types.ts b/src/platform/packages/shared/kbn-doc-links/src/types.ts index 90a4d9c5278e7..ba961ca826fe5 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/types.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/types.ts @@ -295,7 +295,9 @@ export interface DocLinks { readonly artifactControl: string; readonly avcResults: string; readonly bidirectionalIntegrations: string; + readonly thirdPartyLlmProviders: string; readonly trustedApps: string; + readonly elasticAiFeatures: string; readonly eventFilters: string; readonly eventMerging: string; readonly blocklist: string; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/elastic_llm_callout.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/elastic_llm_callout.test.tsx new file mode 100644 index 0000000000000..a02632fc602a6 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/elastic_llm_callout.test.tsx @@ -0,0 +1,69 @@ +/* + * 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 { render } from '@testing-library/react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { ElasticLlmCallout } from './elastic_llm_callout'; +import { TestProviders } from '../../mock/test_providers/test_providers'; + +jest.mock('react-use/lib/useLocalStorage'); + +describe('ElasticLlmCallout', () => { + const defaultProps = { + showEISCallout: true, + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]); + }); + + it('should not render when showEISCallout is false', () => { + const { queryByTestId } = render(, { + wrapper: ({ children }) => {children}, + }); + expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument(); + }); + + it('should not render when tour is completed', () => { + (useLocalStorage as jest.Mock).mockReturnValue([true, jest.fn()]); + const { queryByTestId } = render( + + + , + { + wrapper: ({ children }) => {children}, + } + ); + + expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument(); + }); + + it('should render links', () => { + const { queryByTestId } = render( + + + , + { wrapper: ({ children }) => {children} } + ); + expect(queryByTestId('elasticLlmUsageCostLink')).toHaveTextContent('additional costs incur'); + expect(queryByTestId('elasticLlmConnectorLink')).toHaveTextContent('connector'); + }); + + it('should show callout when showEISCallout changes to true', () => { + const { rerender, queryByTestId } = render( + + + , + { wrapper: ({ children }) => {children} } + ); + expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument(); + + rerender(); + expect(queryByTestId('elasticLlmCallout')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/elastic_llm_callout.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/elastic_llm_callout.tsx new file mode 100644 index 0000000000000..07d2ae2e2cdca --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/elastic_llm_callout.tsx @@ -0,0 +1,104 @@ +/* + * 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, useState } from 'react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; + +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut, EuiLink, useEuiTheme } from '@elastic/eui'; +import { useAssistantContext } from '../../assistant_context'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../tour/const'; +import { useTourStorageKey } from '../../tour/common/hooks/use_tour_storage_key'; + +export const ElasticLlmCallout = ({ showEISCallout }: { showEISCallout: boolean }) => { + const { + getUrlForApp, + docLinks: { + links: { + observability: { elasticManagedLlmUsageCost: ELASTIC_LLM_USAGE_COST_LINK }, + }, + }, + } = useAssistantContext(); + const { euiTheme } = useEuiTheme(); + const tourStorageKey = useTourStorageKey( + NEW_FEATURES_TOUR_STORAGE_KEYS.CONVERSATION_CONNECTOR_ELASTIC_LLM + ); + const [tourCompleted, setTourCompleted] = useLocalStorage(tourStorageKey, false); + const [showCallOut, setShowCallOut] = useState(showEISCallout); + + const onDismiss = useCallback(() => { + setShowCallOut(false); + setTourCompleted(true); + }, [setTourCompleted]); + + useEffect(() => { + if (showEISCallout && !tourCompleted) { + setShowCallOut(true); + } else { + setShowCallOut(false); + } + }, [showEISCallout, tourCompleted]); + + if (!showCallOut) { + return; + } + + return ( + +

+ + + + ), + customConnector: ( + + + + ), + }} + /> +

+
+ ); +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/index.test.tsx new file mode 100644 index 0000000000000..011281dc6f1ec --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/index.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { AssistantConversationBanner } from '.'; +import { AIConnector, Conversation, useAssistantContext } from '../../..'; +import { customConvo } from '../../mock/conversation'; + +jest.mock('../../..'); + +jest.mock('../../connectorland/connector_missing_callout', () => ({ + ConnectorMissingCallout: () =>
, +})); + +jest.mock('./elastic_llm_callout', () => ({ + ElasticLlmCallout: () =>
, +})); + +describe('AssistantConversationBanner', () => { + const setIsSettingsModalVisible = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders ConnectorMissingCallout when shouldShowMissingConnectorCallout is true', () => { + (useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: true }); + + render( + + ); + + expect(screen.getByTestId('connector-missing-callout')).toBeInTheDocument(); + }); + + it('renders ElasticLlmCallout when Elastic LLM is enabled', () => { + (useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: true }); + const mockConnectors = [ + { id: 'mockLLM', actionTypeId: '.inference', isPreconfigured: true }, + ] as AIConnector[]; + + const mockConversation = { + ...customConvo, + id: 'mockConversation', + apiConfig: { + connectorId: 'mockLLM', + actionTypeId: '.inference', + }, + } as Conversation; + + render( + + ); + + expect(screen.getByTestId('elastic-llm-callout')).toBeInTheDocument(); + }); + + it('renders nothing when no conditions are met', () => { + (useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: false }); + + const { container } = render( + + ); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/index.tsx new file mode 100644 index 0000000000000..6a9c53c2e5073 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_conversation_banner/index.tsx @@ -0,0 +1,57 @@ +/* + * 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, { useMemo } from 'react'; +import { AIConnector, Conversation, useAssistantContext } from '../../..'; +import { isElasticManagedLlmConnector } from '../../connectorland/helpers'; +import { ConnectorMissingCallout } from '../../connectorland/connector_missing_callout'; +import { ElasticLlmCallout } from './elastic_llm_callout'; + +export const AssistantConversationBanner = React.memo( + ({ + isSettingsModalVisible, + setIsSettingsModalVisible, + shouldShowMissingConnectorCallout, + currentConversation, + connectors, + }: { + isSettingsModalVisible: boolean; + setIsSettingsModalVisible: React.Dispatch>; + shouldShowMissingConnectorCallout: boolean; + currentConversation: Conversation | undefined; + connectors: AIConnector[] | undefined; + }) => { + const { inferenceEnabled } = useAssistantContext(); + const showEISCallout = useMemo(() => { + if (inferenceEnabled && currentConversation && currentConversation.id !== '') { + if (currentConversation?.apiConfig?.connectorId) { + return connectors?.some( + (c) => + c.id === currentConversation.apiConfig?.connectorId && isElasticManagedLlmConnector(c) + ); + } + } + }, [inferenceEnabled, currentConversation, connectors]); + if (shouldShowMissingConnectorCallout) { + return ( + 0} + isSettingsModalVisible={isSettingsModalVisible} + setIsSettingsModalVisible={setIsSettingsModalVisible} + /> + ); + } + + if (showEISCallout) { + return ; + } + + return null; + } +); + +AssistantConversationBanner.displayName = 'AssistantConversationBanner'; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx index 245ce1226028c..d396a53c3e7fc 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx @@ -27,6 +27,8 @@ import { AssistantSettingsModal } from '../settings/assistant_settings_modal'; import { AIConnector } from '../../connectorland/connector_selector'; import { SettingsContextMenu } from '../settings/settings_context_menu/settings_context_menu'; import * as i18n from './translations'; +import { ElasticLLMCostAwarenessTour } from '../../tour/elastic_llm'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../tour/const'; interface OwnProps { selectedConversation: Conversation | undefined; @@ -177,12 +179,18 @@ export const AssistantHeader: React.FC = ({ - + storageKey={NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ASSISTANT_HEADER} + > + + = ({ `} banner={ !isDisabled && - showMissingConnectorCallout && isFetchedConnectors && ( - 0} + ) } diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx index a3c443411e959..fd232c5d7454d 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx @@ -27,7 +27,6 @@ import { 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 '../../../..'; import * as i18n from '../../assistant_header/translations'; @@ -371,15 +370,13 @@ export const SettingsContextMenu: React.FC = React.memo( <> - - + } isOpen={isPopoverOpen} closePopover={closePopover} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx index 6f0c5ab1c1cdf..04a0ae0250d62 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx @@ -14,7 +14,7 @@ import useLocalStorage from 'react-use/lib/useLocalStorage'; import useSessionStorage from 'react-use/lib/useSessionStorage'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import { AssistantFeatures, defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; -import { ChromeStart, NavigateToAppOptions, UserProfileService } from '@kbn/core/public'; +import { ApplicationStart, ChromeStart, UserProfileService } from '@kbn/core/public'; import type { ProductDocBasePluginStart } from '@kbn/product-doc-base-plugin/public'; import { useQuery } from '@tanstack/react-query'; import { updatePromptContexts } from './helpers'; @@ -60,6 +60,9 @@ type ShowAssistantOverlay = ({ promptContextId, selectedConversation, }: ShowAssistantOverlayProps) => void; + +type GetUrlForApp = ApplicationStart['getUrlForApp']; + export interface AssistantProviderProps { actionTypeRegistry: ActionTypeRegistryContract; alertsIndexPattern?: string; @@ -71,13 +74,14 @@ export interface AssistantProviderProps { ) => CodeBlockDetails[][]; basePath: string; basePromptContexts?: PromptContextTemplate[]; - docLinks: Omit; + docLinks: DocLinksStart; children: React.ReactNode; + getUrlForApp: GetUrlForApp; getComments: GetAssistantMessages; http: HttpSetup; inferenceEnabled?: boolean; nameSpace?: string; - navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise; + navigateToApp: ApplicationStart['navigateToApp']; title?: string; toasts?: IToasts; currentAppId: string; @@ -103,15 +107,16 @@ export interface UseAssistantContext { currentConversation: Conversation, showAnonymizedValues: boolean ) => CodeBlockDetails[][]; - docLinks: Omit; + docLinks: DocLinksStart; basePath: string; currentUserAvatar?: UserAvatar; getComments: GetAssistantMessages; + getUrlForApp: GetUrlForApp; http: HttpSetup; inferenceEnabled: boolean; knowledgeBase: KnowledgeBaseConfig; promptContexts: Record; - navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise; + navigateToApp: ApplicationStart['navigateToApp']; nameSpace: string; registerPromptContext: RegisterPromptContext; selectedSettingsTab: ModalSettingsTabs | null; @@ -154,6 +159,7 @@ export const AssistantProvider: React.FC = ({ basePromptContexts = [], children, getComments, + getUrlForApp, http, inferenceEnabled = false, navigateToApp, @@ -294,6 +300,7 @@ export const AssistantProvider: React.FC = ({ currentUserAvatar, docLinks, getComments, + getUrlForApp, http, inferenceEnabled, knowledgeBase: { @@ -343,6 +350,7 @@ export const AssistantProvider: React.FC = ({ currentUserAvatar, docLinks, getComments, + getUrlForApp, http, inferenceEnabled, localStorageKnowledgeBase, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.tsx index eed597548b862..65917d4941a66 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/helpers.tsx @@ -13,6 +13,7 @@ import type { import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types'; import { PRECONFIGURED_CONNECTOR } from './translations'; +import { AIConnector } from './connector_selector'; // aligns with OpenAiProviderType from '@kbn/stack-connectors-plugin/common/openai/types' export enum OpenAiProviderType { @@ -71,3 +72,9 @@ export const getConnectorTypeTitle = ( return actionType; }; + +export const isElasticManagedLlmConnector = ( + connector: + | { actionTypeId: AIConnector['actionTypeId']; isPreconfigured: AIConnector['isPreconfigured'] } + | undefined +) => connector?.actionTypeId === '.inference' && connector?.isPreconfigured; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/use_load_connectors/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/use_load_connectors/index.test.tsx index 396a9e89fc1c9..e1c7fa93116c5 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/use_load_connectors/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/use_load_connectors/index.test.tsx @@ -9,13 +9,6 @@ import { waitFor, renderHook } from '@testing-library/react'; import { useLoadConnectors, Props } from '.'; import { mockConnectors } from '../../mock/connectors'; import { TestProviders } from '../../mock/test_providers/test_providers'; -import { isInferenceEndpointExists } from '@kbn/inference-endpoint-ui-common'; - -const mockedIsInferenceEndpointExists = isInferenceEndpointExists as jest.Mock; - -jest.mock('@kbn/inference-endpoint-ui-common', () => ({ - isInferenceEndpointExists: jest.fn(), -})); const mockConnectorsAndExtras = [ ...mockConnectors, @@ -64,7 +57,6 @@ const defaultProps = { http, toasts } as unknown as Props; describe('useLoadConnectors', () => { beforeEach(() => { jest.clearAllMocks(); - mockedIsInferenceEndpointExists.mockResolvedValue(true); }); it('should call api to load action types', async () => { renderHook(() => useLoadConnectors(defaultProps), { @@ -100,11 +92,6 @@ describe('useLoadConnectors', () => { await waitFor(() => { expect(result.current.data).toStrictEqual( mockConnectors - .filter( - (c) => - c.actionTypeId !== '.inference' || - (c.actionTypeId === '.inference' && c.isPreconfigured) - ) // @ts-ignore ts does not like config, but we define it in the mock data .map((c) => ({ ...c, referencedByCount: 0, apiProvider: c?.config?.apiProvider })) ); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/use_load_connectors/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/use_load_connectors/index.tsx index 6b26be1277927..078a5c483d3e4 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/use_load_connectors/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/connectorland/use_load_connectors/index.tsx @@ -4,17 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { useEffect } from 'react'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import type { ServerError } from '@kbn/cases-plugin/public/types'; import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; import type { IHttpFetchError } from '@kbn/core-http-browser'; import { HttpSetup } from '@kbn/core-http-browser'; -import { isInferenceEndpointExists } from '@kbn/inference-endpoint-ui-common'; import { IToasts } from '@kbn/core-notifications-browser'; import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; -import { ActionConnector } from '@kbn/cases-plugin/public/containers/configure/types'; import { AIConnector } from '../connector_selector'; import * as i18n from '../translations'; @@ -37,42 +35,30 @@ export const useLoadConnectors = ({ toasts, inferenceEnabled = false, }: Props): UseQueryResult => { - if (inferenceEnabled) { - actionTypes.push('.inference'); - } + useEffect(() => { + if (inferenceEnabled && !actionTypes.includes('.inference')) { + actionTypes.push('.inference'); + } + }, [inferenceEnabled]); return useQuery( QUERY_KEY, async () => { - const queryResult = await loadConnectors({ http }); - return queryResult.reduce( - async (acc: Promise, connector) => [ - ...(await acc), - ...(!connector.isMissingSecrets && - actionTypes.includes(connector.actionTypeId) && - // only include preconfigured .inference connectors - (connector.actionTypeId !== '.inference' || - (connector.actionTypeId === '.inference' && - connector.isPreconfigured && - (await isInferenceEndpointExists( - http, - (connector as ActionConnector)?.config?.inferenceId - )))) - ? [ - { - ...connector, - apiProvider: - !connector.isPreconfigured && - !connector.isSystemAction && - connector?.config?.apiProvider - ? (connector?.config?.apiProvider as OpenAiProviderType) - : undefined, - }, - ] - : []), - ], - Promise.resolve([]) - ); + const connectors = await loadConnectors({ http }); + return connectors.reduce((acc: AIConnector[], connector) => { + if (!connector.isMissingSecrets && actionTypes.includes(connector.actionTypeId)) { + acc.push({ + ...connector, + apiProvider: + !connector.isPreconfigured && + !connector.isSystemAction && + connector?.config?.apiProvider + ? (connector?.config?.apiProvider as OpenAiProviderType) + : undefined, + }); + } + return acc; + }, []); }, { retry: false, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx index bf2e9d61e56c2..fc3f9c20a6e69 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx @@ -21,7 +21,7 @@ import { import { useKnowledgeBaseUpdater } from '../../assistant/settings/use_settings_updater/use_knowledge_base_updater'; import { useUpdateKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_update_knowledge_base_entries'; import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt'; -import { useAssistantContext } from '../../..'; +import { AssistantSpaceIdProvider, useAssistantContext } from '../../..'; import { I18nProvider } from '@kbn/i18n-react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useKnowledgeBaseIndices } from '../../assistant/api/knowledge_base/use_knowledge_base_indices'; @@ -69,9 +69,11 @@ const Wrapper = ({ history?: History; }) => ( - - {children} - + + + {children} + + ); describe('KnowledgeBaseSettingsManagement', () => { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx index 9c925be80b101..a37332b2cf68e 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx @@ -16,6 +16,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { UserProfileService } from '@kbn/core/public'; import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks'; import { of } from 'rxjs'; +import { docLinksServiceMock } from '@kbn/core/public/mocks'; import { AssistantProvider, AssistantProviderProps } from '../../assistant_context'; import { AssistantAvailability } from '../../assistant_context/types'; import { AssistantSpaceIdProvider } from '../../assistant/use_space_aware_context'; @@ -55,6 +56,7 @@ export const TestProvidersComponent: React.FC = ({ const mockGetComments = jest.fn(() => []); const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' }); const mockNavigateToApp = jest.fn(); + const mockGetUrlForApp = jest.fn(); const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -80,11 +82,9 @@ export const TestProvidersComponent: React.FC = ({ assistantAvailability={assistantAvailability} augmentMessageCodeBlocks={jest.fn().mockReturnValue([])} basePath={'https://localhost:5601/kbn'} - docLinks={{ - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', - }} + docLinks={docLinksServiceMock.createStartContract()} getComments={mockGetComments} + getUrlForApp={mockGetUrlForApp} http={mockHttp} navigateToApp={mockNavigateToApp} {...providerContext} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/anonymized_values_and_citations_tour/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/anonymized_values_and_citations_tour/index.test.tsx index 179b473e55804..0ee6263d4a2fd 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/anonymized_values_and_citations_tour/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/anonymized_values_and_citations_tour/index.test.tsx @@ -14,8 +14,8 @@ import { conversationWithContentReferences, welcomeConvo, } from '../../mock/conversation'; -import { I18nProvider } from '@kbn/i18n-react'; import { TourState } from '../knowledge_base'; +import { TestProviders } from '../../mock/test_providers/test_providers'; jest.mock('react-use/lib/useLocalStorage', () => jest.fn()); @@ -32,12 +32,12 @@ Object.defineProperty(window, 'localStorage', { }); const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - +
{children}
- + ); describe('AnonymizedValuesAndCitationsTour', () => { @@ -117,8 +117,8 @@ describe('AnonymizedValuesAndCitationsTour', () => { ).not.toBeInTheDocument(); }); - it('does not render tour if the knowledge base tour is on step 1', async () => { - (useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]); + it('does not render tour if the knowledge base tour or EIS tour is on step 1', async () => { + (useLocalStorage as jest.Mock).mockReturnValueOnce([false, jest.fn()]); mockGetItem.mockReturnValue( JSON.stringify({ diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/anonymized_values_and_citations_tour/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/anonymized_values_and_citations_tour/index.tsx index 83b718d999768..326588f9cad50 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/anonymized_values_and_citations_tour/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/anonymized_values_and_citations_tour/index.tsx @@ -17,6 +17,8 @@ import { conversationContainsAnonymizedValues, conversationContainsContentReferences, } from '../../assistant/conversations/utils'; +import { useTourStorageKey } from '../common/hooks/use_tour_storage_key'; +import { EISUsageCostTourState, tourDefaultConfig } from '../elastic_llm/step_config'; interface Props { conversation: Conversation | undefined; @@ -25,8 +27,8 @@ interface Props { // Throttles reads from local storage to 1 every 5 seconds. // This is to prevent excessive reading from local storage. It acts // as a cache. -const getKnowledgeBaseTourStateThrottled = throttle(() => { - const value = localStorage.getItem(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE); +const getKnowledgeBaseTourStateThrottled = throttle((tourStorageKey) => { + const value = localStorage.getItem(tourStorageKey); if (value) { return JSON.parse(value) as TourState; } @@ -34,9 +36,19 @@ const getKnowledgeBaseTourStateThrottled = throttle(() => { }, 5000); export const AnonymizedValuesAndCitationsTour: React.FC = ({ conversation }) => { - const [tourCompleted, setTourCompleted] = useLocalStorage( - NEW_FEATURES_TOUR_STORAGE_KEYS.ANONYMIZED_VALUES_AND_CITATIONS, - false + const kbTourStorageKey = useTourStorageKey(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE); + + const tourStorageKey = useTourStorageKey( + NEW_FEATURES_TOUR_STORAGE_KEYS.ANONYMIZED_VALUES_AND_CITATIONS + ); + const [tourCompleted, setTourCompleted] = useLocalStorage(tourStorageKey, false); + + const eisLLMUsageCostTourStorageKey = useTourStorageKey( + NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ASSISTANT_HEADER + ); + const [eisLLMUsageCostTourState] = useLocalStorage( + eisLLMUsageCostTourStorageKey, + tourDefaultConfig ); const [showTour, setShowTour] = useState(false); @@ -46,10 +58,13 @@ export const AnonymizedValuesAndCitationsTour: React.FC = ({ conversation return; } - const knowledgeBaseTourState = getKnowledgeBaseTourStateThrottled(); + const knowledgeBaseTourState = getKnowledgeBaseTourStateThrottled(kbTourStorageKey); - // If the knowledge base tour is active on this page (i.e. step 1), don't show this tour to prevent overlap. - if (knowledgeBaseTourState?.isTourActive && knowledgeBaseTourState?.currentTourStep === 1) { + // If the knowledge base tour or EIS tour is active on this page (i.e. step 1), don't show this tour to prevent overlap. + if ( + (knowledgeBaseTourState?.isTourActive && knowledgeBaseTourState?.currentTourStep === 1) || + (eisLLMUsageCostTourState?.isTourActive && eisLLMUsageCostTourState?.currentTourStep === 1) + ) { return; } @@ -65,7 +80,15 @@ export const AnonymizedValuesAndCitationsTour: React.FC = ({ conversation clearTimeout(timer); }; } - }, [conversation, tourCompleted, showTour]); + }, [ + conversation, + tourCompleted, + showTour, + kbTourStorageKey, + eisLLMUsageCostTourStorageKey, + eisLLMUsageCostTourState?.isTourActive, + eisLLMUsageCostTourState?.currentTourStep, + ]); const finishTour = useCallback(() => { setTourCompleted(true); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/common/hooks/use_tour_storage_key.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/common/hooks/use_tour_storage_key.test.tsx new file mode 100644 index 0000000000000..cafc48c4aa1ed --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/common/hooks/use_tour_storage_key.test.tsx @@ -0,0 +1,78 @@ +/* + * 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 { renderHook } from '@testing-library/react'; +import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS, NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS } from '../../const'; +import { useTourStorageKey } from './use_tour_storage_key'; + +const featureNumber = Object.keys(NEW_FEATURES_TOUR_STORAGE_KEYS).length; +const testFeatures = [ + { + featureKey: NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE, + expectedStorageKey: + NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS[NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE], + }, + { + featureKey: NEW_FEATURES_TOUR_STORAGE_KEYS.ANONYMIZED_VALUES_AND_CITATIONS, + expectedStorageKey: + NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS[ + NEW_FEATURES_TOUR_STORAGE_KEYS.ANONYMIZED_VALUES_AND_CITATIONS + ], + }, + { + featureKey: NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ATTACK_DISCOVERY, + expectedStorageKey: + NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS[ + NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ATTACK_DISCOVERY + ], + }, + { + featureKey: NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ATTACK_DISCOVERY_FLYOUT, + expectedStorageKey: + NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS[ + NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ATTACK_DISCOVERY_FLYOUT + ], + }, + { + featureKey: NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ASSISTANT_HEADER, + expectedStorageKey: + NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS[ + NEW_FEATURES_TOUR_STORAGE_KEYS.ELASTIC_LLM_USAGE_ASSISTANT_HEADER + ], + }, + { + featureKey: NEW_FEATURES_TOUR_STORAGE_KEYS.CONVERSATION_CONNECTOR_ELASTIC_LLM, + expectedStorageKey: + NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS[ + NEW_FEATURES_TOUR_STORAGE_KEYS.CONVERSATION_CONNECTOR_ELASTIC_LLM + ], + }, +]; + +describe('useTourStorageKey', () => { + test('testFeatures length should match the number of features', () => { + expect(testFeatures.length).toBe(featureNumber); + }); + + test.each(testFeatures)( + 'should return the correct storage key with spaceId for feature $featureKey', + ({ + featureKey, + expectedStorageKey, + }: { + featureKey: NEW_FEATURES_TOUR_STORAGE_KEYS; + expectedStorageKey: string; + }) => { + const spaceId = 'default'; + const { result } = renderHook(() => useTourStorageKey(featureKey), { + wrapper: ({ children }) => {children}, + }); + expect(result.current).toBe(`${expectedStorageKey}.${spaceId}`); + } + ); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/common/hooks/use_tour_storage_key.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/common/hooks/use_tour_storage_key.ts new file mode 100644 index 0000000000000..1e8e32866a82b --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/common/hooks/use_tour_storage_key.ts @@ -0,0 +1,22 @@ +/* + * 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 { useAssistantSpaceId } from '../../../assistant/use_space_aware_context'; +import { + NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS, + type NEW_FEATURES_TOUR_STORAGE_KEYS, +} from '../../const'; + +/** + * + * @param featureKey The key of the feature for storage key + * @returns A unique storage key for the feature based on the space ID + */ +export const useTourStorageKey = (featureKey: NEW_FEATURES_TOUR_STORAGE_KEYS) => { + const spaceId = useAssistantSpaceId(); + return `${NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS[featureKey]}.${spaceId}`; +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/const.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/const.ts index 91380d73cca5e..d6724d2a9eb37 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/const.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/const.ts @@ -5,8 +5,25 @@ * 2.0. */ -export const NEW_FEATURES_TOUR_STORAGE_KEYS = { +export enum NEW_FEATURES_TOUR_STORAGE_KEYS { + KNOWLEDGE_BASE = 'KNOWLEDGE_BASE', + ANONYMIZED_VALUES_AND_CITATIONS = 'ANONYMIZED_VALUES_AND_CITATIONS', + ELASTIC_LLM_USAGE_ATTACK_DISCOVERY = 'ELASTIC_LLM_USAGE_ATTACK_DISCOVERY', + ELASTIC_LLM_USAGE_ATTACK_DISCOVERY_FLYOUT = 'ELASTIC_LLM_USAGE_ATTACK_DISCOVERY_FLYOUT', + ELASTIC_LLM_USAGE_ASSISTANT_HEADER = 'ELASTIC_LLM_USAGE_ASSISTANT_HEADER', + CONVERSATION_CONNECTOR_ELASTIC_LLM = 'CONVERSATION_CONNECTOR_ELASTIC_LLM', +} + +export const NEW_TOUR_FEATURES_TOUR_STORAGE_KEYS: Record = { KNOWLEDGE_BASE: 'elasticAssistant.knowledgeBase.newFeaturesTour.v8.16', ANONYMIZED_VALUES_AND_CITATIONS: 'elasticAssistant.anonymizedValuesAndCitationsTourCompleted.v8.18', + ELASTIC_LLM_USAGE_ATTACK_DISCOVERY: + 'elasticAssistant.elasticLLM.costAwarenessTour.attackDiscovery.v8.19', + ELASTIC_LLM_USAGE_ATTACK_DISCOVERY_FLYOUT: + 'elasticAssistant.elasticLLM.costAwarenessTour.attackDiscoveryFlyout.v8.19', + ELASTIC_LLM_USAGE_ASSISTANT_HEADER: + 'elasticAssistant.elasticLLM.costAwarenessTour.assistantHeader.v8.19', + CONVERSATION_CONNECTOR_ELASTIC_LLM: + 'elasticAssistant.elasticLLM.conversation.costAwarenessTour.v8.19', }; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/index.test.tsx new file mode 100644 index 0000000000000..dd2a5e12f61d7 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/index.test.tsx @@ -0,0 +1,130 @@ +/* + * 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 { render, waitFor } from '@testing-library/react'; +import { ElasticLLMCostAwarenessTour } from '.'; +import React from 'react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { I18nProvider } from '@kbn/i18n-react'; +import { act } from 'react-dom/test-utils'; +import { useAssistantContext } from '../../assistant_context'; +import { useLoadConnectors } from '../../connectorland/use_load_connectors'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../const'; +import { docLinksServiceMock } from '@kbn/core/public/mocks'; + +jest.mock('react-use/lib/useLocalStorage', () => jest.fn()); +jest.mock('../common/hooks/use_tour_storage_key'); +jest.mock('../../assistant_context'); +jest.mock('../../connectorland/use_load_connectors', () => ({ + useLoadConnectors: jest.fn(), +})); + +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + throttle: jest.fn().mockImplementation((fn) => fn), +})); + +const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} +); + +describe('ElasticLLMCostAwarenessTour', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + (useAssistantContext as jest.Mock).mockReturnValue({ + inferenceEnabled: true, + docLinks: docLinksServiceMock.createStartContract(), + }); + (useLoadConnectors as jest.Mock).mockReturnValue({ + data: [ + { + id: '.inference', + actionTypeId: '.inference', + isPreconfigured: true, + }, + ], + }); + }); + + it('renders tour when there are content references', async () => { + (useLocalStorage as jest.Mock).mockReturnValue([ + { + currentTourStep: 1, + isTourActive: true, + }, + jest.fn(), + ]); + + const { queryByTestId } = render( + +
+ , + { + wrapper: Wrapper, + } + ); + + act(() => { + jest.runAllTimers(); + }); + + await waitFor(() => { + expect(queryByTestId('elasticLLMTourStepPanel')).toBeInTheDocument(); + }); + }); + + it('does not render tour if it has already been shown', async () => { + (useLocalStorage as jest.Mock).mockReturnValue([true, jest.fn()]); + + const { queryByTestId } = render( + , + { + wrapper: Wrapper, + } + ); + + act(() => { + jest.runAllTimers(); + }); + + await waitFor(() => { + expect(queryByTestId('elasticLLMTourStepPanel')).not.toBeInTheDocument(); + }); + }); + + it('does not render tour if disabled', async () => { + (useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]); + + const { queryByTestId } = render( + , + { + wrapper: Wrapper, + } + ); + + act(() => { + jest.runAllTimers(); + }); + + await waitFor(() => { + expect(queryByTestId('elasticLLMTourStepPanel')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/index.tsx new file mode 100644 index 0000000000000..80be8ed1e6dcb --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/index.tsx @@ -0,0 +1,169 @@ +/* + * 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 { + EuiButtonEmpty, + EuiText, + EuiTitle, + EuiTourStep, + EuiTourStepProps, + useEuiTheme, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { css } from '@emotion/react'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../const'; +import { EISUsageCostTourState, elasticLLMTourStep1, tourDefaultConfig } from './step_config'; +import { ELASTIC_LLM_TOUR_FINISH_TOUR } from './translations'; +import { useAssistantContext } from '../../assistant_context'; +import { useLoadConnectors } from '../../connectorland/use_load_connectors'; +import { isElasticManagedLlmConnector } from '../../connectorland/helpers'; +import { useTourStorageKey } from '../common/hooks/use_tour_storage_key'; + +interface Props { + children?: EuiTourStepProps['children']; + isDisabled: boolean; + selectedConnectorId: string | undefined; + storageKey: NEW_FEATURES_TOUR_STORAGE_KEYS; + zIndex?: number; + wrapper?: boolean; +} + +const ElasticLLMCostAwarenessTourComponent: React.FC = ({ + children, + isDisabled, + selectedConnectorId, + storageKey, + zIndex, + wrapper = true, // Whether to wrap the children in a div with padding +}) => { + const { http, inferenceEnabled } = useAssistantContext(); + const { euiTheme } = useEuiTheme(); + const tourStorageKey = useTourStorageKey(storageKey); + const [tourState, setTourState] = useLocalStorage( + tourStorageKey, + tourDefaultConfig + ); + + const [showTour, setShowTour] = useState(!tourState?.isTourActive && !isDisabled); + + const [isTimerExhausted, setIsTimerExhausted] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setIsTimerExhausted(true); + }, 1000); + + return () => clearTimeout(timer); + }, []); + + const finishTour = useCallback(() => { + setTourState((prev = tourDefaultConfig) => ({ + ...prev, + isTourActive: false, + })); + setShowTour(false); + }, [setTourState, setShowTour]); + + const { data: aiConnectors } = useLoadConnectors({ + http, + inferenceEnabled, + }); + const isElasticLLMConnectorSelected = useMemo( + () => + aiConnectors?.some((c) => isElasticManagedLlmConnector(c) && c.id === selectedConnectorId), + [aiConnectors, selectedConnectorId] + ); + + useEffect(() => { + if ( + !inferenceEnabled || + isDisabled || + !tourState?.isTourActive || + aiConnectors?.length === 0 || + !isElasticLLMConnectorSelected + ) { + setShowTour(false); + } else { + setShowTour(true); + } + }, [ + tourState, + isDisabled, + children, + showTour, + inferenceEnabled, + aiConnectors?.length, + isElasticLLMConnectorSelected, + setTourState, + ]); + + if (!children) { + return null; + } + + if (!showTour) { + return children; + } + + return ( + {elasticLLMTourStep1.content}} + // Open the tour step after flyout is open + isStepOpen={isTimerExhausted} + maxWidth={384} + onFinish={finishTour} + panelProps={{ + 'data-test-subj': `elasticLLMTourStepPanel`, + }} + step={1} + stepsTotal={1} + title={ + + {elasticLLMTourStep1.title} + + } + subtitle={ + + {elasticLLMTourStep1.subTitle} + + } + footerAction={[ + + {ELASTIC_LLM_TOUR_FINISH_TOUR} + , + ]} + panelStyle={{ + fontSize: euiTheme.size.m, + }} + zIndex={zIndex} + > + {wrapper ? ( +
+ {children} +
+ ) : ( + children + )} +
+ ); +}; + +export const ElasticLLMCostAwarenessTour = React.memo(ElasticLLMCostAwarenessTourComponent); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/messages.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/messages.tsx new file mode 100644 index 0000000000000..51d058dc74032 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/messages.tsx @@ -0,0 +1,48 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink } from '@elastic/eui'; +import * as i18n from './translations'; +import { useAssistantContext } from '../../assistant_context'; + +export const CostAwareness = () => { + const { + docLinks: { + links: { + observability: { + elasticManagedLlm: ELASTIC_LLM_LINK, + elasticManagedLlmUsageCost: ELASTIC_LLM_USAGE_COST_LINK, + }, + }, + }, + } = useAssistantContext(); + + return ( + + {i18n.ELASTIC_LLM_USAGE_COSTS} + + ), + learnMore: ( + + {i18n.ELASTIC_LLM_TOUR_LEARN_MORE} + + ), + }} + /> + ); +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/step_config.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/step_config.tsx new file mode 100644 index 0000000000000..48cd87387476f --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/step_config.tsx @@ -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. + */ + +import React from 'react'; +import * as i18n from './translations'; +import { CostAwareness } from './messages'; + +export interface EISUsageCostTourState { + currentTourStep: number; + isTourActive: boolean; +} + +export const tourDefaultConfig: EISUsageCostTourState = { + currentTourStep: 1, + isTourActive: true, +}; + +export const elasticLLMTourStep1 = { + title: i18n.ELASTIC_LLM_TOUR_TITLE, + subTitle: i18n.ELASTIC_LLM_TOUR_SUBTITLE, + content: , +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/translations.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/translations.ts new file mode 100644 index 0000000000000..e039a7660b3f4 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/elastic_llm/translations.ts @@ -0,0 +1,71 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ELASTIC_LLM_TOUR_TITLE = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.title', + { + defaultMessage: 'Elastic Managed LLM connector now available', + } +); + +export const ELASTIC_LLM_TOUR_SUBTITLE = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.subtitle', + { + defaultMessage: 'New AI feature!', + } +); + +export const ELASTIC_LLM_TOUR_LEARN_MORE = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.learnMore', + { + defaultMessage: 'Learn more', + } +); + +export const ELASTIC_LLM_TOUR_FINISH_TOUR = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.finishTour', + { + defaultMessage: 'Ok', + } +); + +export const ELASTIC_LLM_AS_DEFAULT_CONNECTOR = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.asDefaultConnector', + { + defaultMessage: 'Make Elastic Managed LLM the default connector', + } +); + +export const ELASTIC_LLM_AI_FEATURES = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.aiFeature', + { + defaultMessage: 'Elastic AI features', + } +); + +export const ELASTIC_LLM_USAGE_COSTS = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.usageCost', + { + defaultMessage: 'additional costs incur', + } +); + +export const ELASTIC_LLM_THIRD_PARTY = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.thirdParty', + { + defaultMessage: 'connect to a third party LLM provider', + } +); + +export const ELASTIC_LLM_TOUR_PERFORMANCE = i18n.translate( + 'xpack.elasticAssistant.elasticLLM.tour.performance', + { + defaultMessage: 'performance', + } +); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx index d7fb792dd413b..3d171f919b06b 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx @@ -20,6 +20,7 @@ import { VideoToast } from './video_toast'; import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../const'; import { knowledgeBaseTourStepOne, tourConfig } from './step_config'; import * as i18n from './translations'; +import { useTourStorageKey } from '../common/hooks/use_tour_storage_key'; export interface TourState { currentTourStep: number; @@ -48,10 +49,8 @@ const KnowledgeBaseTourComp: React.FC<{ }> = ({ children, isKbSettingsPage = false }) => { const { navigateToApp } = useAssistantContext(); - const [tourState, setTourState] = useLocalStorage( - NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE, - tourConfig - ); + const tourStorageKey = useTourStorageKey(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE); + const [tourState, setTourState] = useLocalStorage(tourStorageKey, tourConfig); const advanceToVideoStep = useCallback( () => diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json b/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json index e8e259ab65158..dd245693986ec 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/tsconfig.json @@ -41,7 +41,6 @@ "@kbn/product-doc-base-plugin", "@kbn/spaces-plugin", "@kbn/shared-ux-router", - "@kbn/inference-endpoint-ui-common", "@kbn/datemath", "@kbn/alerts-ui-shared", "@kbn/deeplinks-security" diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 9a39a42121495..f36890e0ae229 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -46869,10 +46869,8 @@ "xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(déclassé)", "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{connectorTypeDesc}", "xpack.triggersActionsUI.sections.editConnectorForm.closeButtonLabel": "Fermer", - "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "Ce connecteur est en lecture seule.", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "Modifier un connecteur", "xpack.triggersActionsUI.sections.editConnectorForm.headerFormLabel": "Le formulaire comprend des erreurs", - "xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel": "En savoir plus sur les connecteurs préconfigurés.", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel": "Enregistrer", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonSavedLabel": "Modifications enregistrées", "xpack.triggersActionsUI.sections.editConnectorForm.tabText": "Configuration", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index f160fe907b524..9b48123193d9f 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -46822,10 +46822,8 @@ "xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(非推奨)", "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{connectorTypeDesc}", "xpack.triggersActionsUI.sections.editConnectorForm.closeButtonLabel": "閉じる", - "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "このコネクターは読み取り専用です。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "コネクターを編集", "xpack.triggersActionsUI.sections.editConnectorForm.headerFormLabel": "フォームにエラーがあります", - "xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel": "あらかじめ構成されたコネクターの詳細をご覧ください。", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonSavedLabel": "変更が保存されました", "xpack.triggersActionsUI.sections.editConnectorForm.tabText": "構成", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 4cd228ff04aad..906678302ea27 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -46910,10 +46910,8 @@ "xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(已过时)", "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{connectorTypeDesc}", "xpack.triggersActionsUI.sections.editConnectorForm.closeButtonLabel": "关闭", - "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "此连接器为只读。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "编辑连接器", "xpack.triggersActionsUI.sections.editConnectorForm.headerFormLabel": "表单中存在错误", - "xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel": "详细了解预配置的连接器。", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.editConnectorForm.saveButtonSavedLabel": "已保存更改", "xpack.triggersActionsUI.sections.editConnectorForm.tabText": "配置", diff --git a/x-pack/platform/plugins/shared/automatic_import/kibana.jsonc b/x-pack/platform/plugins/shared/automatic_import/kibana.jsonc index ac93c97c76466..e15d94f74c720 100644 --- a/x-pack/platform/plugins/shared/automatic_import/kibana.jsonc +++ b/x-pack/platform/plugins/shared/automatic_import/kibana.jsonc @@ -21,8 +21,7 @@ "stackConnectors", ], "requiredBundles": [ - "kibanaUtils", - "esUiShared", + "kibanaUtils" ] } } diff --git a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/connector_step/connector_step.tsx b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/connector_step/connector_step.tsx index 8b770c08746f5..71dad40832ac3 100644 --- a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/connector_step/connector_step.tsx +++ b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_automatic_import/steps/connector_step/connector_step.tsx @@ -19,6 +19,13 @@ import { EuiIcon, useEuiTheme, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + ELASTIC_LLM_USAGE_COSTS, + ELASTIC_LLM_THIRD_PARTY, + ELASTIC_LLM_TOUR_PERFORMANCE, +} from '@kbn/elastic-assistant/impl/tour/elastic_llm/translations'; +import { isElasticManagedLlmConnector } from '@kbn/elastic-assistant/impl/connectorland/helpers'; import { AuthorizationWrapper, MissingPrivilegesTooltip, @@ -37,9 +44,54 @@ import * as i18n from './translations'; */ const AllowedActionTypeIds = ['.bedrock', '.gen-ai', '.gemini']; +const ElasticLLMNewIntegrationMessage = React.memo(() => { + const { + docLinks: { + links: { + securitySolution: { + thirdPartyLlmProviders: THIRD_PARTY_LLM_LINK, + llmPerformanceMatrix: LLM_PERFORMANCE_LINK, + }, + observability: { elasticManagedLlmUsageCost: ELASTIC_LLM_USAGE_COST_LINK }, + }, + }, + } = useKibana().services; + + return ( + + {ELASTIC_LLM_USAGE_COSTS} + + ), + thirdParty: ( + + {ELASTIC_LLM_THIRD_PARTY} + + ), + performance: ( + + {ELASTIC_LLM_TOUR_PERFORMANCE} + + ), + }} + /> + ); +}); +ElasticLLMNewIntegrationMessage.displayName = 'ElasticLLMNewIntegrationMessage'; + interface ConnectorStepProps { connector: AIConnector | undefined; } + export const ConnectorStep = React.memo(({ connector }) => { const { euiTheme } = useEuiTheme(); const { http, notifications, triggersActionsUi } = useKibana().services; @@ -119,14 +171,22 @@ export const ConnectorStep = React.memo(({ connector }) => { - - - - - - {i18n.SUPPORTED_MODELS_INFO} - - + + + + + + + + + {inferenceEnabled && isElasticManagedLlmConnector(connector) ? ( + + ) : ( + i18n.SUPPORTED_MODELS_INFO + )} + + + ); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/inference/inference.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/inference/inference.tsx index b5d90cab4a18a..e65cdf187cba5 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/inference/inference.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/inference/inference.tsx @@ -112,5 +112,6 @@ export function getConnectorType(): InferenceConnector { }, actionConnectorFields: lazy(() => import('./connector')), actionParamsFields: lazy(() => import('./params')), + actionReadOnlyExtraComponent: lazy(() => import('./usage_cost_message')), }; } diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/inference/usage_cost_message.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/inference/usage_cost_message.tsx new file mode 100644 index 0000000000000..030783ff1e3d2 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/inference/usage_cost_message.tsx @@ -0,0 +1,54 @@ +/* + * 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 { EuiLink, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/triggers-actions-ui-plugin/public/common'; + +export const UsageCostMessage: React.FC = () => { + const { docLinks } = useKibana().services; + return ( + + + + + ), + usageCost: ( + + + + ), + }} + /> + + ); +}; +// eslint-disable-next-line import/no-default-export +export { UsageCostMessage as default }; diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/index.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/index.ts index ab1e506993564..7e45847f937d5 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { i18n } from '@kbn/i18n'; import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types'; import { ValidatorType } from '@kbn/actions-plugin/server/sub_action_framework/types'; diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx index eb114ec4e165d..704a3f501a8f7 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Theme } from '@elastic/charts'; -import { coreMock } from '@kbn/core/public/mocks'; +import { coreMock, docLinksServiceMock } from '@kbn/core/public/mocks'; import { UserProfileService } from '@kbn/core/public'; import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks'; import { of } from 'rxjs'; @@ -68,6 +68,7 @@ const TestExternalProvidersComponent: React.FC = ({ error: () => {}, }, }); + const chrome = chromeServiceMock.createStartContract(); chrome.getChromeStyle$.mockReturnValue(of('classic')); @@ -81,10 +82,7 @@ const TestExternalProvidersComponent: React.FC = ({ assistantAvailability={mockAssistantAvailability} augmentMessageCodeBlocks={jest.fn()} basePath={'https://localhost:5601/kbn'} - docLinks={{ - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', - }} + docLinks={docLinksServiceMock.createStartContract()} getComments={mockGetComments} http={mockHttp} navigateToApp={mockNavigateToApp} @@ -93,6 +91,7 @@ const TestExternalProvidersComponent: React.FC = ({ }} currentAppId={'securitySolutionUI'} userProfileService={jest.fn() as unknown as UserProfileService} + getUrlForApp={jest.fn()} chrome={chrome} > {children} diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx index 94baa67ea6e0e..97c5d0c58c24f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx @@ -135,12 +135,12 @@ export const createBasePrompts = async (notifications: NotificationsStart, http: */ export const AssistantProvider: FC> = ({ children }) => { const { - application: { navigateToApp, currentAppId$ }, + application: { navigateToApp, getUrlForApp, currentAppId$ }, http, notifications, storage, triggersActionsUi: { actionTypeRegistry }, - docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + docLinks, userProfile, chrome, productDocBase, @@ -226,9 +226,10 @@ export const AssistantProvider: FC> = ({ children }) augmentMessageCodeBlocks={augmentMessageCodeBlocks} assistantAvailability={assistantAvailability} assistantTelemetry={assistantTelemetry} - docLinks={{ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }} + docLinks={docLinks} basePath={basePath} basePromptContexts={Object.values(PROMPT_CONTEXTS)} + getUrlForApp={getUrlForApp} getComments={getComments} http={http} inferenceEnabled={inferenceEnabled} diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx index 9a36bbac86c8a..e0aab75f2a880 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx @@ -30,6 +30,9 @@ jest.mock('@kbn/elastic-assistant/impl/assistant/use_conversation', () => ({ jest.mock('../../common/lib/kibana', () => ({ useKibana: jest.fn(), })); +jest.mock('../../common/hooks/use_space_id', () => ({ + useSpaceId: jest.fn().mockReturnValue('default'), +})); const useAssistantContextMock = useAssistantContext as jest.Mock; const useFetchCurrentUserConversationsMock = useFetchCurrentUserConversations as jest.Mock; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.tsx index b8b3b443763b6..08f14861860a8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/stack_management/management_settings.tsx @@ -12,7 +12,10 @@ import { i18n } from '@kbn/i18n'; import { SECURITY_AI_SETTINGS } from '@kbn/elastic-assistant/impl/assistant/settings/translations'; import { CONVERSATIONS_TAB } from '@kbn/elastic-assistant/impl/assistant/settings/const'; import type { ManagementSettingsTabs } from '@kbn/elastic-assistant/impl/assistant/settings/types'; + +import { AssistantSpaceIdProvider } from '@kbn/elastic-assistant/impl/assistant/use_space_aware_context'; import { useKibana } from '../../common/lib/kibana'; +import { useSpaceId } from '../../common/hooks/use_space_id'; export const ManagementSettings = React.memo(() => { const { @@ -26,6 +29,7 @@ export const ManagementSettings = React.memo(() => { chrome: { docTitle, setBreadcrumbs }, serverless, } = useKibana().services; + const spaceId = useSpaceId(); docTitle.change(SECURITY_AI_SETTINGS); @@ -92,13 +96,15 @@ export const ManagementSettings = React.memo(() => { navigateToApp('home'); } - return ( - - ); + return spaceId ? ( + + + + ) : null; }); ManagementSettings.displayName = 'ManagementSettings'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx index 7f8fc1beeffc0..9190f1428ec38 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx @@ -13,9 +13,13 @@ import { useAssistantAvailability } from '../../../assistant/use_assistant_avail import { useKibana } from '../../../common/lib/kibana'; import { TestProviders } from '../../../common/mock'; import { Header } from '.'; +import { useSpaceId } from '../../../common/hooks/use_space_id'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../assistant/use_assistant_availability'); +jest.mock('../../../common/hooks/use_space_id', () => ({ + useSpaceId: jest.fn(), +})); const mockUseKibana = useKibana as jest.MockedFunction; @@ -31,12 +35,15 @@ const defaultProps = { onConnectorIdSelected: jest.fn(), openFlyout: jest.fn(), setLocalStorageAttackDiscoveryMaxAlerts: jest.fn(), + showFlyout: false, }; describe('Actions', () => { beforeEach(() => { jest.clearAllMocks(); + (useSpaceId as jest.Mock).mockReturnValue('default'); + (useAssistantAvailability as jest.Mock).mockReturnValue({ hasAssistantPrivilege: true, isAssistantEnabled: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/header/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/header/index.tsx index 1d5ba0529efc3..db58ff0f7d6e7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/header/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/header/index.tsx @@ -15,14 +15,21 @@ import { useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; -import { ConnectorSelectorInline } from '@kbn/elastic-assistant'; +import { + AssistantSpaceIdProvider, + ConnectorSelectorInline, + useAssistantContext, +} from '@kbn/elastic-assistant'; import { type AttackDiscoveryStats } from '@kbn/elastic-assistant-common'; import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { ElasticLLMCostAwarenessTour } from '@kbn/elastic-assistant/impl/tour/elastic_llm'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '@kbn/elastic-assistant/impl/tour/const'; import { StatusBell } from './status_bell'; import * as i18n from './translations'; import { useKibanaFeatureFlags } from '../use_kibana_feature_flags'; +import { useSpaceId } from '../../../common/hooks/use_space_id'; interface Props { connectorId: string | undefined; @@ -34,6 +41,7 @@ interface Props { onConnectorIdSelected: (connectorId: string) => void; openFlyout: () => void; stats: AttackDiscoveryStats | null; + showFlyout: boolean; } const HeaderComponent: React.FC = ({ @@ -46,11 +54,43 @@ const HeaderComponent: React.FC = ({ onCancel, openFlyout, stats, + showFlyout, }) => { const { euiTheme } = useEuiTheme(); const disabled = connectorId == null; const [didCancel, setDidCancel] = useState(false); + const { inferenceEnabled } = useAssistantContext(); const { attackDiscoveryAlertsEnabled } = useKibanaFeatureFlags(); + const spaceId = useSpaceId(); + + const [isEISCostTourDisabled, setIsEISCostTourDisabled] = useState( + attackDiscoveryAlertsEnabled || + !connectorsAreConfigured || + !spaceId || + !inferenceEnabled || + showFlyout + ); + + useEffect(() => { + if ( + attackDiscoveryAlertsEnabled || + !connectorsAreConfigured || + !spaceId || + !inferenceEnabled || + showFlyout + ) { + setIsEISCostTourDisabled(true); + } else { + setIsEISCostTourDisabled(false); + } + }, [ + attackDiscoveryAlertsEnabled, + connectorsAreConfigured, + inferenceEnabled, + isEISCostTourDisabled, + showFlyout, + spaceId, + ]); const handleCancel = useCallback(() => { setDidCancel(true); @@ -103,6 +143,7 @@ const HeaderComponent: React.FC = ({ {!attackDiscoveryAlertsEnabled && connectorsAreConfigured && ( = ({ - + {spaceId && ( + + + + + + )} )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/index.tsx index 9f1718914c194..2f03c7155bbc2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/index.tsx @@ -292,6 +292,7 @@ const AttackDiscoveryPageComponent: React.FC = () => { onGenerate={onGenerate} openFlyout={openFlyout} stats={stats} + showFlyout={showFlyout} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/alert_selection/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/alert_selection/index.test.tsx index 7edb416470250..82f41cf0d3c33 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/alert_selection/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/alert_selection/index.test.tsx @@ -24,6 +24,9 @@ jest.mock('react-router', () => ({ })); jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../sourcerer/containers'); +jest.mock('../../../../common/hooks/use_space_id', () => ({ + useSpaceId: jest.fn().mockReturnValue('default'), +})); const defaultProps = { connectorId: undefined, diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/alert_selection/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/alert_selection/index.tsx index 1c23fd623c862..70babff083996 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/alert_selection/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/alert_selection/index.tsx @@ -5,13 +5,16 @@ * 2.0. */ -import { ConnectorSelectorInline } from '@kbn/elastic-assistant'; +import { AssistantSpaceIdProvider, ConnectorSelectorInline } from '@kbn/elastic-assistant'; import type { AttackDiscoveryStats } from '@kbn/elastic-assistant-common'; import { EuiForm, EuiFormRow, EuiTab, EuiTabs, EuiText, EuiSpacer } from '@elastic/eui'; import type { FilterManager } from '@kbn/data-plugin/public'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; +import { ElasticLLMCostAwarenessTour } from '@kbn/elastic-assistant/impl/tour/elastic_llm'; +import { css } from '@emotion/react'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '@kbn/elastic-assistant/impl/tour/const'; import { AlertSelectionQuery } from './alert_selection_query'; import { AlertSelectionRange } from './alert_selection_range'; import { getMaxAlerts } from './helpers/get_max_alerts'; @@ -19,6 +22,7 @@ import { getTabs } from './helpers/get_tabs'; import * as i18n from './translations'; import type { AlertsSelectionSettings } from '../types'; import { useKibanaFeatureFlags } from '../../use_kibana_feature_flags'; +import { useSpaceId } from '../../../../common/hooks/use_space_id'; interface Props { alertsPreviewStackBy0: string; @@ -48,6 +52,7 @@ const AlertSelectionComponent: React.FC = ({ stats, }) => { const { attackDiscoveryAlertsEnabled } = useKibanaFeatureFlags(); + const spaceId = useSpaceId(); const tabs = useMemo( () => @@ -87,8 +92,8 @@ const AlertSelectionComponent: React.FC = ({ return ( - {showConnectorSelector && ( - <> + {showConnectorSelector && spaceId && ( + @@ -97,18 +102,29 @@ const AlertSelectionComponent: React.FC = ({ - - - - + + + + + - + )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/hooks/use_settings_view.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/hooks/use_settings_view.test.tsx index aafef78022891..694a8c5f906a1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/hooks/use_settings_view.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/hooks/use_settings_view.test.tsx @@ -25,6 +25,11 @@ jest.mock('react-router', () => ({ })); jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../sourcerer/containers'); +jest.mock('../../../../common/hooks/use_space_id', () => { + return { + useSpaceId: jest.fn().mockReturnValue('default'), + }; +}); const defaultProps = { connectorId: undefined, diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx index adabe515bd47a..29965d1bc61ff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx @@ -9,10 +9,11 @@ import { httpServiceMock } from '@kbn/core-http-browser-mocks'; import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; import React from 'react'; import type { AssistantAvailability } from '@kbn/elastic-assistant'; -import { AssistantProvider } from '@kbn/elastic-assistant'; +import { AssistantProvider, AssistantSpaceIdProvider } from '@kbn/elastic-assistant'; import type { UserProfileService } from '@kbn/core/public'; import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks'; import { of } from 'rxjs'; +import { docLinksServiceMock } from '@kbn/core/public/mocks'; interface Props { assistantAvailability?: AssistantAvailability; @@ -52,11 +53,9 @@ export const MockAssistantProviderComponent: React.FC = ({ assistantAvailability={assistantAvailability ?? defaultAssistantAvailability} augmentMessageCodeBlocks={jest.fn(() => [])} basePath={'https://localhost:5601/kbn'} - docLinks={{ - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', - }} + docLinks={docLinksServiceMock.createStartContract()} getComments={jest.fn(() => [])} + getUrlForApp={jest.fn()} http={mockHttp} navigateToApp={mockNavigateToApp} currentAppId={'test'} @@ -66,7 +65,7 @@ export const MockAssistantProviderComponent: React.FC = ({ userProfileService={mockUserProfileService} chrome={chrome} > - {children} + {children} ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.test.tsx index 374cbad6693ff..002a91d997220 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.test.tsx @@ -16,6 +16,9 @@ import { SecurityPageName } from '@kbn/deeplinks-security'; const mockNavigateTo = jest.fn(); jest.mock('../../common/lib/kibana'); +jest.mock('../../common/hooks/use_space_id', () => ({ + useSpaceId: jest.fn().mockReturnValue('default'), +})); describe('AISettings', () => { beforeEach(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.tsx index 6c7b313eac4fb..c6963e4f19ceb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/ai_settings.tsx @@ -10,10 +10,12 @@ import { SearchAILakeConfigurationsSettingsManagement, CONVERSATIONS_TAB, type ManagementSettingsTabs, + AssistantSpaceIdProvider, } from '@kbn/elastic-assistant'; import { useSearchParams } from 'react-router-dom-v5-compat'; import { SecurityPageName } from '../../../common/constants'; import { useKibana, useNavigation } from '../../common/lib/kibana'; +import { useSpaceId } from '../../common/hooks/use_space_id'; export const AISettings: React.FC = () => { const { navigateTo } = useNavigation(); @@ -26,7 +28,7 @@ export const AISettings: React.FC = () => { }, data: { dataViews }, } = useKibana().services; - + const spaceId = useSpaceId(); const onTabChange = useCallback( (tab: string) => { navigateTo({ @@ -45,11 +47,13 @@ export const AISettings: React.FC = () => { if (!securityAIAssistantEnabled) { navigateToApp('home'); } - return ( - - ); + return spaceId ? ( + + + + ) : null; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/ai_feature_message.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/ai_feature_message.tsx new file mode 100644 index 0000000000000..95afdbfba313c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/ai_feature_message.tsx @@ -0,0 +1,65 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { + ELASTIC_LLM_AI_FEATURES, + ELASTIC_LLM_THIRD_PARTY, + ELASTIC_LLM_USAGE_COSTS, +} from '@kbn/elastic-assistant/impl/tour/elastic_llm/translations'; +import { useKibana } from '../../../../../common/lib/kibana'; + +export const ElasticAIFeatureMessage = React.memo(() => { + const { + docLinks: { + links: { + securitySolution: { + elasticAiFeatures: ELASTIC_AI_FEATURES_LINK, + thirdPartyLlmProviders: THIRD_PARTY_LLM_LINK, + }, + observability: { elasticManagedLlmUsageCost: ELASTIC_LLM_USAGE_COST_LINK }, + }, + }, + } = useKibana().services; + + return ( + + {ELASTIC_LLM_AI_FEATURES} + + ), + usageCost: ( + + {ELASTIC_LLM_USAGE_COSTS} + + ), + thirdParty: ( + + {ELASTIC_LLM_THIRD_PARTY} + + ), + }} + /> + ); +}); + +ElasticAIFeatureMessage.displayName = 'ElasticAIFeatureMessage'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx index 7ed02852d4d73..62c3be16e7cf2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx @@ -16,8 +16,12 @@ import { import { useCurrentConversation } from '@kbn/elastic-assistant/impl/assistant/use_current_conversation'; import { useDataStreamApis } from '@kbn/elastic-assistant/impl/assistant/use_data_stream_apis'; import { getDefaultConnector } from '@kbn/elastic-assistant/impl/assistant/helpers'; -import { getGenAiConfig } from '@kbn/elastic-assistant/impl/connectorland/helpers'; +import { + getGenAiConfig, + isElasticManagedLlmConnector, +} from '@kbn/elastic-assistant/impl/connectorland/helpers'; import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conversation'; + import { CenteredLoadingSpinner } from '../../../../../common/components/centered_loading_spinner'; import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; @@ -31,6 +35,7 @@ import { CardCallOut } from '../common/card_callout'; import { CardSubduedText } from '../common/card_subdued_text'; import type { AIConnector } from '../common/connectors/types'; import type { AssistantCardMetadata } from './types'; +import { ElasticAIFeatureMessage } from './ai_feature_message'; export const AssistantCard: OnboardingCardComponent = ({ isCardComplete, @@ -135,6 +140,11 @@ export const AssistantCard: OnboardingCardComponent = ({ [currentConversation, setApiConfig, onConversationChange, setSelectedConnectorId] ); + const isEISConnectorAvailable = useMemo( + () => connectors?.some((c) => isElasticManagedLlmConnector(c)) ?? false, + [connectors] + ); + if (!checkCompleteMetadata) { return ( @@ -153,7 +163,13 @@ export const AssistantCard: OnboardingCardComponent = ({ {canExecuteConnectors ? ( - {i18n.ASSISTANT_CARD_DESCRIPTION} + + {isEISConnectorAvailable ? ( + + ) : ( + i18n.ASSISTANT_CARD_DESCRIPTION + )} + {isIntegrationsCardAvailable && !isIntegrationsCardComplete ? ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.test.tsx index 5c05ed008ad3c..006fb96bb33a7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.test.tsx @@ -73,4 +73,25 @@ describe('ConnectorSelectorPanel', () => { await userEvent.click(screen.getByText('Connector 2')); expect(onConnectorSelected).toHaveBeenCalledWith(mockConnectors[1]); }); + + it('renders beta badge props when selected connector is a pre-configured connector', () => { + const props = { + connectors: [ + { + id: '.inference', + actionTypeId: '.inference', + isPreconfigured: true, + name: 'Elastic Managed LLM', + config: {}, + isDeprecated: false, + isSystemAction: false, + secrets: {}, + }, + ] as AIConnector[], + onConnectorSelected: jest.fn(), + selectedConnectorId: '.inference', + }; + const { getByTestId } = render(); + expect(getByTestId('connectorSelectorPanelBetaBadge')).toBeInTheDocument(); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx index 2008b86d1c284..835469ef7fb67 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx @@ -6,12 +6,13 @@ */ import React, { useMemo, useEffect, useCallback } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiText } from '@elastic/eui'; +import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { ConnectorSelector } from '@kbn/security-solution-connectors'; import { getActionTypeTitle, getGenAiConfig, + isElasticManagedLlmConnector, } from '@kbn/elastic-assistant/impl/connectorland/helpers'; import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; import type { AIConnector } from './types'; @@ -26,7 +27,7 @@ interface ConnectorSelectorPanelProps { export const ConnectorSelectorPanel = React.memo( ({ connectors, selectedConnectorId, onConnectorSelected }) => { const { actionTypeRegistry } = useKibana().services.triggersActionsUi; - + const { euiTheme } = useEuiTheme(); const selectedConnector = useMemo( () => connectors.find((connector) => connector.id === selectedConnectorId), [connectors, selectedConnectorId] @@ -72,51 +73,83 @@ export const ConnectorSelectorPanel = React.memo( [connectors, onConnectorSelected] ); + const betaBadgeProps = useMemo(() => { + if (!selectedConnector || !isElasticManagedLlmConnector(selectedConnector)) { + return; + } + + return { + label: ( + + {i18n.PRECONFIGURED_CONNECTOR_LABEL} + + ), + title: i18n.PRECONFIGURED_CONNECTOR_LABEL, + color: 'subdued' as const, + css: css` + height: ${euiTheme.base * 1.25}px; + `, + }; + }, [euiTheme, selectedConnector]); + return ( - - - - {i18n.SELECTED_PROVIDER} - - - - {selectedConnector && ( + + + {i18n.SELECTED_PROVIDER} + + + + {selectedConnector && ( + + + + )} - - )} - - - - - - - + + + + } + /> ); } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts index c12788eafbfa2..703f943b908f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts @@ -35,3 +35,10 @@ export const REQUIRED_PRIVILEGES_CONNECTORS_ALL = i18n.translate( 'xpack.securitySolution.onboarding.assistantCard.requiredPrivileges.connectorsAll', { defaultMessage: 'Management > Actions & Connectors: All' } ); + +export const PRECONFIGURED_CONNECTOR_LABEL = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.preconfiguredConnectorLabel', + { + defaultMessage: 'Pre-configured', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index 69594853cccf7..47c21bff4e269 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -253,6 +253,6 @@ "@kbn/actions-types", "@kbn/triggers-actions-ui-types", "@kbn/unified-histogram", - "@kbn/react-kibana-context-theme" + "@kbn/react-kibana-context-theme", ] }