diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index d57f5c605ff61..64eb2860d944d 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -37898,6 +37898,8 @@ paths: type: boolean pipeline_exists: type: boolean + product_documentation_status: + type: string security_labs_exists: type: boolean user_data_exists: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index c2b455f3b3ea6..ac82f6412c7b4 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -40450,6 +40450,8 @@ paths: type: boolean pipeline_exists: type: boolean + product_documentation_status: + type: string security_labs_exists: type: boolean user_data_exists: diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml index c57ef291cb74e..604c6c9bf47a7 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -445,6 +445,8 @@ paths: type: boolean pipeline_exists: type: boolean + product_documentation_status: + type: string security_labs_exists: type: boolean user_data_exists: diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml index a36d5d7ef7bd0..12dc55918b236 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -445,6 +445,8 @@ paths: type: boolean pipeline_exists: type: boolean + product_documentation_status: + type: string security_labs_exists: type: boolean user_data_exists: diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts index ce1865496cd35..b031bc8d37185 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts @@ -73,4 +73,5 @@ export const ReadKnowledgeBaseResponse = z.object({ pipeline_exists: z.boolean().optional(), security_labs_exists: z.boolean().optional(), user_data_exists: z.boolean().optional(), + product_documentation_status: z.string().optional(), }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml index 90617149886f6..8b9e3bf741652 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml @@ -87,6 +87,8 @@ paths: type: boolean user_data_exists: type: boolean + product_documentation_status: + type: string 400: description: Generic Error content: diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx index e3d3a2065b98a..5745e17a3a151 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx @@ -12,6 +12,7 @@ import type { IToasts } from '@kbn/core-notifications-browser'; import { i18n } from '@kbn/i18n'; import { useCallback } from 'react'; import { ReadKnowledgeBaseResponse } from '@kbn/elastic-assistant-common'; +import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; import { getKnowledgeBaseStatus } from './api'; const KNOWLEDGE_BASE_STATUS_QUERY_KEY = ['elastic-assistant', 'knowledge-base-status']; @@ -38,7 +39,10 @@ export const useKnowledgeBaseStatus = ({ resource, toasts, enabled, -}: UseKnowledgeBaseStatusParams): UseQueryResult => { +}: UseKnowledgeBaseStatusParams): UseQueryResult< + ReadKnowledgeBaseResponse & { product_documentation_status: InstallationStatus }, + IHttpFetchError +> => { return useQuery( KNOWLEDGE_BASE_STATUS_QUERY_KEY, async ({ signal }) => { @@ -49,7 +53,10 @@ export const useKnowledgeBaseStatus = ({ retry: false, keepPreviousData: true, // Polling interval for Knowledge Base setup in progress - refetchInterval: (data) => (data?.is_setup_in_progress ? 30000 : false), + refetchInterval: (data) => + data?.is_setup_in_progress || data?.product_documentation_status === 'installing' + ? 30000 + : false, // Deprecated, hoist to `queryCache` w/in `QueryClient. See: https://stackoverflow.com/a/76961109 onError: (error: IHttpFetchError) => { if (error.name !== 'AbortError') { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx index c8700f995862f..d8d865b9353f4 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.test.tsx @@ -9,7 +9,6 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { ProductDocumentationManagement } from '.'; import * as i18n from './translations'; import { useInstallProductDoc } from '../../api/product_docs/use_install_product_doc'; -import { useGetProductDocStatus } from '../../api/product_docs/use_get_product_doc_status'; jest.mock('../../api/product_docs/use_install_product_doc'); jest.mock('../../api/product_docs/use_get_product_doc_status'); @@ -18,69 +17,64 @@ describe('ProductDocumentationManagement', () => { const mockInstallProductDoc = jest.fn().mockResolvedValue({}); beforeEach(() => { - (useInstallProductDoc as jest.Mock).mockReturnValue({ mutateAsync: mockInstallProductDoc }); - (useGetProductDocStatus as jest.Mock).mockReturnValue({ status: null, isLoading: false }); - jest.clearAllMocks(); - }); - - it('renders loading spinner when status is loading', async () => { - (useGetProductDocStatus as jest.Mock).mockReturnValue({ - status: { overall: 'not_installed' }, - isLoading: true, + (useInstallProductDoc as jest.Mock).mockReturnValue({ + mutateAsync: mockInstallProductDoc, + isLoading: false, + isSuccess: false, }); - render(); - expect(screen.getByTestId('statusLoading')).toBeInTheDocument(); + jest.clearAllMocks(); }); it('renders install button when not installed', () => { - (useGetProductDocStatus as jest.Mock).mockReturnValue({ - status: { overall: 'not_installed' }, - isLoading: false, - }); - render(); + render(); expect(screen.getByText(i18n.INSTALL)).toBeInTheDocument(); }); it('does not render anything when already installed', () => { - (useGetProductDocStatus as jest.Mock).mockReturnValue({ - status: { overall: 'installed' }, + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('does not render anything when the installation was started by the plugin', () => { + (useInstallProductDoc as jest.Mock).mockReturnValue({ + mutateAsync: mockInstallProductDoc, isLoading: false, + isSuccess: false, }); - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it('shows installing spinner and text when installing', async () => { - (useGetProductDocStatus as jest.Mock).mockReturnValue({ - status: { overall: 'not_installed' }, - isLoading: false, - }); - render(); - fireEvent.click(screen.getByText(i18n.INSTALL)); - await waitFor(() => { - expect(screen.getByTestId('installing')).toBeInTheDocument(); - expect(screen.getByText(i18n.INSTALLING)).toBeInTheDocument(); + (useInstallProductDoc as jest.Mock).mockReturnValue({ + mutateAsync: mockInstallProductDoc, + isLoading: true, + isSuccess: false, }); + render(); + expect(screen.getByTestId('installing')).toBeInTheDocument(); + expect(screen.getByText(i18n.INSTALLING)).toBeInTheDocument(); }); it('sets installed state to true after successful installation', async () => { - (useGetProductDocStatus as jest.Mock).mockReturnValue({ - status: { overall: 'not_installed' }, + (useInstallProductDoc as jest.Mock).mockReturnValue({ + mutateAsync: mockInstallProductDoc, isLoading: false, + isSuccess: true, }); mockInstallProductDoc.mockResolvedValueOnce({}); - render(); - fireEvent.click(screen.getByText(i18n.INSTALL)); - await waitFor(() => expect(screen.queryByText(i18n.INSTALL)).not.toBeInTheDocument()); + render(); + expect(screen.queryByText(i18n.INSTALL)).not.toBeInTheDocument(); }); it('sets installed state to false after failed installation', async () => { - (useGetProductDocStatus as jest.Mock).mockReturnValue({ - status: { overall: 'not_installed' }, + (useInstallProductDoc as jest.Mock).mockReturnValue({ + mutateAsync: mockInstallProductDoc, isLoading: false, + isSuccess: false, }); mockInstallProductDoc.mockRejectedValueOnce(new Error('Installation failed')); - render(); + render(); fireEvent.click(screen.getByText(i18n.INSTALL)); await waitFor(() => expect(screen.getByText(i18n.INSTALL)).toBeInTheDocument()); }); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx index 45dc67c59784f..5cd0754164107 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/settings/product_documentation/index.tsx @@ -14,43 +14,25 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; import { useInstallProductDoc } from '../../api/product_docs/use_install_product_doc'; -import { useGetProductDocStatus } from '../../api/product_docs/use_get_product_doc_status'; import * as i18n from './translations'; -export const ProductDocumentationManagement: React.FC = React.memo(() => { - const [{ isInstalled, isInstalling }, setState] = useState({ - isInstalled: true, - isInstalling: false, - }); +export const ProductDocumentationManagement = React.memo<{ + status?: InstallationStatus; +}>(({ status }) => { + const { + mutateAsync: installProductDoc, + isSuccess: isInstalled, + isLoading: isInstalling, + } = useInstallProductDoc(); - const { mutateAsync: installProductDoc } = useInstallProductDoc(); - const { status, isLoading: isStatusLoading } = useGetProductDocStatus(); - - useEffect(() => { - if (status) { - setState((prevState) => ({ - ...prevState, - isInstalled: status.overall === 'installed', - })); - } - }, [status]); - - const onClickInstall = useCallback(async () => { - setState((prevState) => ({ ...prevState, isInstalling: true })); - try { - await installProductDoc(); - setState({ isInstalled: true, isInstalling: false }); - } catch { - setState({ isInstalled: false, isInstalling: false }); - } + const onClickInstall = useCallback(() => { + installProductDoc(); }, [installProductDoc]); const content = useMemo(() => { - if (isStatusLoading) { - return ; - } if (isInstalling) { return ( @@ -72,11 +54,18 @@ export const ProductDocumentationManagement: React.FC = React.memo(() => { ); - }, [isInstalling, isStatusLoading, onClickInstall]); + }, [isInstalling, onClickInstall]); - if (isInstalled) { + // The last condition means that the installation was started by the plugin + if ( + !status || + status === 'installed' || + isInstalled || + (status === 'installing' && !isInstalling) + ) { return null; } + return ( <> diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx index 72a4b487794d6..d154506c89c80 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx @@ -340,7 +340,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ d return ( <> - + ; type AttackDiscoveryDataClientContract = PublicMethodsOf; export type AttackDiscoveryDataClientMock = jest.Mocked; -type KnowledgeBaseDataClientContract = PublicMethodsOf; +type KnowledgeBaseDataClientContract = PublicMethodsOf & { + isSetupInProgress: AIAssistantKnowledgeBaseDataClient['isSetupInProgress']; +}; export type KnowledgeBaseDataClientMock = jest.Mocked; const createConversationsDataClientMock = () => { @@ -73,9 +75,11 @@ const createKnowledgeBaseDataClientMock = () => { isModelInstalled: jest.fn(), isSecurityLabsDocsLoaded: jest.fn(), isSetupAvailable: jest.fn(), + isSetupInProgress: jest.fn().mockReturnValue(false)(), isUserDataExists: jest.fn(), setupKnowledgeBase: jest.fn(), getLoadedSecurityLabsDocsCount: jest.fn(), + getProductDocumentationStatus: jest.fn(), }; return mocked; }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts index e77bd921e7fe0..f34b523efcad4 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts @@ -64,6 +64,7 @@ describe('AIAssistantKnowledgeBaseDataClient', () => { ml, getElserId: getElserId.mockResolvedValue('elser-id'), getIsKBSetupInProgress: mockGetIsKBSetupInProgress.mockReturnValue(false), + getProductDocumentationStatus: jest.fn().mockResolvedValue('installed'), ingestPipelineResourceName: 'something', setIsKBSetupInProgress: jest.fn().mockImplementation(() => {}), manageGlobalKnowledgeBaseAIAssistant: true, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts index a38034494c955..afbc972add33a 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts @@ -33,6 +33,7 @@ import { } from '@kbn/core/server'; import { IndexPatternsFetcher } from '@kbn/data-views-plugin/server'; import { map } from 'lodash'; +import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; import type { TrainedModelsProvider } from '@kbn/ml-plugin/server/shared_services/providers'; import { getMlNodeCount } from '@kbn/ml-plugin/server/lib/node_utils'; import { AIAssistantDataClient, AIAssistantDataClientParams } from '..'; @@ -86,6 +87,7 @@ export interface KnowledgeBaseDataClientParams extends AIAssistantDataClientPara ml: MlPluginSetup; getElserId: GetElser; getIsKBSetupInProgress: (spaceId: string) => boolean; + getProductDocumentationStatus: () => Promise; ingestPipelineResourceName: string; setIsKBSetupInProgress: (spaceId: string, isInProgress: boolean) => void; manageGlobalKnowledgeBaseAIAssistant: boolean; @@ -100,6 +102,11 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient { public get isSetupInProgress() { return this.options.getIsKBSetupInProgress(this.spaceId); } + + public getProductDocumentationStatus = async () => { + return (await this.options.getProductDocumentationStatus()) ?? 'uninstalled'; + }; + /** * Returns whether setup of the Knowledge Base can be performed (essentially an ML features check) * diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.test.ts index 8cd020149c433..42a2dcd0dd237 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.test.ts @@ -26,7 +26,11 @@ describe('helpers', () => { mockProductDocManager.getStatus.mockResolvedValue({ status: 'uninstalled' }); mockProductDocManager.install.mockResolvedValue(null); - await ensureProductDocumentationInstalled(mockProductDocManager, mockLogger); + await ensureProductDocumentationInstalled({ + productDocManager: mockProductDocManager, + setIsProductDocumentationInProgress: jest.fn(), + logger: mockLogger, + }); expect(mockProductDocManager.getStatus).toHaveBeenCalled(); expect(mockLogger.debug).toHaveBeenCalledWith( @@ -42,7 +46,11 @@ describe('helpers', () => { it('should not install product documentation if already installed', async () => { mockProductDocManager.getStatus.mockResolvedValue({ status: 'installed' }); - await ensureProductDocumentationInstalled(mockProductDocManager, mockLogger); + await ensureProductDocumentationInstalled({ + productDocManager: mockProductDocManager, + setIsProductDocumentationInProgress: jest.fn(), + logger: mockLogger, + }); expect(mockProductDocManager.getStatus).toHaveBeenCalled(); expect(mockProductDocManager.install).not.toHaveBeenCalled(); @@ -54,7 +62,11 @@ describe('helpers', () => { mockProductDocManager.getStatus.mockResolvedValue({ status: 'not_installed' }); mockProductDocManager.install.mockRejectedValue(new Error('Install failed')); - await ensureProductDocumentationInstalled(mockProductDocManager, mockLogger); + await ensureProductDocumentationInstalled({ + productDocManager: mockProductDocManager, + setIsProductDocumentationInProgress: jest.fn(), + logger: mockLogger, + }); expect(mockProductDocManager.getStatus).toHaveBeenCalled(); expect(mockProductDocManager.install).toHaveBeenCalled(); @@ -67,7 +79,11 @@ describe('helpers', () => { it('should log a warning if getStatus fails', async () => { mockProductDocManager.getStatus.mockRejectedValue(new Error('Status check failed')); - await ensureProductDocumentationInstalled(mockProductDocManager, mockLogger); + await ensureProductDocumentationInstalled({ + productDocManager: mockProductDocManager, + setIsProductDocumentationInProgress: jest.fn(), + logger: mockLogger, + }); expect(mockProductDocManager.getStatus).toHaveBeenCalled(); expect(mockLogger.warn).toHaveBeenCalledWith( diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts index 9067e42ca88bb..c6405e0685cf5 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/helpers.ts @@ -144,19 +144,27 @@ const ESQL_QUERY_GENERATION_TITLE = i18n.translate( } ); -export const ensureProductDocumentationInstalled = async ( - productDocManager: ProductDocBaseStartContract['management'], - logger: Logger -) => { +export const ensureProductDocumentationInstalled = async ({ + productDocManager, + setIsProductDocumentationInProgress, + logger, +}: { + productDocManager: ProductDocBaseStartContract['management']; + setIsProductDocumentationInProgress: (value: boolean) => void; + logger: Logger; +}) => { try { const { status } = await productDocManager.getStatus(); if (status !== 'installed') { logger.debug(`Installing product documentation for AIAssistantService`); + setIsProductDocumentationInProgress(true); try { - await productDocManager.install(); + await productDocManager.install({ wait: true }); logger.debug(`Successfully installed product documentation for AIAssistantService`); } catch (e) { logger.warn(`Failed to install product documentation for AIAssistantService: ${e.message}`); + } finally { + setIsProductDocumentationInProgress(false); } } } catch (e) { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts index 691f914598c55..4eb101a3e857a 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts @@ -18,6 +18,7 @@ import { IndicesIndexSettings, } from '@elastic/elasticsearch/lib/api/types'; import { omit } from 'lodash'; +import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; import { TrainedModelsProvider } from '@kbn/ml-plugin/server/shared_services/providers'; import { attackDiscoveryFieldMap } from '../lib/attack_discovery/persistence/field_maps_configuration/field_maps_configuration'; import { defendInsightsFieldMap } from '../ai_assistant_data_clients/defend_insights/field_maps_configuration'; @@ -105,6 +106,7 @@ export class AIAssistantService { private isKBSetupInProgress: Map = new Map(); private hasInitializedV2KnowledgeBase: boolean = false; private productDocManager?: ProductDocBaseStartContract['management']; + private isProductDocumentationInProgress: boolean = false; // Temporary 'feature flag' to determine if we should initialize the new knowledge base mappings private assistantDefaultInferenceEndpoint: boolean = false; @@ -170,6 +172,14 @@ export class AIAssistantService { this.isKBSetupInProgress.set(spaceId, isInProgress); } + public getIsProductDocumentationInProgress() { + return this.isProductDocumentationInProgress; + } + + public setIsProductDocumentationInProgress(isInProgress: boolean) { + this.isProductDocumentationInProgress = isInProgress; + } + private createDataStream: CreateDataStream = ({ resource, kibanaVersion, @@ -220,7 +230,11 @@ export class AIAssistantService { if (this.productDocManager) { // install product documentation without blocking other resources - void ensureProductDocumentationInstalled(this.productDocManager, this.options.logger); + void ensureProductDocumentationInstalled({ + productDocManager: this.productDocManager, + logger: this.options.logger, + setIsProductDocumentationInProgress: this.setIsProductDocumentationInProgress.bind(this), + }); } await this.conversationsDataStream.install({ @@ -469,6 +483,16 @@ export class AIAssistantService { } } + public async getProductDocumentationStatus(): Promise { + const status = await this.productDocManager?.getStatus(); + + if (!status) { + return 'uninstalled'; + } + + return this.isProductDocumentationInProgress ? 'installing' : status.status; + } + public async createAIAssistantConversationsDataClient( opts: CreateAIAssistantClientParams & GetAIAssistantConversationsDataClientParams ): Promise { @@ -523,6 +547,7 @@ export class AIAssistantService { ingestPipelineResourceName: this.resourceNames.pipelines.knowledgeBase, getElserId: this.getElserId, getIsKBSetupInProgress: this.getIsKBSetupInProgress.bind(this), + getProductDocumentationStatus: this.getProductDocumentationStatus.bind(this), kibanaVersion: this.options.kibanaVersion, ml: this.options.ml, setIsKBSetupInProgress: this.setIsKBSetupInProgress.bind(this), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts index e46207cc83883..c846a1bfbbf60 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts @@ -10,6 +10,7 @@ import { serverMock } from '../../__mocks__/server'; import { requestContextMock } from '../../__mocks__/request_context'; import { getGetKnowledgeBaseStatusRequest } from '../../__mocks__/request'; import { AuthenticatedUser } from '@kbn/core-security-common'; +import { knowledgeBaseDataClientMock } from '../../__mocks__/data_clients.mock'; describe('Get Knowledge Base Status Route', () => { let server: ReturnType; @@ -28,19 +29,18 @@ describe('Get Knowledge Base Status Route', () => { server = serverMock.create(); ({ context } = requestContextMock.createTools()); context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser); - context.elasticAssistant.getAIAssistantKnowledgeBaseDataClient = jest.fn().mockResolvedValue({ - getKnowledgeBaseDocuments: jest.fn().mockResolvedValue([]), - indexTemplateAndPattern: { - alias: 'knowledge-base-alias', - }, - isModelInstalled: jest.fn().mockResolvedValue(true), - isSetupAvailable: jest.fn().mockResolvedValue(true), - isInferenceEndpointExists: jest.fn().mockResolvedValue(true), - isSetupInProgress: false, - isSecurityLabsDocsLoaded: jest.fn().mockResolvedValue(true), - isUserDataExists: jest.fn().mockResolvedValue(true), - getLoadedSecurityLabsDocsCount: jest.fn().mockResolvedValue(0), - }); + const kbDataClient = knowledgeBaseDataClientMock.create(); + context.elasticAssistant.getAIAssistantKnowledgeBaseDataClient = jest + .fn() + .mockResolvedValue(kbDataClient); + + kbDataClient.isInferenceEndpointExists.mockResolvedValue(true); + kbDataClient.isModelInstalled.mockResolvedValue(true); + kbDataClient.isSetupAvailable.mockResolvedValue(true); + kbDataClient.getProductDocumentationStatus.mockResolvedValue('installed'); + kbDataClient.isSecurityLabsDocsLoaded.mockResolvedValue(true); + kbDataClient.isUserDataExists.mockResolvedValue(true); + kbDataClient.isSetupInProgress = false; getKnowledgeBaseStatusRoute(server.router); }); @@ -61,6 +61,7 @@ describe('Get Knowledge Base Status Route', () => { pipeline_exists: true, security_labs_exists: true, user_data_exists: true, + product_documentation_status: 'installed', }); }); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts index 5884b08f864fd..6258db2b71d92 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.ts @@ -61,6 +61,7 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter const securityLabsExists = await kbDataClient.isSecurityLabsDocsLoaded(); const loadedSecurityLabsDocsCount = await kbDataClient.getLoadedSecurityLabsDocsCount(); const userDataExists = await kbDataClient.isUserDataExists(); + const productDocumentationStatus = await kbDataClient.getProductDocumentationStatus(); return response.ok({ body: { @@ -72,6 +73,7 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter // If user data exists, we should have at least one document in the Security Labs index user_data_exists: userDataExists || !!loadedSecurityLabsDocsCount, pipeline_exists: pipelineExists, + product_documentation_status: productDocumentationStatus, }, }); } catch (err) {