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
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ pageLoadAssetSize:
screenshotMode: 2351
screenshotting: 3252
searchAssistant: 7079
searchGettingStarted: 6678
searchGettingStarted: 7548
searchHomepage: 9005
searchInferenceEndpoints: 9765
searchNavigation: 8900
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ import type {
import { AgentBuilderPlugin } from './plugin';
import { AGENTBUILDER_FEATURE_ID, AGENTBUILDER_APP_ID, uiPrivileges } from '../common/features';
import { type CreateSkillResponse, SKILLS_API_PATH } from '../common/http_api/skills';
import { MCP_SERVER_PATH } from '../common/mcp';

export type {
AgentBuilderPluginSetup,
AgentBuilderPluginStart,
PublicEmbeddableConversationProps,
} from './types';
export type { EmbeddableConversationProps } from './embeddable/types';
export { AGENTBUILDER_FEATURE_ID, AGENTBUILDER_APP_ID, uiPrivileges };
export { AGENTBUILDER_FEATURE_ID, AGENTBUILDER_APP_ID, uiPrivileges, MCP_SERVER_PATH };
export { type CreateSkillResponse, SKILLS_API_PATH };
export { ConversationInputShell } from '@kbn/agent-builder-browser';
export type { ConversationInputShellProps } from '@kbn/agent-builder-browser';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
"cloud",
"console",
"searchNavigation",
"spaces",
"usageCollection"
],
"requiredBundles": [
"kibanaReact"
"agentBuilder",
"kibanaReact",
"spaces",
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ dependsOn:
- '@kbn/scout-search'
- '@kbn/search-agent'
- '@kbn/shared-ux-ai-components'
- '@kbn/agent-builder-plugin'
- '@kbn/spaces-plugin'
- '@kbn/deeplinks-agent-builder'
- '@kbn/agent-builder-common'
tags:
- plugin
- prod
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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, fireEvent } from '@testing-library/react';
import { I18nProvider } from '@kbn/i18n-react';
import { EuiThemeProvider } from '@elastic/eui';
import { GettingStartedAgentPrompt } from './agent_prompt';
import { useUsageTracker } from '../../contexts/usage_tracker_context';

jest.mock('../../contexts/usage_tracker_context', () => ({
useUsageTracker: jest.fn(),
}));

const mockUseUsageTracker = useUsageTracker as jest.Mock;

const renderComponent = () =>
render(
<I18nProvider>
<EuiThemeProvider>
<GettingStartedAgentPrompt />
</EuiThemeProvider>
</I18nProvider>
);

describe('GettingStartedAgentPrompt', () => {
beforeEach(() => {
mockUseUsageTracker.mockReturnValue({ click: jest.fn(), count: jest.fn(), load: jest.fn() });
});

it('does not render the modal on initial mount', () => {
renderComponent();

expect(screen.queryByTestId('promptModalCode')).not.toBeInTheDocument();
});

it('opens the modal when the Copy prompt button is clicked', () => {
renderComponent();

fireEvent.click(screen.getByTestId('chatFirstAgentInstallBtn'));

expect(screen.getByTestId('promptModalCode')).toBeInTheDocument();
});

it('renders the modal with prompt content when opened', () => {
renderComponent();

fireEvent.click(screen.getByTestId('chatFirstAgentInstallBtn'));

expect(screen.getByTestId('promptModalCode')).not.toBeEmptyDOMElement();
});

it('closes the modal when the Close button is clicked', () => {
renderComponent();

fireEvent.click(screen.getByTestId('chatFirstAgentInstallBtn'));
expect(screen.getByTestId('promptModalCode')).toBeInTheDocument();

fireEvent.click(screen.getByTestId('promptModalCloseBtn'));
expect(screen.queryByTestId('promptModalCode')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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, { useState } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { buildPrompt } from '../agent_install/util';
import { PromptModal } from '../agent_install/prompt_modal';

export const GettingStartedAgentPrompt = () => {
const [isPromptModalOpen, setIsPromptModalOpen] = useState(false);

return (
<>
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>
<EuiTitle size="xxs">
<h5>
{i18n.translate('xpack.search.gettingStarted.chat.agentPrompt.title', {
defaultMessage: 'Prompt your agent',
})}
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="xs">
<p>
{i18n.translate('xpack.search.gettingStarted.chat.agentPrompt.description', {
defaultMessage:
'Set up our official optimized Elasticsearch skills in your preferred agentic code workflow.',
})}
</p>
</EuiText>
</EuiFlexItem>
<EuiSpacer size="s" />
<EuiFlexItem>
<span>
<EuiButton
color="text"
onClick={() => setIsPromptModalOpen(true)}
iconType="copy"
size="s"
data-test-subj="chatFirstAgentInstallBtn"
>
{i18n.translate('xpack.search.gettingStarted.chat.agentPrompt.cta', {
defaultMessage: 'Copy prompt',
})}
</EuiButton>
</span>
</EuiFlexItem>
</EuiFlexGroup>
{isPromptModalOpen && (
<PromptModal prompt={buildPrompt('cli')} onClose={() => setIsPromptModalOpen(false)} />
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 { EuiFlexGrid, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import { ChatElasticsearchConnectionDetails } from './connection_details';
import { ConversationPrompt } from './conversation_prompt';
import { ChatColumnsGrid, ChatContentSeparator } from './styles';
import { GettingStartedAgentPrompt } from './agent_prompt';

export const GettingStartedChatContent = () => {
return (
<EuiFlexGrid data-test-subj="gettingStartedChatContent" columns={2} css={ChatColumnsGrid}>
<EuiFlexItem css={ChatContentSeparator}>
<ConversationPrompt />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexItem>
<ChatElasticsearchConnectionDetails />
</EuiFlexItem>
<EuiFlexItem>
<GettingStartedAgentPrompt />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGrid>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { DeploymentStatusBadges } from '../header/deployment_status_badges';
import { ChatColumnsGrid, ChatStretchedFlexItem } from './styles';

export const ChatHeader = () => {
return (
<EuiFlexGroup gutterSize="l" alignItems="flexStart" direction="column">
<EuiFlexItem css={ChatStretchedFlexItem}>
<DeploymentStatusBadges />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGrid columns={2} css={ChatColumnsGrid}>
<EuiFlexItem>
<EuiTitle size="l">
<h1>
{i18n.translate('xpack.search.gettingStarted.chatPage.title', {
defaultMessage: 'Bring your data and start building your next search experience.',
})}
</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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, fireEvent } from '@testing-library/react';
import { I18nProvider } from '@kbn/i18n-react';
import { EuiThemeProvider } from '@elastic/eui';
import { ChatElasticsearchConnectionDetails } from './connection_details';
import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url';
import { useAgentBuilderMcpUrl } from '../../hooks/use_mcp_url';

jest.mock('../../hooks/use_elasticsearch_url');
jest.mock('../../hooks/use_mcp_url');
jest.mock('@kbn/search-api-keys-components', () => ({
ApiKeyForm: () => <div data-test-subj="apiKeyForm" />,
}));

const mockUseElasticsearchUrl = useElasticsearchUrl as jest.Mock;
const mockUseAgentBuilderMcpUrl = useAgentBuilderMcpUrl as jest.Mock;

const MOCK_ES_URL = 'https://my-deployment.es.us-east-1.aws.elastic.cloud';
const MOCK_MCP_URL = 'https://my-kibana.kb.us-east-1.aws.elastic.cloud/api/agent_builder/mcp';

const renderComponent = () =>
render(
<I18nProvider>
<EuiThemeProvider>
<ChatElasticsearchConnectionDetails />
</EuiThemeProvider>
</I18nProvider>
);

describe('ChatElasticsearchConnectionDetails', () => {
beforeEach(() => {
mockUseElasticsearchUrl.mockReturnValue(MOCK_ES_URL);
mockUseAgentBuilderMcpUrl.mockReturnValue(MOCK_MCP_URL);
});

it('shows the Elasticsearch URL by default', () => {
renderComponent();

expect(screen.getByTestId('endpointValueField')).toHaveTextContent(MOCK_ES_URL);
expect(screen.queryByTestId('mcpEndpointValueField')).not.toBeInTheDocument();
});

it('switches to the MCP URL when the MCP badge is clicked', () => {
renderComponent();

fireEvent.click(screen.getByTestId('viewMCPUrlBtn'));

expect(screen.getByTestId('mcpEndpointValueField')).toHaveTextContent(MOCK_MCP_URL);
expect(screen.queryByTestId('endpointValueField')).not.toBeInTheDocument();
});

it('switches back to the Elasticsearch URL when the Elasticsearch badge is clicked', () => {
renderComponent();

fireEvent.click(screen.getByTestId('viewMCPUrlBtn'));
fireEvent.click(screen.getByTestId('viewElasticsearchUrlBtn'));

expect(screen.getByTestId('endpointValueField')).toHaveTextContent(MOCK_ES_URL);
expect(screen.queryByTestId('mcpEndpointValueField')).not.toBeInTheDocument();
});

describe('badge colors', () => {
it('Elasticsearch badge is default color and MCP badge is hollow on initial render', () => {
renderComponent();

const esBadge = screen.getByTestId('viewElasticsearchUrlBtn');
const mcpBadge = screen.getByTestId('viewMCPUrlBtn');

// EuiBadge uses CSS-in-JS; check the className string for the variant name
expect(esBadge.closest('.euiBadge')?.className).not.toContain('hollow');
expect(mcpBadge.closest('.euiBadge')?.className).toContain('hollow');
});

it('MCP badge becomes default color and Elasticsearch badge becomes hollow after switching', () => {
renderComponent();

fireEvent.click(screen.getByTestId('viewMCPUrlBtn'));

const esBadge = screen.getByTestId('viewElasticsearchUrlBtn');
const mcpBadge = screen.getByTestId('viewMCPUrlBtn');

expect(mcpBadge.closest('.euiBadge')?.className).not.toContain('hollow');
expect(esBadge.closest('.euiBadge')?.className).toContain('hollow');
});
});

it('always renders the ApiKeyForm regardless of URL view', () => {
renderComponent();

expect(screen.getByTestId('apiKeyForm')).toBeInTheDocument();

fireEvent.click(screen.getByTestId('viewMCPUrlBtn'));

expect(screen.getByTestId('apiKeyForm')).toBeInTheDocument();
});
});
Loading
Loading