[Playground chat] UX cleanup for EIS on by default (#217410)

## Summary

This PR involves changes in the UX for playground setup page and
Palyground Chat. Following items have been addressed.

- [x] Convert LLM Connected button to a label that is not interactive
- [x] Rename that label to "Elastic LLM Connected" if EIS is connected,
otherwise "LLM Connected"
- [x] Split the main panel into two panel: one for connecting to an LLM,
one for adding data
- [x] Add unit tests

# Before 
![Screenshot 2025-04-09 at 4 48
35 PM](https://github.com/user-attachments/assets/a632bc94-eeea-4403-bbd3-f7bfcc0deae2)
![Screenshot 2025-04-09 at 4 49
37 PM](https://github.com/user-attachments/assets/fb667ff6-6efc-470b-bb55-5b63bf33f61a)



# After
![Screenshot 2025-04-14 at 5 43
20 PM](https://github.com/user-attachments/assets/d9da3bd9-b7b5-490d-9b7c-d4783e3a4d3b)

![Screenshot 2025-04-09 at 4 40
24 PM](https://github.com/user-attachments/assets/ab0a9fac-d8e0-4f64-a7d5-588c2990a015)


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [ ] [See some risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
- [ ] ...

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com>
This commit is contained in:
Saikat Sarkar 2025-04-15 18:04:00 -06:00 committed by GitHub
parent 9ee5741134
commit 398123d22c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 396 additions and 133 deletions

View file

@ -34378,7 +34378,6 @@
"xpack.searchPlayground.setupPage.descriptionLLM": "Pour commencer, connectez-vous à votre fournisseur LLM et sélectionnez vos sources de données.",
"xpack.searchPlayground.setupPage.documentationLink": "Lire la documentation",
"xpack.searchPlayground.setupPage.learnMore": "Envie d'en savoir plus ?",
"xpack.searchPlayground.setupPage.llmConnectedButtonLabel": "LLM connecté",
"xpack.searchPlayground.setupPage.queryBuilder.title": "Ajouter des données à cette requête",
"xpack.searchPlayground.setupPage.title": "Configurer une expérience de chat",
"xpack.searchPlayground.setupPage.uploadFileLabel": "Charger le fichier",
@ -34390,7 +34389,6 @@
"xpack.searchPlayground.sidebar.instructionsField.placeholder": "Me remplacer",
"xpack.searchPlayground.sidebar.summarizationModel.label": "Modèle",
"xpack.searchPlayground.sidebar.summarizationModel.manageConnectorLink": "Gérer le connecteur",
"xpack.searchPlayground.sidebar.summarizationModel.manageConnectorTooltip": "Gérer",
"xpack.searchPlayground.sidebar.summarizationTitle": "Paramètres du modèle",
"xpack.searchPlayground.sources.indices.label": "Index sélectionnés",
"xpack.searchPlayground.sources.indices.removeIndex": "Retirer l'index des sources",

View file

@ -34356,7 +34356,6 @@
"xpack.searchPlayground.setupPage.descriptionLLM": "開始するには、LLMプロバイダーに接続し、データソースを選択してください。",
"xpack.searchPlayground.setupPage.documentationLink": "ドキュメンテーションを表示",
"xpack.searchPlayground.setupPage.learnMore": "詳細について",
"xpack.searchPlayground.setupPage.llmConnectedButtonLabel": "LLM未接続",
"xpack.searchPlayground.setupPage.queryBuilder.title": "データをクエリに追加",
"xpack.searchPlayground.setupPage.title": "チャット体験を設定",
"xpack.searchPlayground.setupPage.uploadFileLabel": "ファイルをアップロード",
@ -34367,7 +34366,6 @@
"xpack.searchPlayground.sidebar.instructionsField.placeholder": "置換",
"xpack.searchPlayground.sidebar.summarizationModel.label": "モデル",
"xpack.searchPlayground.sidebar.summarizationModel.manageConnectorLink": "コネクターを管理",
"xpack.searchPlayground.sidebar.summarizationModel.manageConnectorTooltip": "管理",
"xpack.searchPlayground.sidebar.summarizationTitle": "モデル設定",
"xpack.searchPlayground.sources.indices.label": "選択したインデックス",
"xpack.searchPlayground.sources.indices.removeIndex": "ソースからインデックスを削除",

View file

@ -34414,7 +34414,6 @@
"xpack.searchPlayground.setupPage.descriptionLLM": "连接到 LLM 提供商并选择数据以开始。",
"xpack.searchPlayground.setupPage.documentationLink": "阅读文档",
"xpack.searchPlayground.setupPage.learnMore": "希望了解详情?",
"xpack.searchPlayground.setupPage.llmConnectedButtonLabel": "已连接 LLM",
"xpack.searchPlayground.setupPage.queryBuilder.title": "将数据添加到查询",
"xpack.searchPlayground.setupPage.title": "设置聊天体验",
"xpack.searchPlayground.setupPage.uploadFileLabel": "上传文件",
@ -34426,7 +34425,6 @@
"xpack.searchPlayground.sidebar.instructionsField.placeholder": "替换我",
"xpack.searchPlayground.sidebar.summarizationModel.label": "模型",
"xpack.searchPlayground.sidebar.summarizationModel.manageConnectorLink": "管理连接器",
"xpack.searchPlayground.sidebar.summarizationModel.manageConnectorTooltip": "管理",
"xpack.searchPlayground.sidebar.summarizationTitle": "模型设置",
"xpack.searchPlayground.sources.indices.label": "选定索引",
"xpack.searchPlayground.sources.indices.removeIndex": "从源中移除索引",

View file

@ -5,60 +5,26 @@
* 2.0.
*/
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiTitle, useEuiTheme } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useWatch } from 'react-hook-form';
import { docLinks } from '../../common/doc_links';
import { EditContextPanel } from './edit_context/edit_context_panel';
import { PlaygroundForm, PlaygroundFormFields } from '../types';
import { useManagementLink } from '../hooks/use_management_link';
import { SummarizationPanel } from './summarization_panel/summarization_panel';
export const ChatSidebar: React.FC = () => {
const { euiTheme } = useEuiTheme();
const selectedModel = useWatch<PlaygroundForm, PlaygroundFormFields.summarizationModel>({
name: PlaygroundFormFields.summarizationModel,
});
const managementLink = useManagementLink(selectedModel?.connectorId);
const panels = [
{
title: i18n.translate('xpack.searchPlayground.sidebar.summarizationTitle', {
defaultMessage: 'Model settings',
defaultMessage: 'LLM settings',
}),
children: <SummarizationPanel />,
extraAction: (
<EuiButtonEmpty
target="_blank"
href={managementLink}
data-test-subj="manageConnectorsLink"
iconType="wrench"
size="s"
aria-label={i18n.translate(
'xpack.searchPlayground.sidebar.summarizationModel.manageConnectorLink',
{
defaultMessage: 'Manage connector',
}
)}
>
<FormattedMessage
id="xpack.searchPlayground.sidebar.summarizationModel.manageConnectorTooltip"
defaultMessage="Manage"
/>
</EuiButtonEmpty>
),
},
{
title: i18n.translate('xpack.searchPlayground.sidebar.contextTitle', {
defaultMessage: 'Context',
defaultMessage: 'Playground context',
}),
extraAction: (
<EuiLink

View file

@ -66,6 +66,18 @@ describe('EditContextFlyout component tests', () => {
);
});
it('should render the documentSizeButtonGroup with all options', () => {
// Check if the EuiButtonGroup is rendered
const buttonGroup = screen.getByTestId('documentSizeButtonGroup');
expect(buttonGroup).toBeInTheDocument();
// Check if all options are rendered within the button group
expect(screen.getByTestId('playground_context_doc_number-1')).toBeInTheDocument();
expect(screen.getByTestId('playground_context_doc_number-3')).toBeInTheDocument();
expect(screen.getByTestId('playground_context_doc_number-5')).toBeInTheDocument();
expect(screen.getByTestId('playground_context_doc_number-10')).toBeInTheDocument();
});
it('should see the context fields', async () => {
expect(screen.getByTestId('contextFieldsSelectable-index1')).toBeInTheDocument();
const listButton = screen

View file

@ -5,8 +5,14 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiPanel, EuiSelect, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPanel,
EuiText,
EuiButtonGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback } from 'react';
import { useController } from 'react-hook-form';
@ -17,6 +23,7 @@ import { AnalyticsEvents } from '../../analytics/constants';
import { ContextFieldsSelect } from './context_fields_select';
export const EditContextPanel: React.FC = () => {
const idPrefix = 'playground_context_doc_number';
const usageTracker = useUsageTracker();
const { fields } = useSourceIndicesFields();
@ -43,46 +50,75 @@ export const EditContextPanel: React.FC = () => {
[onChangeSourceFields, sourceFields, usageTracker]
);
const handleDocSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const handleDocSizeButtonGroupChange = (value: number) => {
usageTracker?.click(AnalyticsEvents.editContextDocSizeChanged);
onChangeSize(Number(e.target.value));
onChangeSize(value);
};
return (
<EuiPanel data-test-subj="editContextPanel">
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexItem grow={false}>
<EuiFormRow
label={i18n.translate('xpack.searchPlayground.editContext.docsRetrievedCount', {
defaultMessage: 'Number of documents sent to LLM',
})}
fullWidth
>
<EuiSelect
data-test-subj="contextPanelDocumentNumberSelect"
options={[
{
value: 1,
text: '1',
},
{
value: 3,
text: '3',
},
{
value: 5,
text: '5',
},
{
value: 10,
text: '10',
},
]}
value={docSize}
onChange={handleDocSizeChange}
fullWidth
/>
</EuiFormRow>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiText>
<h5>
<FormattedMessage
id="xpack.searchPlayground.documentsSize.table.title"
defaultMessage="Documents"
/>
</h5>
</EuiText>
</EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
<EuiFlexItem>
<EuiText size="xs">
<strong>
<FormattedMessage
id="xpack.searchPlayground.editContext.docsRetrievedCount"
defaultMessage="Number of documents sent"
/>
</strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiButtonGroup
data-test-subj="documentSizeButtonGroup"
legend="Number of documents sent"
isFullWidth={true}
buttonSize="compressed"
options={[
{
id: `${idPrefix}-1`,
label: '1',
value: 1,
'data-test-subj': `${idPrefix}-1`,
},
{
id: `${idPrefix}-3`,
label: '3',
value: 3,
'data-test-subj': `${idPrefix}-3`,
},
{
id: `${idPrefix}-5`,
label: '5',
value: 5,
'data-test-subj': `${idPrefix}-5`,
},
{
id: `${idPrefix}-10`,
label: '10',
value: 10,
'data-test-subj': `${idPrefix}-10`,
},
]}
idSelected={`${idPrefix}-${docSize}`}
onChange={(_, value) => handleDocSizeButtonGroupChange(value)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="m">

View file

@ -37,7 +37,6 @@ export const AddDataSources: React.FC = () => {
</EuiButtonEmpty>
) : (
<EuiButton
fill
iconType="plusInCircle"
onClick={() => setShowFlyout(true)}
data-test-subj="addDataSourcesButton"

View file

@ -14,6 +14,7 @@ import {
EuiLink,
EuiLoadingSpinner,
EuiTitle,
EuiCard,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
@ -70,14 +71,52 @@ export const ChatSetupPage: React.FC = () => {
<EuiLoadingSpinner />
) : (
<>
<EuiFlexItem grow={false}>
<ConnectLLMButton />
<EuiFlexItem style={{ minWidth: 360 }}>
<EuiCard
textAlign="left"
titleSize="xs"
title={
<FormattedMessage
id="xpack.searchPlayground.setupPage.connectToLLM"
defaultMessage="Large Language Model (LLM)"
/>
}
description={
<FormattedMessage
id="xpack.searchPlayground.setupPage.connectToLLMDescription"
defaultMessage="Select a model to integrate with your chat experience. You can also set up your own connection."
/>
}
footer={<ConnectLLMButton />}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{indices.length ? <AddDataSources /> : <CreateIndexButton />}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UploadFileButton isSetup={true} />
<EuiFlexItem style={{ minWidth: 360 }}>
<EuiCard
textAlign="left"
titleSize="xs"
title={
<FormattedMessage
id="xpack.searchPlayground.setupPage.elasticsearchData"
defaultMessage="Elasticsearch Data"
/>
}
description={
<FormattedMessage
id="xpack.searchPlayground.setupPage.elasticsearchDataDescription"
defaultMessage="Select your data sources to include as context or upload files to start."
/>
}
footer={
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{indices.length ? <AddDataSources /> : <CreateIndexButton />}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UploadFileButton isSetup={true} />
</EuiFlexItem>
</EuiFlexGroup>
}
/>
</EuiFlexItem>
</>
)}

View file

@ -11,6 +11,7 @@ import { ConnectLLMButton } from './connect_llm_button';
import { useKibana } from '../../hooks/use_kibana';
import { useLoadConnectors } from '../../hooks/use_load_connectors';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { LLMs } from '../../../common/types';
const render = (children: React.ReactNode) =>
testingLibraryRender(<IntlProvider locale="en">{children}</IntlProvider>);
@ -30,6 +31,12 @@ const mockConnectors = {
'2': { title: 'Connector 2' },
};
const mockEisConnectors = {
id: 'connectorId4',
name: 'Elastic Managed LLM',
type: LLMs.inference,
};
describe('ConnectLLMButton', () => {
beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
@ -59,8 +66,9 @@ describe('ConnectLLMButton', () => {
isLoading: false,
isSuccess: true,
});
const { getByTestId } = render(<ConnectLLMButton />);
const { getByTestId, getByText } = render(<ConnectLLMButton />);
expect(getByTestId('connectLLMButton')).toBeInTheDocument();
expect(getByText('Connect to an LLM')).toBeInTheDocument();
});
it('show the flyout when the button is clicked', async () => {
@ -77,14 +85,51 @@ describe('ConnectLLMButton', () => {
await waitFor(() => expect(getByTestId('addConnectorFlyout')).toBeInTheDocument());
});
it('show success button when connector exists', async () => {
it('show the flyout when manageConnectorsLink is clicked', async () => {
(useLoadConnectors as jest.Mock).mockReturnValue({
data: [{}],
data: [
{
name: 'conn-1',
type: LLMs.openai,
},
],
isLoading: false,
isSuccess: true,
});
const { queryByTestId } = render(<ConnectLLMButton />);
const { getByTestId, queryByTestId } = render(<ConnectLLMButton />);
expect(queryByTestId('successConnectLLMButton')).toBeInTheDocument();
expect(queryByTestId('addConnectorFlyout')).not.toBeInTheDocument();
fireEvent.click(getByTestId('manageConnectorsLink'));
await waitFor(() => expect(getByTestId('addConnectorFlyout')).toBeInTheDocument());
});
it('show success text when connector exists', async () => {
(useLoadConnectors as jest.Mock).mockReturnValue({
data: [
{
name: 'conn-1',
type: LLMs.openai,
},
],
isLoading: false,
isSuccess: true,
});
const { queryByTestId, getByText } = render(<ConnectLLMButton />);
expect(queryByTestId('successConnectLLMText')).toBeInTheDocument();
expect(getByText('conn-1 connected')).toBeInTheDocument();
});
it('show success text when EIS connector exists', async () => {
(useLoadConnectors as jest.Mock).mockReturnValue({
data: [mockEisConnectors],
isLoading: false,
isSuccess: true,
});
const { queryByTestId, getByText } = render(<ConnectLLMButton />);
expect(queryByTestId('successConnectLLMText')).toBeInTheDocument();
expect(getByText('Elastic Managed LLM connected')).toBeInTheDocument();
});
});

View file

@ -5,14 +5,23 @@
* 2.0.
*/
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
import {
EuiButton,
EuiText,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useEffect, useState } from 'react';
import { GenerativeAIForSearchPlaygroundConnectorFeatureId } from '@kbn/actions-plugin/common';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../hooks/use_kibana';
import { useLoadConnectors } from '../../hooks/use_load_connectors';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import { AnalyticsEvents } from '../../analytics/constants';
import { LLMs } from '../../../common/types';
export const ConnectLLMButton: React.FC = () => {
const [connectorFlyoutOpen, setConnectorFlyoutOpen] = useState(false);
@ -49,21 +58,47 @@ export const ConnectLLMButton: React.FC = () => {
return (
<>
{connectors?.length ? (
<EuiButtonEmpty
iconType="check"
color="success"
onClick={handleSetupGenAiConnector}
data-test-subj="successConnectLLMButton"
>
<FormattedMessage
id="xpack.searchPlayground.setupPage.llmConnectedButtonLabel"
defaultMessage="LLM connected"
/>
</EuiButtonEmpty>
<EuiFlexGroup alignItems="center" gutterSize="s" data-test-subj="successConnectLLMText">
<EuiFlexItem grow={false}>
<EuiIcon type="checkInCircleFilled" color="success" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="success">
{connectors.some((connector) => connector.type === LLMs.inference) ? (
<FormattedMessage
id="xpack.searchPlayground.setupPage.elasticManagedLlmConnectedButtonLabel"
defaultMessage="{connectorName} connected"
values={{
connectorName:
connectors.filter((connector) => connector.type === LLMs.inference)[0]
?.name || 'Elastic Managed LLM',
}}
/>
) : (
<FormattedMessage
id="xpack.searchPlayground.setupPage.llmConnectedButtonLabel"
defaultMessage="{connectorName} connected"
values={{ connectorName: connectors[0]?.name || 'LLM' }}
/>
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
target="_blank"
data-test-subj="manageConnectorsLink"
iconType="wrench"
size="s"
onClick={handleSetupGenAiConnector}
aria-label={i18n.translate('xpack.searchPlayground.setupPage.manageConnectorLink', {
defaultMessage: 'Manage connector',
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiButton
fill
iconType="link"
iconType="sparkles"
data-test-subj="connectLLMButton"
onClick={handleSetupGenAiConnector}
>

View file

@ -37,9 +37,7 @@ export const CreateIndexButton: React.FC = () => {
return createIndexUrl ? (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiButton
color="primary"
iconType="plusInCircle"
fill
data-test-subj="createIndexButton"
href={createIndexUrl}
onClick={handleCreateIndexClick}

View file

@ -60,6 +60,7 @@ describe('SummarizationModel', () => {
<SummarizationModel selectedModel={models[1]} models={models} onSelect={jest.fn()} />
);
expect(getByTestId('aiConnectorTitle')).toBeInTheDocument();
expect(getByTestId('summarizationModelSelect')).toBeInTheDocument();
});
});

View file

@ -13,14 +13,17 @@ import {
EuiFormRow,
EuiIcon,
EuiSuperSelect,
EuiButtonEmpty,
type EuiSuperSelectOption,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { AnalyticsEvents } from '../../analytics/constants';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
import type { LLMModel } from '../../types';
import { useManagementLink } from '../../hooks/use_management_link';
interface SummarizationModelProps {
selectedModel?: LLMModel;
@ -36,6 +39,7 @@ export const SummarizationModel: React.FC<SummarizationModelProps> = ({
onSelect,
}) => {
const usageTracker = useUsageTracker();
const managementLink = useManagementLink(selectedModel?.connectorId || '');
const onChange = (modelValue: string) => {
const newSelectedModel = models.find((model) => getOptionValue(model) === modelValue);
@ -102,23 +106,43 @@ export const SummarizationModel: React.FC<SummarizationModelProps> = ({
return (
<EuiFormRow
css={{ '.euiFormLabel': { display: 'flex', alignItems: 'center' } }}
data-test-subj="aiConnectorTitle"
label={
<>
<FormattedMessage
id="xpack.searchPlayground.sidebar.summarizationModel.label"
defaultMessage="Model"
defaultMessage="AI Connector"
/>{' '}
</>
}
fullWidth
>
<EuiSuperSelect
data-test-subj="summarizationModelSelect"
options={modelsOption}
valueOfSelected={selectedModel && getOptionValue(selectedModel)}
onChange={onChange}
fullWidth
/>
<EuiFlexGroup direction="row" gutterSize="m">
<EuiFlexItem>
<EuiSuperSelect
data-test-subj="summarizationModelSelect"
options={modelsOption}
valueOfSelected={selectedModel && getOptionValue(selectedModel)}
onChange={onChange}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
target="_blank"
href={managementLink}
data-test-subj="manageConnectorsLink"
iconType="wrench"
size="s"
aria-label={i18n.translate(
'xpack.searchPlayground.sidebar.summarizationModel.manageConnectorLink',
{
defaultMessage: 'Manage connector',
}
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};

View file

@ -0,0 +1,100 @@
/*
* 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, { FC, PropsWithChildren } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { UploadFileButton } from './upload_file_button';
import { useKibana } from '../hooks/use_kibana';
import { useSourceIndicesFields } from '../hooks/use_source_indices_field';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
// Mock hooks
jest.mock('../hooks/use_kibana', () => ({
useKibana: jest.fn(),
}));
jest.mock('../hooks/use_source_indices_field', () => ({
useSourceIndicesFields: jest.fn(),
}));
// Wrapper for rendering with IntlProvider
const Wrapper: FC<PropsWithChildren<unknown>> = ({ children }) => {
return (
<>
<IntlProvider locale="en">{children}</IntlProvider>
</>
);
};
describe('UploadFileButton', () => {
const mockUiActions = {
getTrigger: jest.fn().mockReturnValue({
exec: jest.fn(),
}),
};
const mockSetSelectedIndices = jest.fn();
beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
services: {
uiActions: mockUiActions,
},
});
(useSourceIndicesFields as jest.Mock).mockReturnValue({
setIndices: mockSetSelectedIndices,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders EuiButtonEmpty when isSetup is true', () => {
render(<UploadFileButton isSetup={true} />, { wrapper: Wrapper });
// Check if EuiButtonEmpty is rendered
const button = screen.getByTestId('uploadFileButtonEmpty');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Upload a file');
});
it('calls showFileUploadFlyout when EuiButtonEmpty is clicked', () => {
render(<UploadFileButton isSetup={true} />, { wrapper: Wrapper });
// Click the button
const button = screen.getByTestId('uploadFileButtonEmpty');
fireEvent.click(button);
// Check if the flyout trigger was executed
expect(mockUiActions.getTrigger).toHaveBeenCalledWith('OPEN_FILE_UPLOAD_LITE_TRIGGER');
expect(mockUiActions.getTrigger().exec).toHaveBeenCalled();
});
it('renders EuiButton when isSetup is false', () => {
render(<UploadFileButton isSetup={false} />, { wrapper: Wrapper });
// Check if EuiButton is rendered
const button = screen.getByTestId('uploadFileButton');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Upload file');
});
it('calls showFileUploadFlyout when EuiButton is clicked', () => {
render(<UploadFileButton isSetup={false} />, { wrapper: Wrapper });
// Click the button
const button = screen.getByTestId('uploadFileButton');
fireEvent.click(button);
// Check if the flyout trigger was executed
expect(mockUiActions.getTrigger).toHaveBeenCalledWith('OPEN_FILE_UPLOAD_LITE_TRIGGER');
expect(mockUiActions.getTrigger().exec).toHaveBeenCalled();
});
});

View file

@ -7,7 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton } from '@elastic/eui';
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
import { type FileUploadResults, OPEN_FILE_UPLOAD_LITE_TRIGGER } from '@kbn/file-upload-common';
import { useKibana } from '../hooks/use_kibana';
import { useSourceIndicesFields } from '../hooks/use_source_indices_field';
@ -35,18 +35,32 @@ export const UploadFileButton: React.FC<Props> = ({ isSetup }) => {
return (
<>
<EuiButton
size={isSetup ? 'm' : 's'}
fill={isSetup}
iconType="plusInCircle"
onClick={() => showFileUploadFlyout()}
data-test-subj="uploadFileButton"
>
<FormattedMessage
id="xpack.searchPlayground.setupPage.uploadFileLabel"
defaultMessage="Upload file"
/>
</EuiButton>
{isSetup ? (
<EuiButtonEmpty
flush="right"
iconType="importAction"
onClick={() => showFileUploadFlyout()}
data-test-subj="uploadFileButtonEmpty"
>
<FormattedMessage
id="xpack.searchPlayground.setupPage.uploadFileButtonEmptyLabel"
defaultMessage="Upload a file"
/>
</EuiButtonEmpty>
) : (
<EuiButton
size="s"
fill={false}
iconType="plusInCircle"
onClick={() => showFileUploadFlyout()}
data-test-subj="uploadFileButton"
>
<FormattedMessage
id="xpack.searchPlayground.setupPage.uploadFileLabel"
defaultMessage="Upload file"
/>
</EuiButton>
)}
</>
);
};

View file

@ -69,7 +69,7 @@ export default function (ftrContext: FtrProviderContext) {
await browser.refresh();
});
it('show success llm button', async () => {
await pageObjects.searchPlayground.PlaygroundStartChatPage.expectShowSuccessLLMButton();
await pageObjects.searchPlayground.PlaygroundStartChatPage.expectShowSuccessLLMText();
});
});

View file

@ -146,11 +146,11 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext)
async expectSuccessButtonAfterCreatingConnector(createConnector: () => Promise<void>) {
await createConnector();
await browser.refresh();
await testSubjects.existOrFail('successConnectLLMButton');
await testSubjects.existOrFail('successConnectLLMText');
},
async expectShowSuccessLLMButton() {
await testSubjects.existOrFail('successConnectLLMButton');
async expectShowSuccessLLMText() {
await testSubjects.existOrFail('successConnectLLMText');
},
},
PlaygroundChatPage: {

View file

@ -90,7 +90,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await browser.refresh();
});
it('show success llm button', async () => {
await pageObjects.searchPlayground.PlaygroundStartChatPage.expectShowSuccessLLMButton();
await pageObjects.searchPlayground.PlaygroundStartChatPage.expectShowSuccessLLMText();
});
});