Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-doc-links/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions x-pack/platform/plugins/private/gen_ai_settings/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiSplitPanel,
EuiSpacer,
EuiText,
Expand All @@ -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,
Expand All @@ -40,7 +42,7 @@ interface DocumentationSectionProps {

export const DocumentationSection: React.FC<DocumentationSectionProps> = ({ 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;
Expand Down Expand Up @@ -72,8 +74,21 @@ export const DocumentationSection: React.FC<DocumentationSectionProps> = ({ 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(
<EuiText size="s">
<p>{message}</p>
<p>{i18n.AIR_GAPPED_HINT}</p>
<p>
<EuiLink href={docLinks.links.aiAssistantSettings} target="_blank" external>
{i18n.LEARN_MORE}
</EuiLink>
</p>
</EuiText>,
rendering
),
});
},
});
Expand Down Expand Up @@ -418,7 +433,10 @@ export const DocumentationSection: React.FC<DocumentationSectionProps> = ({ prod
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner>
<EuiText size="s" color="subdued">
{i18n.DOCUMENTATION_DESCRIPTION}
{i18n.DOCUMENTATION_DESCRIPTION}{' '}
<EuiLink href={docLinks.links.aiAssistantSettings} target="_blank" external>
{i18n.LEARN_MORE}
</EuiLink>
</EuiText>
<EuiSpacer size="m" />
<EuiText size="xs" color="subdued">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*"]
}