diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_get_product_doc.ts b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_get_product_doc.ts deleted file mode 100644 index 743375ffa1487..0000000000000 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_get_product_doc.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { REACT_QUERY_KEYS } from '../constants'; -import { useKibana } from './use_kibana'; -import { useUninstallProductDoc } from './use_uninstall_product_doc'; -import { useInstallProductDoc } from './use_install_product_doc'; - -export function useGetProductDoc(inferenceId: string | undefined) { - const { productDocBase } = useKibana().services; - - const { mutateAsync: installProductDoc, isLoading: isInstalling } = useInstallProductDoc(); - - const { mutateAsync: uninstallProductDoc, isLoading: isUninstalling } = useUninstallProductDoc(); - - const { isLoading, data, refetch } = useQuery({ - queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS, inferenceId], - queryFn: async () => { - return productDocBase!.installation.getStatus({ inferenceId }); - }, - keepPreviousData: false, - refetchOnWindowFocus: false, - }); - - useEffect(() => { - refetch(); - }, [inferenceId, isInstalling, isUninstalling, refetch]); - - return { - status: data?.inferenceId === inferenceId ? data?.overall : undefined, - refetch, - isLoading, - installProductDoc, - uninstallProductDoc, - }; -} diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_product_doc.ts b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_product_doc.ts new file mode 100644 index 0000000000000..f0ad45fec139e --- /dev/null +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/hooks/use_product_doc.ts @@ -0,0 +1,95 @@ +/* + * 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 { useEffect, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; +import { + PerformInstallResponse, + UninstallResponse, +} from '@kbn/product-doc-base-plugin/common/http_api/installation'; +import { REACT_QUERY_KEYS } from '../constants'; +import { useKibana } from './use_kibana'; +import { useUninstallProductDoc } from './use_uninstall_product_doc'; +import { useInstallProductDoc } from './use_install_product_doc'; + +export interface UseProductDoc { + status: InstallationStatus | undefined; + isLoading: boolean; + installProductDoc: (inferenceId: string) => Promise; + uninstallProductDoc: (inferenceId: string) => Promise; +} + +/** + * Custom hook to get the status of the product documentation installation. + * It also provides methods to install and uninstall the product documentation. + * + * @param inferenceId - The ID of the inference for which to get the product documentation status. + * @returns An object containing the status of the product documentation, loading state, and methods to install and uninstall the product documentation. + */ +export function useProductDoc(inferenceId: string | undefined): UseProductDoc { + const { productDocBase } = useKibana().services; + + const { mutateAsync: installProductDoc, isLoading: isInstalling } = useInstallProductDoc(); + + const { mutateAsync: uninstallProductDoc, isLoading: isUninstalling } = useUninstallProductDoc(); + + const { isLoading, data, refetch, isRefetching } = useQuery({ + networkMode: 'always', + queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS, inferenceId], + queryFn: async () => { + return productDocBase!.installation.getStatus({ inferenceId }); + }, + keepPreviousData: false, + refetchOnWindowFocus: false, + }); + + useEffect(() => { + refetch(); + }, [inferenceId, refetch]); + + // poll the status if when is installing or uninstalling + useEffect(() => { + if ( + !( + data?.overall === 'installing' || + data?.overall === 'uninstalling' || + isInstalling || + isUninstalling + ) + ) { + return; + } + + const interval = setInterval(refetch, 5000); + + // cleanup the interval if unmount + return () => { + clearInterval(interval); + }; + }, [refetch, data?.overall, isInstalling, isUninstalling]); + + const status: InstallationStatus | undefined = useMemo(() => { + if (!inferenceId || data?.inferenceId !== inferenceId) { + return undefined; + } + if (isInstalling) { + return 'installing'; + } + if (isUninstalling) { + return 'uninstalling'; + } + return data?.overall; + }, [inferenceId, isInstalling, isUninstalling, data]); + + return { + status, + isLoading: isLoading || isRefetching || isInstalling || isUninstalling, + installProductDoc, + uninstallProductDoc, + }; +} diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.test.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.test.tsx index b8f51157c5cb9..e45c9539a4266 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.test.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.test.tsx @@ -24,6 +24,8 @@ import { elserTitle, } from '@kbn/ai-assistant/src/utils/get_model_options_for_inference_endpoints'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; +import { UseProductDoc } from '../../../hooks/use_product_doc'; jest.mock('../../../hooks/use_install_product_doc', () => ({ useInstallProductDoc: () => ({ @@ -94,6 +96,14 @@ const createMockKnowledgeBase = ( ...overrides, }); +const createProductDoc = (overrides: Partial = {}) => ({ + status: 'uninstalled' as InstallationStatus, + isLoading: false, + installProductDoc: jest.fn().mockResolvedValue({} as any), + uninstallProductDoc: jest.fn().mockResolvedValue({} as any), + ...overrides, +}); + const modelOptions = [ { key: ELSER_ON_ML_NODE_INFERENCE_ID, @@ -112,12 +122,16 @@ const setupMockGetModelOptions = (options = modelOptions) => { mockGetModelOptions.mockReturnValue(options); }; -const renderComponent = (mockKb: UseKnowledgeBaseResult) => { +const renderComponent = (mockKb: UseKnowledgeBaseResult, mockProductDoc: UseProductDoc) => { const queryClient = new QueryClient(); render( - {' '} + ); }; @@ -133,7 +147,8 @@ describe('ChangeKbModel', () => { it('disables the `Update` button when selected model is the same as current and no redeployment needed', () => { const mockKb = createMockKnowledgeBase(); - renderComponent(mockKb); + const mockProductDoc = createProductDoc(); + renderComponent(mockKb, mockProductDoc); const button = screen.getByTestId('observabilityAiAssistantKnowledgeBaseUpdateModelButton'); expect(button).toBeDisabled(); @@ -141,7 +156,8 @@ describe('ChangeKbModel', () => { it('enables the `Update` button when a different model is selected', async () => { const mockKb = createMockKnowledgeBase(); - renderComponent(mockKb); + const mockProductDoc = createProductDoc(); + renderComponent(mockKb, mockProductDoc); const button = screen.getByTestId('observabilityAiAssistantKnowledgeBaseUpdateModelButton'); expect(button).toBeDisabled(); @@ -159,7 +175,8 @@ describe('ChangeKbModel', () => { it('disables the `Update` button when knowledge base is installing', () => { const mockKb = createMockKnowledgeBase({ isInstalling: true }); - renderComponent(mockKb); + const mockProductDoc = createProductDoc(); + renderComponent(mockKb, mockProductDoc); const button = screen.getByTestId('observabilityAiAssistantKnowledgeBaseUpdateModelButton'); expect(button).toBeDisabled(); @@ -180,7 +197,8 @@ describe('ChangeKbModel', () => { }, }), }); - renderComponent(mockKb); + const mockProductDoc = createProductDoc(); + renderComponent(mockKb, mockProductDoc); const dropdown = screen.getByTestId('observabilityAiAssistantKnowledgeBaseModelDropdown'); expect(dropdown).toHaveTextContent(elserTitle); @@ -198,7 +216,8 @@ describe('ChangeKbModel', () => { }, }), }); - renderComponent(mockKb); + const mockProductDoc = createProductDoc(); + renderComponent(mockKb, mockProductDoc); const dropdown = screen.getByTestId('observabilityAiAssistantKnowledgeBaseModelDropdown'); dropdown.click(); @@ -226,7 +245,8 @@ describe('ChangeKbModel', () => { }, }), }); - renderComponent(mockKb); + const mockProductDoc = createProductDoc(); + renderComponent(mockKb, mockProductDoc); const dropdown = screen.getByTestId('observabilityAiAssistantKnowledgeBaseModelDropdown'); dropdown.click(); diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.tsx index ff2045d851e18..47842401461f0 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/change_kb_model.tsx @@ -31,10 +31,17 @@ import { LEGACY_CUSTOM_INFERENCE_ID, useKibana, } from '@kbn/observability-ai-assistant-plugin/public'; -import { getMappedInferenceId } from '../../../helpers/inference_utils'; -import { useGetProductDoc } from '../../../hooks/use_get_product_doc'; +import { UseProductDoc } from '../../../hooks/use_product_doc'; -export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { +export function ChangeKbModel({ + knowledgeBase, + productDoc, + currentlyDeployedInferenceId, +}: { + knowledgeBase: UseKnowledgeBaseResult; + productDoc: UseProductDoc; + currentlyDeployedInferenceId: string | undefined; +}) { const { overlays } = useKibana().services; const [hasLoadedCurrentModel, setHasLoadedCurrentModel] = useState(false); @@ -46,12 +53,6 @@ export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBa endpoints: inferenceEndpoints, }); - const currentlyDeployedInferenceId = getMappedInferenceId( - knowledgeBase.status.value?.currentInferenceId - ); - - const { installProductDoc } = useGetProductDoc(currentlyDeployedInferenceId); - const [selectedInferenceId, setSelectedInferenceId] = useState( currentlyDeployedInferenceId || '' ); @@ -165,7 +166,7 @@ export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBa if (isConfirmed) { setIsUpdatingModel(true); knowledgeBase.install(selectedInferenceId); - installProductDoc(selectedInferenceId); + productDoc.installProductDoc(selectedInferenceId); } }); } @@ -177,7 +178,7 @@ export function ChangeKbModel({ knowledgeBase }: { knowledgeBase: UseKnowledgeBa isSelectedModelCurrentModel, overlays, confirmationMessages, - installProductDoc, + productDoc, ]); const superSelectOptions = modelOptions.map((option: ModelOptionsData) => ({ diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.test.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.test.tsx index 97bd56aac5d6d..dfc0fad31cec8 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.test.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.test.tsx @@ -15,19 +15,8 @@ import { LEGACY_CUSTOM_INFERENCE_ID, } from '@kbn/observability-ai-assistant-plugin/public'; import { UseKnowledgeBaseResult } from '@kbn/ai-assistant'; -import { useGetProductDoc } from '../../../hooks/use_get_product_doc'; - -jest.mock('../../../hooks/use_get_product_doc', () => ({ - useGetProductDoc: jest.fn(), -})); - -jest.mock('../../../hooks/use_install_product_doc', () => ({ - useInstallProductDoc: () => ({ mutateAsync: jest.fn() }), -})); - -jest.mock('../../../hooks/use_uninstall_product_doc', () => ({ - useUninstallProductDoc: () => ({ mutateAsync: jest.fn() }), -})); +import { UseProductDoc } from '../../../hooks/use_product_doc'; +import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; const createMockStatus = ( overrides?: Partial> @@ -62,12 +51,16 @@ const createMockKnowledgeBase = ( ...overrides, }); -describe('ProductDocEntry', () => { - it('calls useGetProductDocStatus with ELSER_ON_ML_NODE_INFERENCE_ID when inference ID is LEGACY_CUSTOM_INFERENCE_ID', async () => { - (useGetProductDoc as jest.Mock).mockReturnValue({ - status: 'installed', - }); +const createProductDoc = (overrides: Partial = {}) => ({ + status: 'uninstalled' as InstallationStatus, + isLoading: false, + installProductDoc: jest.fn().mockResolvedValue({} as any), + uninstallProductDoc: jest.fn().mockResolvedValue({} as any), + ...overrides, +}); +describe('ProductDocEntry', () => { + it('should render the installed state correctly', async () => { const mockKnowledgeBase = createMockKnowledgeBase({ status: createMockStatus({ currentInferenceId: LEGACY_CUSTOM_INFERENCE_ID, @@ -80,28 +73,37 @@ describe('ProductDocEntry', () => { }), }); - render(); + const productDoc = createProductDoc({ + status: 'installed', + }); + + render( + + ); await waitFor(() => { expect(screen.getByText('Installed')).toBeInTheDocument(); }); - - expect(useGetProductDoc).toHaveBeenCalledWith(ELSER_ON_ML_NODE_INFERENCE_ID); }); - it('calls useGetProductDocStatus with the current inference ID when inference ID is not LEGACY_CUSTOM_INFERENCE_ID"', async () => { + it('should render the uninstalled state correctly', async () => { const mockKnowledgeBase = createMockKnowledgeBase(); + const productDoc = createProductDoc(); - (useGetProductDoc as jest.Mock).mockReturnValue({ - status: 'installed', - }); - - render(); + render( + + ); await waitFor(() => { - expect(screen.getByText('Installed')).toBeInTheDocument(); + expect(screen.getByText('Install')).toBeInTheDocument(); }); - - expect(useGetProductDoc).toHaveBeenCalledWith(ELSER_ON_ML_NODE_INFERENCE_ID); }); }); diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx index d5fd1a089867b..c19c97a7aa746 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx @@ -10,40 +10,64 @@ import { EuiButton, EuiDescribedFormGroup, EuiFormRow, - EuiText, EuiFlexGroup, EuiFlexItem, EuiHealth, - EuiLoadingSpinner, EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { UseKnowledgeBaseResult } from '@kbn/ai-assistant/src/hooks'; -import { getMappedInferenceId } from '../../../helpers/inference_utils'; +import { KnowledgeBaseState } from '@kbn/observability-ai-assistant-plugin/public'; +import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status'; import { useKibana } from '../../../hooks/use_kibana'; -import { useGetProductDoc } from '../../../hooks/use_get_product_doc'; +import { UseProductDoc } from '../../../hooks/use_product_doc'; -export function ProductDocEntry({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { - const { overlays } = useKibana().services; +const statusToButtonTextMap: Record | 'loading', string> = { + installing: i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.installingText', + { defaultMessage: 'Installing...' } + ), + uninstalling: i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.uninstallingText', + { defaultMessage: 'Uninstalling...' } + ), + installed: i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.uninstallProductDocButtonLabel', + { defaultMessage: 'Uninstall' } + ), + uninstalled: i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.installProductDocButtonLabel', + { defaultMessage: 'Install' } + ), + loading: i18n.translate('xpack.observabilityAiAssistantManagement.settingsPage.loadingText', { + defaultMessage: 'Loading...', + }), +}; - const selectedInferenceId = getMappedInferenceId(knowledgeBase.status.value?.currentInferenceId); +export function ProductDocEntry({ + knowledgeBase, + productDoc, + currentlyDeployedInferenceId, +}: { + knowledgeBase: UseKnowledgeBaseResult; + productDoc: UseProductDoc; + currentlyDeployedInferenceId: string | undefined; +}) { + const { overlays } = useKibana().services; - const canInstallProductDoc = selectedInferenceId !== undefined; + const canInstallProductDoc = + currentlyDeployedInferenceId !== undefined && + !(knowledgeBase.isInstalling || knowledgeBase.isWarmingUpModel || knowledgeBase.isPolling) && + knowledgeBase.status?.value?.kbState === KnowledgeBaseState.READY; - const { - status, - isLoading: isStatusLoading, - installProductDoc, - uninstallProductDoc, - } = useGetProductDoc(selectedInferenceId); + const { status, isLoading: isStatusLoading, installProductDoc, uninstallProductDoc } = productDoc; const onClickInstall = useCallback(() => { - if (!selectedInferenceId) { + if (!currentlyDeployedInferenceId) { throw new Error('Inference ID is required to install product documentation'); } - installProductDoc(selectedInferenceId); - }, [installProductDoc, selectedInferenceId]); + installProductDoc(currentlyDeployedInferenceId); + }, [installProductDoc, currentlyDeployedInferenceId]); const onClickUninstall = useCallback(() => { overlays @@ -64,29 +88,25 @@ export function ProductDocEntry({ knowledgeBase }: { knowledgeBase: UseKnowledge } ) .then((confirmed) => { - if (confirmed && selectedInferenceId) { - uninstallProductDoc(selectedInferenceId); + if (confirmed && currentlyDeployedInferenceId) { + uninstallProductDoc(currentlyDeployedInferenceId); } }); - }, [overlays, uninstallProductDoc, selectedInferenceId]); + }, [overlays, uninstallProductDoc, currentlyDeployedInferenceId]); - const content = useMemo(() => { - if (isStatusLoading) { - return ; + const buttonText = useMemo(() => { + if (!status || status === 'error' || !canInstallProductDoc) { + return statusToButtonTextMap.uninstalled; } - if (status === 'installing') { - return ( - - - - - - - ); + if (isStatusLoading && status !== 'installing' && status !== 'uninstalling') { + return statusToButtonTextMap.loading; } + return statusToButtonTextMap[status]; + }, [status, isStatusLoading, canInstallProductDoc]); + + const isLoading = isStatusLoading || status === 'installing' || status === 'uninstalling'; + + const content = useMemo(() => { if (status === 'installed') { return ( @@ -104,10 +124,7 @@ export function ProductDocEntry({ knowledgeBase }: { knowledgeBase: UseKnowledge onClick={onClickUninstall} color="warning" > - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.uninstallProductDocButtonLabel', - { defaultMessage: 'Uninstall' } - )} + {buttonText} @@ -119,11 +136,9 @@ export function ProductDocEntry({ knowledgeBase }: { knowledgeBase: UseKnowledge data-test-subj="settingsTabInstallProductDocButton" onClick={onClickInstall} disabled={!canInstallProductDoc} + isLoading={isLoading} > - {i18n.translate( - 'xpack.observabilityAiAssistantManagement.settingsPage.installProductDocButtonLabel', - { defaultMessage: 'Install' } - )} + {buttonText} ); @@ -148,7 +163,7 @@ export function ProductDocEntry({ knowledgeBase }: { knowledgeBase: UseKnowledge ); - }, [canInstallProductDoc, isStatusLoading, onClickInstall, onClickUninstall, status]); + }, [canInstallProductDoc, onClickInstall, onClickUninstall, status, buttonText, isLoading]); return ( Promise.resolve()); @@ -61,6 +69,12 @@ describe('SettingsTab', () => { isPolling: false, isWarmingUpModel: false, }); + useProductDocMock.mockReturnValue({ + status: 'uninstalled', + isLoading: false, + installProductDoc: jest.fn().mockResolvedValue({}), + uninstallProductDoc: jest.fn().mockResolvedValue({}), + }); useGenAIConnectorsMock.mockReturnValue({ connectors: [{ id: 'test-connector' }] }); useInferenceEndpointsMock.mockReturnValue({ inferenceEndpoints: [{ id: 'test-endpoint', inference_id: 'test-inference-id' }], @@ -136,4 +150,60 @@ describe('SettingsTab', () => { expect(getByTestId('observabilityAiAssistantKnowledgeBaseLoadingSpinner')).toBeInTheDocument(); expect(getByTestId('observabilityAiAssistantKnowledgeBaseUpdateModelButton')).toBeDisabled(); }); + + describe('should call useProductDoc with the correct inference ID', () => { + it('calls useProductDoc with ELSER_ON_ML_NODE_INFERENCE_ID when inference ID is LEGACY_CUSTOM_INFERENCE_ID', async () => { + useKnowledgeBaseMock.mockReturnValue({ + status: { + value: { + enabled: true, + kbState: KnowledgeBaseState.READY, + currentInferenceId: ELSER_ON_ML_NODE_INFERENCE_ID, + }, + }, + isInstalling: false, + isPolling: false, + isWarmingUpModel: false, + }); + render(); + + expect(useProductDoc).toHaveBeenCalledWith(ELSER_ON_ML_NODE_INFERENCE_ID); + }); + + it('calls useProductDoc with the current inference ID when inference ID is not LEGACY_CUSTOM_INFERENCE_ID"', async () => { + useKnowledgeBaseMock.mockReturnValue({ + status: { + value: { + enabled: true, + kbState: KnowledgeBaseState.READY, + currentInferenceId: LEGACY_CUSTOM_INFERENCE_ID, + }, + }, + isInstalling: false, + isPolling: false, + isWarmingUpModel: false, + }); + render(); + + expect(useProductDoc).toHaveBeenCalledWith(ELSER_ON_ML_NODE_INFERENCE_ID); + }); + + it('calls useProductDoc with the current inference ID when inference ID is not E5_SMALL_INFERENCE_ID"', async () => { + useKnowledgeBaseMock.mockReturnValue({ + status: { + value: { + enabled: true, + kbState: KnowledgeBaseState.READY, + currentInferenceId: E5_SMALL_INFERENCE_ID, + }, + }, + isInstalling: false, + isPolling: false, + isWarmingUpModel: false, + }); + render(); + + expect(useProductDoc).toHaveBeenCalledWith(E5_SMALL_INFERENCE_ID); + }); + }); }); diff --git a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx index 0aa34a1a8e273..7ec76eb82aa7f 100644 --- a/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx +++ b/x-pack/platform/plugins/private/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx @@ -29,6 +29,8 @@ import { useKibana } from '../../../hooks/use_kibana'; import { UISettings } from './ui_settings'; import { ProductDocEntry } from './product_doc_entry'; import { ChangeKbModel } from './change_kb_model'; +import { getMappedInferenceId } from '../../../helpers/inference_utils'; +import { useProductDoc } from '../../../hooks/use_product_doc'; const GoToSpacesButton = ({ getUrlForSpaces }: { getUrlForSpaces: () => string }) => { return ( @@ -59,6 +61,11 @@ export function SettingsTab() { const { config } = useAppContext(); const knowledgeBase = useKnowledgeBase(); + const currentlyDeployedInferenceId = getMappedInferenceId( + knowledgeBase.status.value?.currentInferenceId + ); + const productDoc = useProductDoc(currentlyDeployedInferenceId); + const connectors = useGenAIConnectors(); const elasticManagedLlm = getElasticManagedLlmConnector(connectors.connectors); @@ -178,10 +185,20 @@ export function SettingsTab() { - {productDocBase ? : undefined} + {productDocBase ? ( + + ) : undefined} {knowledgeBase.status.value?.enabled && connectors.connectors?.length ? ( - + ) : undefined} diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/install_status.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/install_status.ts index 20625b268ebdd..9a9247cfe28e4 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/install_status.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/install_status.ts @@ -7,7 +7,12 @@ import type { ProductName } from '@kbn/product-doc-common'; -export type InstallationStatus = 'installed' | 'uninstalled' | 'installing' | 'error'; +export type InstallationStatus = + | 'installed' + | 'uninstalled' + | 'installing' + | 'uninstalling' + | 'error'; /** * DTO representation of the product doc install status SO diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts index 8572e12352eea..bd0948c5f050b 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts @@ -99,6 +99,14 @@ export class ProductDocInstallClient { }); } + async setUninstallationStarted(productName: ProductName, inferenceId: string | undefined) { + const objectId = getObjectIdFromProductName(productName, inferenceId); + await this.soClient.update(typeName, objectId, { + installation_status: 'uninstalling', + inference_id: inferenceId, + }); + } + async setInstallationSuccessful( productName: ProductName, indexName: string, diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts index 91497404f8bf4..8b484fe66a76f 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts @@ -16,6 +16,7 @@ const createInstallClientMock = (): InstallClientMock => { setInstallationSuccessful: jest.fn(), setInstallationFailed: jest.fn(), setUninstalled: jest.fn(), + setUninstallationStarted: jest.fn(), getPreviouslyInstalledInferenceIds: jest.fn().mockResolvedValue([]), } as unknown as InstallClientMock; }; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts index 8d7baefdd4364..fbb83d2864368 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts @@ -211,7 +211,13 @@ const convertTaskStatus = (taskStatus: TaskStatus): InstallationStatus | 'unknow }; const getOverallStatus = (statuses: InstallationStatus[]): InstallationStatus => { - const statusOrder: InstallationStatus[] = ['error', 'installing', 'uninstalled', 'installed']; + const statusOrder: InstallationStatus[] = [ + 'error', + 'installing', + 'uninstalling', + 'uninstalled', + 'installed', + ]; for (const status of statusOrder) { if (statuses.includes(status)) { return status; diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts index 7903f6bb613a7..7aa4fca24cb48 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts @@ -280,15 +280,16 @@ describe('PackageInstaller', () => { describe('uninstallAll', () => { it('calls uninstall for all packages', async () => { jest.spyOn(packageInstaller, 'uninstallPackage'); - + const totalProducts = Object.keys(DocumentationProduct).length; await packageInstaller.uninstallAll(); - expect(packageInstaller.uninstallPackage).toHaveBeenCalledTimes( - Object.keys(DocumentationProduct).length - ); + expect(productDocClient.setUninstallationStarted).toHaveBeenCalledTimes(totalProducts); + + expect(packageInstaller.uninstallPackage).toHaveBeenCalledTimes(totalProducts); Object.values(DocumentationProduct).forEach((productName) => { expect(packageInstaller.uninstallPackage).toHaveBeenCalledWith({ productName }); }); + expect(productDocClient.setUninstalled).toHaveBeenCalledTimes(totalProducts); }); }); }); diff --git a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts index ede3a5aedd285..4326cf55c76ee 100644 --- a/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts +++ b/x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts @@ -290,6 +290,7 @@ export class PackageInstaller { const { inferenceId } = params; const allProducts = Object.values(DocumentationProduct); for (const productName of allProducts) { + await this.productDocClient.setUninstallationStarted(productName, inferenceId); await this.uninstallPackage({ productName, inferenceId }); } }