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 3915484152798..7c19d0044708b 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 @@ -40,6 +40,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D return deepFreeze({ settings: `${ELASTIC_DOCS}reference/kibana/configuration-reference`, + aiAssistantSettings: `${ELASTIC_DOCS}reference/kibana/configuration-reference/ai-assistant-settings`, elasticStackGetStarted: isServerless ? `${ELASTIC_DOCS}deploy-manage/deploy/elastic-cloud/serverless` : `${ELASTIC_DOCS}get-started`, 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 fc78a41126e3f..3739e222bba06 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/types.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/types.ts @@ -25,6 +25,7 @@ export interface DocLinksMeta { */ export interface DocLinks { readonly settings: string; + readonly aiAssistantSettings: string; readonly elasticStackGetStarted: string; readonly apiReference: string; readonly serverlessReleaseNotes: string; diff --git a/x-pack/platform/plugins/private/gen_ai_settings/moon.yml b/x-pack/platform/plugins/private/gen_ai_settings/moon.yml index 1eaf12482344c..0be2d983ded4a 100644 --- a/x-pack/platform/plugins/private/gen_ai_settings/moon.yml +++ b/x-pack/platform/plugins/private/gen_ai_settings/moon.yml @@ -47,6 +47,7 @@ dependsOn: - '@kbn/ai-assistant-common' - '@kbn/ai-agent-confirmation-modal' - '@kbn/product-doc-common' + - '@kbn/react-kibana-mount' tags: - plugin - prod diff --git a/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/documentation_section.test.tsx b/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/documentation_section.test.tsx index 4d8d787a7a5fd..df7054ec9f538 100644 --- a/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/documentation_section.test.tsx +++ b/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/documentation_section.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; import { coreMock } from '@kbn/core/public/mocks'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { QueryClient, QueryClientProvider } from '@kbn/react-query'; @@ -15,6 +15,11 @@ import type { ProductDocBasePluginStart } from '@kbn/product-doc-base-plugin/pub import { ResourceTypes } from '@kbn/product-doc-common'; import { DocumentationSection } from './documentation_section'; +jest.mock('@kbn/react-kibana-mount', () => ({ + // In unit tests we don’t need a real MountPoint; returning the node allows us to assert on its contents. + toMountPoint: (node: unknown) => node, +})); + describe('DocumentationSection', () => { const coreStart = coreMock.createStart(); @@ -84,6 +89,17 @@ describe('DocumentationSection', () => { }); }); + it('should render a "Learn more" link in the description', async () => { + renderComponent(mockProductDocBase); + + await waitFor(() => { + expect(screen.getByRole('link', { name: /Learn more/ })).toHaveAttribute( + 'href', + coreStart.docLinks.links.aiAssistantSettings + ); + }); + }); + it('should render all documentation items', async () => { renderComponent(mockProductDocBase); @@ -233,6 +249,43 @@ describe('DocumentationSection', () => { }); }); + it('should show a helpful toast (air-gapped hint + docs link) when install fails', async () => { + mockProductDocBase.installation.getStatus = jest.fn().mockResolvedValue({ + inferenceId: '.elser-2-elasticsearch', + overall: 'uninstalled', + perProducts: {}, + }); + mockProductDocBase.installation.install = jest.fn().mockRejectedValue(new Error('boom')); + + renderComponent(mockProductDocBase, true); + + await waitFor(() => { + expect(screen.getByTestId('documentation-install-elastic_documents')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('documentation-install-elastic_documents')); + + await waitFor(() => { + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalled(); + }); + + const toastArg = (coreStart.notifications.toasts.addDanger as jest.Mock).mock.calls[0][0]; + expect(toastArg.title).toBe('Failed to install documentation'); + + // toMountPoint is mocked to return a React node, so we can assert on its contents. + const { container } = render(<>{toastArg.text}); + const toast = within(container); + expect( + toast.getByText( + 'If your environment has no internet access, you can host these artifacts yourself.' + ) + ).toBeInTheDocument(); + expect(toast.getByRole('link', { name: /Learn more/ })).toHaveAttribute( + 'href', + coreStart.docLinks.links.aiAssistantSettings + ); + }); + it('should call install for Security Labs when install action is clicked', async () => { mockProductDocBase.installation.getStatus = jest .fn() diff --git a/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/documentation_section.tsx b/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/documentation_section.tsx index 2dbc830d94eeb..641570c5a371d 100644 --- a/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/documentation_section.tsx +++ b/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/documentation_section.tsx @@ -14,6 +14,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiLink, EuiSplitPanel, EuiSpacer, EuiText, @@ -23,6 +24,7 @@ import { } from '@elastic/eui'; import { defaultInferenceEndpoints } from '@kbn/inference-common'; import { ResourceTypes } from '@kbn/product-doc-common'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { useProductDocStatus, useInstallProductDoc, @@ -40,7 +42,7 @@ interface DocumentationSectionProps { export const DocumentationSection: React.FC = ({ productDocBase }) => { const { services } = useKibana(); - const { notifications, application } = services; + const { notifications, application, rendering, docLinks } = services; // Check if user has Agent Builder 'All' privileges (manageAgents capability) const hasManagePrivilege = application.capabilities.agentBuilder?.manageAgents === true; @@ -72,8 +74,21 @@ export const DocumentationSection: React.FC = ({ prod notifications.toasts.addSuccess({ title: i18n.INSTALL_SUCCESS }); }, onError: (error) => { - notifications.toasts.addError(new Error(error.body?.message ?? error.message), { + const message = error.body?.message ?? error.message; + notifications.toasts.addDanger({ title: i18n.INSTALL_ERROR, + text: toMountPoint( + +

{message}

+

{i18n.AIR_GAPPED_HINT}

+

+ + {i18n.LEARN_MORE} + +

+
, + rendering + ), }); }, }); @@ -418,7 +433,10 @@ export const DocumentationSection: React.FC = ({ prod - {i18n.DOCUMENTATION_DESCRIPTION} + {i18n.DOCUMENTATION_DESCRIPTION}{' '} + + {i18n.LEARN_MORE} + diff --git a/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/translations.ts b/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/translations.ts index 8399411ec8fb3..a53c883e4c216 100644 --- a/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/translations.ts +++ b/x-pack/platform/plugins/private/gen_ai_settings/public/components/documentation/translations.ts @@ -16,6 +16,15 @@ export const DOCUMENTATION_DESCRIPTION = i18n.translate('genAiSettings.documenta 'Help improve Agent Builder responses to your prompts by installing product documentation. All entries are global to the cluster.', }); +export const LEARN_MORE = i18n.translate('genAiSettings.documentation.learnMore', { + defaultMessage: 'Learn more', +}); + +export const AIR_GAPPED_HINT = i18n.translate('genAiSettings.documentation.airGappedHint', { + defaultMessage: + 'If your environment has no internet access, you can host these artifacts yourself.', +}); + export const ELASTIC_DOCS_NAME = i18n.translate('genAiSettings.documentation.elasticDocs.name', { defaultMessage: 'Elastic documentation', }); diff --git a/x-pack/platform/plugins/private/gen_ai_settings/tsconfig.json b/x-pack/platform/plugins/private/gen_ai_settings/tsconfig.json index 0b8e43d3cd547..a504978e21e11 100644 --- a/x-pack/platform/plugins/private/gen_ai_settings/tsconfig.json +++ b/x-pack/platform/plugins/private/gen_ai_settings/tsconfig.json @@ -33,7 +33,8 @@ "@kbn/product-doc-base-plugin", "@kbn/ai-assistant-common", "@kbn/ai-agent-confirmation-modal", - "@kbn/product-doc-common" + "@kbn/product-doc-common", + "@kbn/react-kibana-mount" ], "exclude": ["target/**/*"] }