mirror of
https://github.com/elastic/kibana.git
synced 2025-04-19 15:35:00 -04:00
[Search][Playground] Query mode support for running search (#214482)
## Summary Updated the Search Playground Query View to allow running the query and seeing the JSON response. ### Screenshots Empty State  With Query Response:  ### Checklist - [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] 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) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
605651259e
commit
5b504f8f2a
30 changed files with 1610 additions and 500 deletions
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { getErrorMessage } from './errors';
|
||||
|
||||
describe('getErrorMessage', () => {
|
||||
it('should return the message if the input is an Error instance', () => {
|
||||
const error = new Error('Test error message');
|
||||
expect(getErrorMessage(error)).toBe('Test error message');
|
||||
});
|
||||
|
||||
it('should return the message and cause message if the input is an Error instance with a cause', () => {
|
||||
const error = new Error('Test error message');
|
||||
error.cause = new Error('Test cause message');
|
||||
expect(getErrorMessage(error)).toBe('Test error message. Caused by: Test cause message');
|
||||
});
|
||||
|
||||
it('should return the input if it is a string', () => {
|
||||
const errorMessage = 'Test error message';
|
||||
expect(getErrorMessage(errorMessage)).toBe(errorMessage);
|
||||
});
|
||||
|
||||
it('should return the message property if the input is an object with a message property', () => {
|
||||
const errorObject = { message: 'Test error message' };
|
||||
expect(getErrorMessage(errorObject)).toBe('Test error message');
|
||||
});
|
||||
|
||||
it('should return the result of toString if the input is an object with a toString method', () => {
|
||||
const errorObject = {
|
||||
toString: () => 'Test error message',
|
||||
};
|
||||
expect(getErrorMessage(errorObject)).toBe('Test error message');
|
||||
});
|
||||
|
||||
it('should return the string representation of the input if it is not an Error, string, or object with message/toString', () => {
|
||||
const errorNumber = 12345;
|
||||
expect(getErrorMessage(errorNumber)).toBe('12345');
|
||||
});
|
||||
|
||||
it('should return "undefined" if the input is undefined', () => {
|
||||
expect(getErrorMessage(undefined)).toBe('undefined');
|
||||
});
|
||||
|
||||
it('should return "null" if the input is null', () => {
|
||||
expect(getErrorMessage(null)).toBe('null');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export function getErrorMessage(e: unknown): string {
|
||||
if (e instanceof Error) {
|
||||
if (e.cause instanceof Error) {
|
||||
return i18n.translate('xpack.searchPlayground.errorWithCauseMessage', {
|
||||
defaultMessage: '{message}. Caused by: {causeMessage}',
|
||||
values: {
|
||||
message: e.message,
|
||||
causeMessage: e.cause.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
return e.message;
|
||||
} else if (typeof e === 'string') {
|
||||
return e;
|
||||
} else if (typeof e === 'object' && e !== null && 'message' in e) {
|
||||
return (e as { message: string }).message;
|
||||
} else if (typeof e === 'object' && e !== null && 'toString' in e) {
|
||||
return (e as { toString: () => string }).toString();
|
||||
} else {
|
||||
return String(e);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
export type IndicesQuerySourceFields = Record<string, QuerySourceFields>;
|
||||
|
||||
export enum MessageRole {
|
||||
|
@ -52,6 +52,7 @@ export enum APIRoutes {
|
|||
GET_INDICES = '/internal/search_playground/indices',
|
||||
POST_SEARCH_QUERY = '/internal/search_playground/search',
|
||||
GET_INDEX_MAPPINGS = '/internal/search_playground/mappings',
|
||||
POST_QUERY_TEST = '/internal/search_playground/query_test',
|
||||
}
|
||||
|
||||
export enum LLMs {
|
||||
|
@ -92,3 +93,7 @@ export interface Pagination {
|
|||
size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface QueryTestResponse {
|
||||
searchResponse: SearchResponse;
|
||||
}
|
||||
|
|
|
@ -6,13 +6,12 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { Route, Routes } from '@kbn/shared-ux-router';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { PLUGIN_ID } from '../../common';
|
||||
import { QueryMode } from './query_mode/query_mode';
|
||||
import { SearchQueryMode } from './query_mode/search_query_mode';
|
||||
import { ChatSetupPage } from './setup_page/chat_setup_page';
|
||||
import { Header } from './header';
|
||||
import { useLoadConnectors } from '../hooks/use_load_connectors';
|
||||
|
@ -31,13 +30,6 @@ import {
|
|||
SEARCH_PLAYGROUND_SEARCH_PATH,
|
||||
} from '../routes';
|
||||
|
||||
const SectionStyle = css`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
`;
|
||||
|
||||
export interface AppProps {
|
||||
showDocs?: boolean;
|
||||
}
|
||||
|
@ -67,16 +59,6 @@ export const App: React.FC<AppProps> = ({ showDocs = false }) => {
|
|||
hasConnectors: Boolean(connectors?.length),
|
||||
});
|
||||
|
||||
const restrictedWidth =
|
||||
pageMode === PlaygroundPageMode.search && viewMode === PlaygroundViewMode.preview;
|
||||
const paddingSize =
|
||||
pageMode === PlaygroundPageMode.search && viewMode === PlaygroundViewMode.preview
|
||||
? 'xl'
|
||||
: 'none';
|
||||
const useSectionStyling = !(
|
||||
pageMode === PlaygroundPageMode.search && viewMode === PlaygroundViewMode.preview
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
|
@ -85,39 +67,27 @@ export const App: React.FC<AppProps> = ({ showDocs = false }) => {
|
|||
isActionsDisabled={showSetupPage}
|
||||
onSelectPageModeChange={handlePageModeChange}
|
||||
/>
|
||||
<KibanaPageTemplate.Section
|
||||
alignment="top"
|
||||
restrictWidth={restrictedWidth}
|
||||
grow
|
||||
css={{
|
||||
position: 'relative',
|
||||
}}
|
||||
contentProps={{ css: useSectionStyling ? SectionStyle : undefined }}
|
||||
paddingSize={paddingSize}
|
||||
className="eui-fullHeight"
|
||||
>
|
||||
<Routes>
|
||||
{showSetupPage ? (
|
||||
<>
|
||||
<Route path={SEARCH_PLAYGROUND_CHAT_PATH} component={ChatSetupPage} />
|
||||
{isSearchModeEnabled && (
|
||||
<Route path={SEARCH_PLAYGROUND_SEARCH_PATH} component={SearchPlaygroundSetupPage} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Route exact path={SEARCH_PLAYGROUND_CHAT_PATH} component={Chat} />
|
||||
<Route exact path={PLAYGROUND_CHAT_QUERY_PATH} component={QueryMode} />
|
||||
{isSearchModeEnabled && (
|
||||
<>
|
||||
<Route exact path={SEARCH_PLAYGROUND_SEARCH_PATH} component={SearchMode} />
|
||||
<Route exact path={PLAYGROUND_SEARCH_QUERY_PATH} component={QueryMode} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</KibanaPageTemplate.Section>
|
||||
<Routes>
|
||||
{showSetupPage ? (
|
||||
<>
|
||||
<Route path={SEARCH_PLAYGROUND_CHAT_PATH} component={ChatSetupPage} />
|
||||
{isSearchModeEnabled && (
|
||||
<Route path={SEARCH_PLAYGROUND_SEARCH_PATH} component={SearchPlaygroundSetupPage} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Route exact path={SEARCH_PLAYGROUND_CHAT_PATH} component={Chat} />
|
||||
<Route exact path={PLAYGROUND_CHAT_QUERY_PATH} component={QueryMode} />
|
||||
{isSearchModeEnabled && (
|
||||
<>
|
||||
<Route exact path={SEARCH_PLAYGROUND_SEARCH_PATH} component={SearchMode} />
|
||||
<Route exact path={PLAYGROUND_SEARCH_QUERY_PATH} component={SearchQueryMode} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -35,6 +35,7 @@ import { QuestionInput } from './question_input';
|
|||
import { TelegramIcon } from './telegram_icon';
|
||||
import { transformFromChatMessages } from '../utils/transform_to_messages';
|
||||
import { useUsageTracker } from '../hooks/use_usage_tracker';
|
||||
import { PlaygroundBodySection } from './playground_body_section';
|
||||
|
||||
const buildFormData = (formData: ChatForm): ChatRequestData => ({
|
||||
connector_id: formData[ChatFormFields.summarizationModel].connectorId!,
|
||||
|
@ -112,135 +113,137 @@ export const Chat = () => {
|
|||
}, [usageTracker]);
|
||||
|
||||
return (
|
||||
<EuiForm
|
||||
component="form"
|
||||
css={{ display: 'flex', flexGrow: 1 }}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
data-test-subj="chatPage"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem
|
||||
grow={2}
|
||||
css={{
|
||||
paddingTop: euiTheme.size.l,
|
||||
paddingBottom: euiTheme.size.l,
|
||||
// don't allow the chat to shrink below 66.6% of the screen
|
||||
flexBasis: 0,
|
||||
minWidth: '66.6%',
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup direction="column" className="eui-fullHeight">
|
||||
{/* // Set scroll at the border of parent element*/}
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
className="eui-yScroll"
|
||||
css={{ paddingLeft: euiTheme.size.l, paddingRight: euiTheme.size.l }}
|
||||
tabIndex={0}
|
||||
ref={messagesRef}
|
||||
>
|
||||
<MessageList messages={chatMessages} />
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={{ paddingLeft: euiTheme.size.l, paddingRight: euiTheme.size.l }}
|
||||
>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="sparkles"
|
||||
disabled={isToolBarActionsDisabled}
|
||||
onClick={regenerateMessages}
|
||||
size="xs"
|
||||
data-test-subj="regenerateActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.chat.regenerateBtn"
|
||||
defaultMessage="Regenerate"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="refresh"
|
||||
disabled={isToolBarActionsDisabled}
|
||||
onClick={handleClearChat}
|
||||
size="xs"
|
||||
data-test-subj="clearChatActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.chat.clearChatBtn"
|
||||
defaultMessage="Clear chat"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<PlaygroundBodySection>
|
||||
<EuiForm
|
||||
component="form"
|
||||
css={{ display: 'flex', flexGrow: 1 }}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
data-test-subj="chatPage"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem
|
||||
grow={2}
|
||||
css={{
|
||||
paddingTop: euiTheme.size.l,
|
||||
paddingBottom: euiTheme.size.l,
|
||||
// don't allow the chat to shrink below 66.6% of the screen
|
||||
flexBasis: 0,
|
||||
minWidth: '66.6%',
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup direction="column" className="eui-fullHeight">
|
||||
{/* // Set scroll at the border of parent element*/}
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
className="eui-yScroll"
|
||||
css={{ paddingLeft: euiTheme.size.l, paddingRight: euiTheme.size.l }}
|
||||
tabIndex={0}
|
||||
ref={messagesRef}
|
||||
>
|
||||
<MessageList messages={chatMessages} />
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={{ paddingLeft: euiTheme.size.l, paddingRight: euiTheme.size.l }}
|
||||
>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
<Controller
|
||||
name={ChatFormFields.question}
|
||||
control={control}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
validate: (rule) => !!rule?.trim(),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<QuestionInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
isDisabled={isSubmitting || isRegenerating}
|
||||
button={
|
||||
isSubmitting || isRegenerating ? (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="stopRequestButton"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.searchPlayground.chat.stopButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Stop request',
|
||||
}
|
||||
)}
|
||||
display="base"
|
||||
size="s"
|
||||
iconType="stop"
|
||||
onClick={handleStopRequest}
|
||||
/>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.searchPlayground.chat.sendButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Send a question',
|
||||
}
|
||||
)}
|
||||
display={isValid ? 'base' : 'empty'}
|
||||
size="s"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={!isValid}
|
||||
iconType={TelegramIcon}
|
||||
data-test-subj="sendQuestionButton"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
<EuiFlexItem grow={1} css={{ flexBasis: 0, minWidth: '33.3%' }}>
|
||||
<ChatSidebar />
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="sparkles"
|
||||
disabled={isToolBarActionsDisabled}
|
||||
onClick={regenerateMessages}
|
||||
size="xs"
|
||||
data-test-subj="regenerateActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.chat.regenerateBtn"
|
||||
defaultMessage="Regenerate"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="refresh"
|
||||
disabled={isToolBarActionsDisabled}
|
||||
onClick={handleClearChat}
|
||||
size="xs"
|
||||
data-test-subj="clearChatActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.chat.clearChatBtn"
|
||||
defaultMessage="Clear chat"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<Controller
|
||||
name={ChatFormFields.question}
|
||||
control={control}
|
||||
defaultValue=""
|
||||
rules={{
|
||||
required: true,
|
||||
validate: (rule) => !!rule?.trim(),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<QuestionInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
isDisabled={isSubmitting || isRegenerating}
|
||||
button={
|
||||
isSubmitting || isRegenerating ? (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="stopRequestButton"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.searchPlayground.chat.stopButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Stop request',
|
||||
}
|
||||
)}
|
||||
display="base"
|
||||
size="s"
|
||||
iconType="stop"
|
||||
onClick={handleStopRequest}
|
||||
/>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.searchPlayground.chat.sendButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Send a question',
|
||||
}
|
||||
)}
|
||||
display={isValid ? 'base' : 'empty'}
|
||||
size="s"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={!isValid}
|
||||
iconType={TelegramIcon}
|
||||
data-test-subj="sendQuestionButton"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiHideFor>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
<EuiFlexItem grow={1} css={{ flexBasis: 0, minWidth: '33.3%' }}>
|
||||
<ChatSidebar />
|
||||
</EuiFlexItem>
|
||||
</EuiHideFor>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</PlaygroundBodySection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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, { CSSProperties } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
const PlaygroundBodySectionStyle = css({
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
});
|
||||
|
||||
export const PlaygroundBodySection = ({
|
||||
color,
|
||||
children,
|
||||
dataTestSubj,
|
||||
}: {
|
||||
color?: CSSProperties['backgroundColor'];
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
dataTestSubj?: string;
|
||||
}) => (
|
||||
<KibanaPageTemplate.Section
|
||||
alignment="top"
|
||||
grow
|
||||
css={{
|
||||
position: 'relative',
|
||||
backgroundColor: color,
|
||||
}}
|
||||
contentProps={{ css: PlaygroundBodySectionStyle }}
|
||||
paddingSize="none"
|
||||
className="eui-fullHeight"
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
{children}
|
||||
</KibanaPageTemplate.Section>
|
||||
);
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiBasicTable,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { docLinks } from '../../../common/doc_links';
|
||||
import type { ChatForm, ChatFormFields, QuerySourceFields } from '../../types';
|
||||
|
||||
const isQueryFieldSelected = (
|
||||
queryFields: ChatForm[ChatFormFields.queryFields],
|
||||
index: string,
|
||||
field: string
|
||||
): boolean => {
|
||||
return Boolean(queryFields[index]?.includes(field));
|
||||
};
|
||||
|
||||
export interface QueryFieldsPanelProps {
|
||||
index: string;
|
||||
indexFields: QuerySourceFields;
|
||||
updateFields: (index: string, fieldName: string, checked: boolean) => void;
|
||||
queryFields: ChatForm[ChatFormFields.queryFields];
|
||||
}
|
||||
|
||||
export const QueryFieldsPanel = ({
|
||||
index,
|
||||
indexFields,
|
||||
updateFields,
|
||||
queryFields,
|
||||
}: QueryFieldsPanelProps) => {
|
||||
const queryTableFields = useMemo(
|
||||
() =>
|
||||
[
|
||||
...indexFields.semantic_fields,
|
||||
...indexFields.elser_query_fields,
|
||||
...indexFields.dense_vector_query_fields,
|
||||
...indexFields.bm25_query_fields,
|
||||
].map((field) => ({
|
||||
name: typeof field === 'string' ? field : field.field,
|
||||
checked: isQueryFieldSelected(
|
||||
queryFields,
|
||||
index,
|
||||
typeof field === 'string' ? field : field.field
|
||||
),
|
||||
})),
|
||||
[index, indexFields, queryFields]
|
||||
);
|
||||
return (
|
||||
<EuiPanel
|
||||
grow={false}
|
||||
hasShadow={false}
|
||||
hasBorder
|
||||
data-test-subj={`${index}-query-fields-panel`}
|
||||
>
|
||||
<EuiAccordion
|
||||
id={index}
|
||||
buttonContent={
|
||||
<EuiText>
|
||||
<h5>{index}</h5>
|
||||
</EuiText>
|
||||
}
|
||||
initialIsOpen
|
||||
data-test-subj={`${index}-fieldsAccordion`}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiBasicTable
|
||||
tableCaption={i18n.translate('xpack.searchPlayground.viewQuery.flyout.table.caption', {
|
||||
defaultMessage: 'Query Model table',
|
||||
})}
|
||||
items={queryTableFields}
|
||||
rowHeader="name"
|
||||
columns={[
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate(
|
||||
'xpack.searchPlayground.viewQuery.flyout.table.column.field.name',
|
||||
{ defaultMessage: 'Field' }
|
||||
),
|
||||
'data-test-subj': 'fieldName',
|
||||
},
|
||||
{
|
||||
field: 'checked',
|
||||
name: i18n.translate(
|
||||
'xpack.searchPlayground.viewQuery.flyout.table.column.enabled.name',
|
||||
{ defaultMessage: 'Enabled' }
|
||||
),
|
||||
align: 'right',
|
||||
render: (checked, field) => {
|
||||
return (
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
label={field.name}
|
||||
checked={checked}
|
||||
onChange={(e) => updateFields(index, field.name, e.target.checked)}
|
||||
compressed
|
||||
data-test-subj={`field-${field.name}-${checked}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{indexFields.skipped_fields > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued" data-test-subj={`${index}-skippedFields`}>
|
||||
<EuiIcon type="eyeClosed" />
|
||||
{` `}
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.hiddenFields"
|
||||
defaultMessage="{skippedFields} fields are hidden."
|
||||
values={{ skippedFields: indexFields.skipped_fields }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
href={docLinks.hiddenFields}
|
||||
target="_blank"
|
||||
data-test-subj="hidden-fields-documentation-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.learnMoreLink"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -28,6 +28,8 @@ import { ChatForm, ChatFormFields } from '../../types';
|
|||
import { AnalyticsEvents } from '../../analytics/constants';
|
||||
import { docLinks } from '../../../common/doc_links';
|
||||
import { createQuery } from '../../utils/create_query';
|
||||
import { PlaygroundBodySection } from '../playground_body_section';
|
||||
import { QueryViewSidebarContainer, QueryViewContainer } from './styles';
|
||||
|
||||
const isQueryFieldSelected = (
|
||||
queryFields: ChatForm[ChatFormFields.queryFields],
|
||||
|
@ -72,134 +74,128 @@ export const QueryMode: React.FC = () => {
|
|||
const query = useMemo(() => JSON.stringify(elasticsearchQuery, null, 2), [elasticsearchQuery]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem
|
||||
grow={6}
|
||||
className="eui-yScroll"
|
||||
css={{ padding: euiTheme.size.l, paddingRight: 0 }}
|
||||
>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="m"
|
||||
paddingSize="none"
|
||||
lineNumbers
|
||||
transparentBackground
|
||||
data-test-subj="ViewElasticsearchQueryResult"
|
||||
>
|
||||
{query}
|
||||
</EuiCodeBlock>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={3}
|
||||
className="eui-yScroll"
|
||||
css={{ padding: euiTheme.size.l, paddingLeft: 0 }}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.table.title"
|
||||
defaultMessage="Fields to search (per index)"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
{Object.entries(fields).map(([index, group], indexNum) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
<EuiPanel grow={false} hasShadow={false} hasBorder>
|
||||
<EuiAccordion
|
||||
id={index}
|
||||
buttonContent={
|
||||
<EuiText>
|
||||
<h5>{index}</h5>
|
||||
</EuiText>
|
||||
}
|
||||
initialIsOpen
|
||||
data-test-subj={`fieldsAccordion-${indexNum}`}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<PlaygroundBodySection dataTestSubj="queryModeSection">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={6} className="eui-yScroll" css={QueryViewContainer(euiTheme)}>
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="m"
|
||||
paddingSize="none"
|
||||
lineNumbers
|
||||
transparentBackground
|
||||
data-test-subj="ViewElasticsearchQueryResult"
|
||||
>
|
||||
{query}
|
||||
</EuiCodeBlock>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={3} className="eui-yScroll" css={QueryViewSidebarContainer(euiTheme)}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.table.title"
|
||||
defaultMessage="Fields to search (per index)"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
{Object.entries(fields).map(([index, group], indexNum) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
<EuiPanel grow={false} hasShadow={false} hasBorder>
|
||||
<EuiAccordion
|
||||
id={index}
|
||||
buttonContent={
|
||||
<EuiText>
|
||||
<h5>{index}</h5>
|
||||
</EuiText>
|
||||
}
|
||||
initialIsOpen
|
||||
data-test-subj={`fieldsAccordion-${indexNum}`}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiBasicTable
|
||||
tableCaption="Query Model table"
|
||||
items={[
|
||||
...group.semantic_fields,
|
||||
...group.elser_query_fields,
|
||||
...group.dense_vector_query_fields,
|
||||
...group.bm25_query_fields,
|
||||
].map((field) => ({
|
||||
name: typeof field === 'string' ? field : field.field,
|
||||
checked: isQueryFieldSelected(
|
||||
queryFields,
|
||||
index,
|
||||
typeof field === 'string' ? field : field.field
|
||||
),
|
||||
}))}
|
||||
rowHeader="name"
|
||||
columns={[
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Field',
|
||||
'data-test-subj': 'fieldName',
|
||||
},
|
||||
{
|
||||
field: 'checked',
|
||||
name: 'Enabled',
|
||||
align: 'right',
|
||||
render: (checked, field) => {
|
||||
return (
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
label={field.name}
|
||||
checked={checked}
|
||||
onChange={(e) => updateFields(index, field.name, e.target.checked)}
|
||||
compressed
|
||||
data-test-subj={`field-${field.name}-${checked}`}
|
||||
/>
|
||||
);
|
||||
<EuiBasicTable
|
||||
tableCaption="Query Model table"
|
||||
items={[
|
||||
...group.semantic_fields,
|
||||
...group.elser_query_fields,
|
||||
...group.dense_vector_query_fields,
|
||||
...group.bm25_query_fields,
|
||||
].map((field) => ({
|
||||
name: typeof field === 'string' ? field : field.field,
|
||||
checked: isQueryFieldSelected(
|
||||
queryFields,
|
||||
index,
|
||||
typeof field === 'string' ? field : field.field
|
||||
),
|
||||
}))}
|
||||
rowHeader="name"
|
||||
columns={[
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Field',
|
||||
'data-test-subj': 'fieldName',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{
|
||||
field: 'checked',
|
||||
name: 'Enabled',
|
||||
align: 'right',
|
||||
render: (checked, field) => {
|
||||
return (
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
label={field.name}
|
||||
checked={checked}
|
||||
onChange={(e) => updateFields(index, field.name, e.target.checked)}
|
||||
compressed
|
||||
data-test-subj={`field-${field.name}-${checked}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{group.skipped_fields > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="s"
|
||||
color="subdued"
|
||||
data-test-subj={`skippedFields-${indexNum}`}
|
||||
>
|
||||
<EuiIcon type="eyeClosed" />
|
||||
{` `}
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.hiddenFields"
|
||||
defaultMessage="{skippedFields} fields are hidden."
|
||||
values={{ skippedFields: group.skipped_fields }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
href={docLinks.hiddenFields}
|
||||
target="_blank"
|
||||
data-test-subj="hidden-fields-documentation-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.learnMoreLink"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{group.skipped_fields > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="s"
|
||||
color="subdued"
|
||||
data-test-subj={`skippedFields-${indexNum}`}
|
||||
>
|
||||
<EuiIcon type="eyeClosed" />
|
||||
{` `}
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.hiddenFields"
|
||||
defaultMessage="{skippedFields} fields are hidden."
|
||||
values={{ skippedFields: group.skipped_fields }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
href={docLinks.hiddenFields}
|
||||
target="_blank"
|
||||
data-test-subj="hidden-fields-documentation-link"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.learnMoreLink"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</PlaygroundBodySection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiSplitPanel, EuiText, useEuiTheme } from '@elastic/eui';
|
||||
import { CodeEditor } from '@kbn/code-editor';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { getErrorMessage } from '../../../common/errors';
|
||||
import { FullHeight, QueryViewTitlePanel } from './styles';
|
||||
import { QueryTestResponse } from '../../types';
|
||||
|
||||
export interface ElasticsearchQueryOutputProps {
|
||||
queryResponse?: QueryTestResponse;
|
||||
queryError?: unknown;
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const ElasticsearchQueryOutput = ({
|
||||
queryResponse,
|
||||
isError,
|
||||
queryError,
|
||||
}: ElasticsearchQueryOutputProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const respJSON = useMemo(() => {
|
||||
if (isError) {
|
||||
return getErrorMessage(queryError);
|
||||
}
|
||||
return queryResponse ? JSON.stringify(queryResponse.searchResponse, null, 2) : undefined;
|
||||
}, [isError, queryError, queryResponse]);
|
||||
return (
|
||||
<EuiSplitPanel.Outer hasBorder css={FullHeight} grow={false}>
|
||||
<EuiSplitPanel.Inner grow={false} css={QueryViewTitlePanel(euiTheme)}>
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.queryOutput.title"
|
||||
defaultMessage="Query output"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner paddingSize="none">
|
||||
{!!respJSON ? (
|
||||
<CodeEditor
|
||||
dataTestSubj="ViewElasticsearchQueryResponse"
|
||||
languageId="json"
|
||||
value={respJSON}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
readOnly: true,
|
||||
}}
|
||||
enableFindAction
|
||||
fullWidth
|
||||
isCopyable
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
css={FullHeight}
|
||||
data-test-subj="ViewElasticsearchQueryResponseEmptyState"
|
||||
>
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.queryOutput.emptyPrompt.body"
|
||||
defaultMessage="Run your query above to view the raw JSON output here."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
);
|
||||
};
|
|
@ -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, { useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiForm, EuiPanel, EuiText } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useController, useWatch } from 'react-hook-form';
|
||||
import { useSourceIndicesFields } from '../../hooks/use_source_indices_field';
|
||||
import { useUsageTracker } from '../../hooks/use_usage_tracker';
|
||||
import { ChatForm, ChatFormFields } from '../../types';
|
||||
import { AnalyticsEvents } from '../../analytics/constants';
|
||||
import { createQuery } from '../../utils/create_query';
|
||||
import { SearchQuery } from './search_query';
|
||||
import { QueryFieldsPanel } from './query_fields_panel';
|
||||
|
||||
export interface QuerySidePanelProps {
|
||||
executeQuery: () => void;
|
||||
}
|
||||
|
||||
export const QuerySidePanel = ({ executeQuery }: QuerySidePanelProps) => {
|
||||
const usageTracker = useUsageTracker();
|
||||
const { fields } = useSourceIndicesFields();
|
||||
const sourceFields = useWatch<ChatForm, ChatFormFields.sourceFields>({
|
||||
name: ChatFormFields.sourceFields,
|
||||
});
|
||||
const {
|
||||
field: { onChange: queryFieldsOnChange, value: queryFields },
|
||||
} = useController<ChatForm, ChatFormFields.queryFields>({
|
||||
name: ChatFormFields.queryFields,
|
||||
});
|
||||
const {
|
||||
field: { onChange: elasticsearchQueryChange },
|
||||
} = useController<ChatForm, ChatFormFields.elasticsearchQuery>({
|
||||
name: ChatFormFields.elasticsearchQuery,
|
||||
});
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
executeQuery();
|
||||
},
|
||||
[executeQuery]
|
||||
);
|
||||
const updateFields = useCallback(
|
||||
(index: string, fieldName: string, checked: boolean) => {
|
||||
const currentIndexFields = checked
|
||||
? [...queryFields[index], fieldName]
|
||||
: queryFields[index].filter((field) => fieldName !== field);
|
||||
const updatedQueryFields = { ...queryFields, [index]: currentIndexFields };
|
||||
|
||||
queryFieldsOnChange(updatedQueryFields);
|
||||
elasticsearchQueryChange(createQuery(updatedQueryFields, sourceFields, fields));
|
||||
usageTracker?.count(AnalyticsEvents.queryFieldsUpdated, currentIndexFields.length);
|
||||
},
|
||||
[elasticsearchQueryChange, fields, queryFields, queryFieldsOnChange, sourceFields, usageTracker]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel color="subdued" hasShadow={false}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.sideBar.query.title"
|
||||
defaultMessage="Query"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
<EuiPanel grow={false} hasShadow={false} hasBorder>
|
||||
<EuiForm component="form" onSubmit={handleSearch}>
|
||||
<SearchQuery />
|
||||
</EuiForm>
|
||||
</EuiPanel>
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.flyout.table.title"
|
||||
defaultMessage="Fields to search (per index)"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
{Object.entries(fields).map(([index, group]) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
<QueryFieldsPanel
|
||||
index={index}
|
||||
indexFields={group}
|
||||
updateFields={updateFields}
|
||||
queryFields={queryFields}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -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, { useMemo } from 'react';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSplitPanel,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { CodeEditor } from '@kbn/code-editor';
|
||||
|
||||
import { useController } from 'react-hook-form';
|
||||
import { ChatForm, ChatFormFields } from '../../types';
|
||||
import { FullHeight, QueryViewTitlePanel } from './styles';
|
||||
|
||||
export const ElasticsearchQueryViewer = ({
|
||||
executeQuery,
|
||||
isLoading,
|
||||
}: {
|
||||
executeQuery: Function;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const {
|
||||
field: { value: elasticsearchQuery },
|
||||
} = useController<ChatForm, ChatFormFields.elasticsearchQuery>({
|
||||
name: ChatFormFields.elasticsearchQuery,
|
||||
});
|
||||
const query = useMemo(() => JSON.stringify(elasticsearchQuery, null, 2), [elasticsearchQuery]);
|
||||
return (
|
||||
<EuiSplitPanel.Outer grow hasBorder css={FullHeight}>
|
||||
<EuiSplitPanel.Inner grow={false} css={QueryViewTitlePanel(euiTheme)}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiText>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.queryViewer.title"
|
||||
defaultMessage="Query"
|
||||
/>
|
||||
</h5>
|
||||
</EuiText>
|
||||
<EuiBadge>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.elasticGenerated.badge"
|
||||
defaultMessage="Elastic-generated"
|
||||
/>
|
||||
</EuiBadge>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
size="s"
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
iconType="play"
|
||||
data-test-subj="RunElasticsearchQueryButton"
|
||||
onClick={() => executeQuery()}
|
||||
isLoading={isLoading}
|
||||
aria-label={i18n.translate('xpack.searchPlayground.viewQuery.runQuery.ariaLabel', {
|
||||
defaultMessage: 'Run the elasticsearch query to view results.',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.viewQuery.runQuery.action"
|
||||
defaultMessage="Run"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner paddingSize="none">
|
||||
<CodeEditor
|
||||
dataTestSubj="ViewElasticsearchQueryResult"
|
||||
languageId="json"
|
||||
value={query}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
readOnly: true,
|
||||
}}
|
||||
enableFindAction
|
||||
fullWidth
|
||||
isCopyable
|
||||
/>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { EuiFieldText } from '@elastic/eui';
|
||||
import { Controller, useController, useFormContext } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ChatForm, ChatFormFields } from '../../types';
|
||||
|
||||
export const SearchQuery = () => {
|
||||
const { control } = useFormContext();
|
||||
const {
|
||||
field: { value: searchBarValue },
|
||||
formState: { isSubmitting },
|
||||
} = useController<ChatForm, ChatFormFields.searchQuery>({
|
||||
name: ChatFormFields.searchQuery,
|
||||
});
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={ChatFormFields.searchQuery}
|
||||
render={({ field }) => (
|
||||
<EuiFieldText
|
||||
data-test-subj="searchPlaygroundSearchModeFieldText"
|
||||
prepend="{query}"
|
||||
{...field}
|
||||
value={searchBarValue}
|
||||
icon="search"
|
||||
fullWidth
|
||||
placeholder={i18n.translate(
|
||||
'xpack.searchPlayground.searchMode.queryView.searchBar.placeholder',
|
||||
{ defaultMessage: 'Search for documents' }
|
||||
)}
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiResizableContainer,
|
||||
EuiPanel,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useUsageTracker } from '../../hooks/use_usage_tracker';
|
||||
import { AnalyticsEvents } from '../../analytics/constants';
|
||||
import { PlaygroundBodySection } from '../playground_body_section';
|
||||
import { ElasticsearchQueryViewer } from './query_viewer';
|
||||
import { ElasticsearchQueryOutput } from './query_output';
|
||||
import { QuerySidePanel } from './query_side_panel';
|
||||
import { useElasticsearchQuery } from '../../hooks/use_elasticsearch_query';
|
||||
import { FullHeight, QueryViewContainer, QueryViewSidebarContainer } from './styles';
|
||||
|
||||
export const SearchQueryMode: React.FC = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const usageTracker = useUsageTracker();
|
||||
useEffect(() => {
|
||||
usageTracker?.load(AnalyticsEvents.queryModeLoaded);
|
||||
}, [usageTracker]);
|
||||
const { executeQuery, data, error, isError, fetchStatus } = useElasticsearchQuery();
|
||||
const isLoading = fetchStatus !== 'idle';
|
||||
|
||||
return (
|
||||
<PlaygroundBodySection
|
||||
color={euiTheme.colors.backgroundBasePlain}
|
||||
dataTestSubj="queryModeSection"
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={6} css={QueryViewContainer(euiTheme)}>
|
||||
<EuiPanel
|
||||
paddingSize="none"
|
||||
hasShadow={false}
|
||||
css={css({
|
||||
// This is needed to maintain the resizable container height when rendering output editor with larger content
|
||||
height: '95%',
|
||||
})}
|
||||
>
|
||||
<EuiResizableContainer direction="vertical" css={FullHeight}>
|
||||
{(EuiResizablePanel, EuiResizableButton) => (
|
||||
<>
|
||||
<EuiResizablePanel initialSize={60} minSize="20%" tabIndex={0} paddingSize="none">
|
||||
<ElasticsearchQueryViewer executeQuery={executeQuery} isLoading={isLoading} />
|
||||
</EuiResizablePanel>
|
||||
<EuiResizableButton accountForScrollbars="both" />
|
||||
<EuiResizablePanel initialSize={40} minSize="25%" tabIndex={0} paddingSize="none">
|
||||
<ElasticsearchQueryOutput
|
||||
queryResponse={data}
|
||||
queryError={error}
|
||||
isError={isError}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</EuiResizableContainer>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={3} className="eui-yScroll" css={QueryViewSidebarContainer(euiTheme)}>
|
||||
<QuerySidePanel executeQuery={executeQuery} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</PlaygroundBodySection>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { type EuiThemeComputed } from '@elastic/eui';
|
||||
|
||||
export const FullHeight = css({
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const QueryViewContainer = (euiTheme: EuiThemeComputed<{}>) =>
|
||||
css({
|
||||
padding: euiTheme.size.l,
|
||||
paddingRight: 0,
|
||||
});
|
||||
|
||||
export const QueryViewSidebarContainer = (euiTheme: EuiThemeComputed<{}>) =>
|
||||
css({
|
||||
padding: euiTheme.size.l,
|
||||
paddingLeft: 0,
|
||||
});
|
||||
|
||||
export const QueryViewTitlePanel = (euiTheme: EuiThemeComputed<{}>) =>
|
||||
css({
|
||||
borderBottom: euiTheme.border.thin,
|
||||
padding: `${euiTheme.size.s} ${euiTheme.size.base}`,
|
||||
});
|
|
@ -17,6 +17,7 @@ import React from 'react';
|
|||
import { css } from '@emotion/react';
|
||||
import { Controller, useController, useFormContext } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { DEFAULT_PAGINATION } from '../../../common';
|
||||
import { ResultList } from './result_list';
|
||||
|
@ -54,72 +55,84 @@ export const SearchMode: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" justifyContent="center">
|
||||
<EuiFlexItem
|
||||
grow
|
||||
css={css`
|
||||
max-width: ${euiTheme.base * 48}px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiForm component="form" onSubmit={handleSubmit(() => handleSearch())}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={ChatFormFields.searchQuery}
|
||||
render={({ field }) => (
|
||||
<EuiFieldText
|
||||
data-test-subj="searchPlaygroundSearchModeFieldText"
|
||||
{...field}
|
||||
value={searchBarValue}
|
||||
icon="search"
|
||||
fullWidth
|
||||
placeholder={i18n.translate(
|
||||
'xpack.searchPlayground.searchMode.searchBar.placeholder',
|
||||
{ defaultMessage: 'Search for documents' }
|
||||
)}
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
{searchQuery.query ? (
|
||||
<ResultList
|
||||
searchResults={results}
|
||||
mappings={mappingData}
|
||||
pagination={pagination}
|
||||
onPaginationChange={onPagination}
|
||||
/>
|
||||
) : (
|
||||
<EuiEmptyPrompt
|
||||
iconType={'checkInCircleFilled'}
|
||||
iconColor="success"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.searchPlayground.searchMode.readyToSearch', {
|
||||
defaultMessage: 'Ready to search',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
{i18n.translate('xpack.searchPlayground.searchMode.searchPrompt', {
|
||||
defaultMessage:
|
||||
'Type in a query in the search bar above or view the query we automatically created for you.',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<KibanaPageTemplate.Section
|
||||
alignment="top"
|
||||
restrictWidth
|
||||
grow
|
||||
css={{
|
||||
position: 'relative',
|
||||
}}
|
||||
paddingSize="xl"
|
||||
className="eui-fullHeight"
|
||||
data-test-subj="playground-search-section"
|
||||
>
|
||||
<EuiFlexGroup direction="row" justifyContent="center">
|
||||
<EuiFlexItem
|
||||
grow
|
||||
css={css`
|
||||
max-width: ${euiTheme.base * 48}px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiForm component="form" onSubmit={handleSubmit(() => handleSearch())}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={ChatFormFields.searchQuery}
|
||||
render={({ field }) => (
|
||||
<EuiFieldText
|
||||
data-test-subj="searchPlaygroundSearchModeFieldText"
|
||||
{...field}
|
||||
value={searchBarValue}
|
||||
icon="search"
|
||||
fullWidth
|
||||
placeholder={i18n.translate(
|
||||
'xpack.searchPlayground.searchMode.searchBar.placeholder',
|
||||
{ defaultMessage: 'Search for documents' }
|
||||
)}
|
||||
isLoading={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
{searchQuery.query ? (
|
||||
<ResultList
|
||||
searchResults={results}
|
||||
mappings={mappingData}
|
||||
pagination={pagination}
|
||||
onPaginationChange={onPagination}
|
||||
/>
|
||||
) : (
|
||||
<EuiEmptyPrompt
|
||||
iconType={'checkInCircleFilled'}
|
||||
iconColor="success"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.searchPlayground.searchMode.readyToSearch', {
|
||||
defaultMessage: 'Ready to search',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
{i18n.translate('xpack.searchPlayground.searchMode.searchPrompt', {
|
||||
defaultMessage:
|
||||
'Type in a query in the search bar above or view the query we automatically created for you.',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</KibanaPageTemplate.Section>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useQueryIndices } from '../../hooks/use_query_indices';
|
||||
import { docLinks } from '../../../common/doc_links';
|
||||
import { useUsageTracker } from '../../hooks/use_usage_tracker';
|
||||
|
@ -24,6 +25,7 @@ import { AddDataSources } from './add_data_sources';
|
|||
import { ConnectLLMButton } from './connect_llm_button';
|
||||
import { CreateIndexButton } from './create_index_button';
|
||||
import { UploadFileButton } from '../upload_file_button';
|
||||
import { PlaygroundBodySection } from '../playground_body_section';
|
||||
|
||||
export const ChatSetupPage: React.FC = () => {
|
||||
const usageTracker = useUsageTracker();
|
||||
|
@ -34,75 +36,77 @@ export const ChatSetupPage: React.FC = () => {
|
|||
}, [usageTracker]);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="discuss"
|
||||
data-test-subj="setupPage"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.title"
|
||||
defaultMessage="Set up a chat experience"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<PlaygroundBodySection>
|
||||
<EuiEmptyPrompt
|
||||
iconType="discuss"
|
||||
data-test-subj="setupPage"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.description"
|
||||
defaultMessage="Experiment with combining your Elasticsearch data with powerful large language models (LLMs) using Playground for retrieval augmented generation (RAG)."
|
||||
id="xpack.searchPlayground.setupPage.title"
|
||||
defaultMessage="Set up a chat experience"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.descriptionLLM"
|
||||
defaultMessage="Connect to your LLM provider and select your data sources to get started."
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
{isIndicesLoading ? (
|
||||
<EuiLoadingSpinner />
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectLLMButton />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{indices.length ? <AddDataSources /> : <CreateIndexButton />}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UploadFileButton isSetup={true} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<span>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.learnMore"
|
||||
defaultMessage="Want to learn more?"
|
||||
id="xpack.searchPlayground.setupPage.description"
|
||||
defaultMessage="Experiment with combining your Elasticsearch data with powerful large language models (LLMs) using Playground for retrieval augmented generation (RAG)."
|
||||
/>
|
||||
</span>
|
||||
</EuiTitle>{' '}
|
||||
<EuiLink
|
||||
data-test-subj="searchPlaygroundChatSetupPageReadDocumentationLink"
|
||||
href={docLinks.chatPlayground}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.documentationLink"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.descriptionLLM"
|
||||
defaultMessage="Connect to your LLM provider and select your data sources to get started."
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
{isIndicesLoading ? (
|
||||
<EuiLoadingSpinner />
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectLLMButton />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{indices.length ? <AddDataSources /> : <CreateIndexButton />}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UploadFileButton isSetup={true} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.learnMore"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
</span>
|
||||
</EuiTitle>{' '}
|
||||
<EuiLink
|
||||
data-test-subj="searchPlaygroundChatSetupPageReadDocumentationLink"
|
||||
href={docLinks.chatPlayground}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.documentationLink"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</PlaygroundBodySection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { useQueryIndices } from '../../hooks/use_query_indices';
|
||||
import { useUsageTracker } from '../../hooks/use_usage_tracker';
|
||||
import { AnalyticsEvents } from '../../analytics/constants';
|
||||
import { PlaygroundBodySection } from '../playground_body_section';
|
||||
import { AddDataSources } from './add_data_sources';
|
||||
|
||||
export const SearchPlaygroundSetupPage: React.FC = () => {
|
||||
|
@ -30,46 +31,48 @@ export const SearchPlaygroundSetupPage: React.FC = () => {
|
|||
}, [usageTracker]);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="indexOpen"
|
||||
data-test-subj="setupPage"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.queryBuilder.title"
|
||||
defaultMessage="Add data to query"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
actions={
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
{isIndicesLoading ? (
|
||||
<EuiLoadingSpinner />
|
||||
) : (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddDataSources />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.learnMore"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
</span>
|
||||
</EuiTitle>{' '}
|
||||
<EuiLink href="todo" target="_blank" external>
|
||||
<PlaygroundBodySection>
|
||||
<EuiEmptyPrompt
|
||||
iconType="indexOpen"
|
||||
data-test-subj="setupPage"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.documentationLink"
|
||||
defaultMessage="Read documentation"
|
||||
id="xpack.searchPlayground.setupPage.queryBuilder.title"
|
||||
defaultMessage="Add data to query"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
actions={
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
{isIndicesLoading ? (
|
||||
<EuiLoadingSpinner />
|
||||
) : (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddDataSources />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.learnMore"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
</span>
|
||||
</EuiTitle>{' '}
|
||||
<EuiLink href="todo" target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="xpack.searchPlayground.setupPage.documentationLink"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</PlaygroundBodySection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ export const ViewCodeAction: React.FC<{ selectedPageMode: PlaygroundPageMode }>
|
|||
<EuiButton
|
||||
iconType="editorCodeBlock"
|
||||
color="primary"
|
||||
fill
|
||||
fill={selectedPageMode === PlaygroundPageMode.chat}
|
||||
onClick={() => setShowFlyout(true)}
|
||||
disabled={!selectedIndices || selectedIndices?.length === 0}
|
||||
data-test-subj="viewCodeActionButton"
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { APIRoutes, ChatFormFields, QueryTestResponse } from '../types';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
export const useElasticsearchQuery = () => {
|
||||
const { http } = useKibana().services;
|
||||
const { getValues } = useFormContext();
|
||||
const executeEsQuery = () => {
|
||||
const indices = getValues(ChatFormFields.indices);
|
||||
const elasticsearchQuery = getValues(ChatFormFields.elasticsearchQuery);
|
||||
const query = getValues(ChatFormFields.searchQuery);
|
||||
return http.post<QueryTestResponse>(APIRoutes.POST_QUERY_TEST, {
|
||||
body: JSON.stringify({
|
||||
elasticsearch_query: JSON.stringify(elasticsearchQuery),
|
||||
indices,
|
||||
query,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const { refetch: executeQuery, ...rest } = useQuery({
|
||||
queryFn: executeEsQuery,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
return {
|
||||
executeQuery,
|
||||
...rest,
|
||||
};
|
||||
};
|
|
@ -5,14 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
import type {
|
||||
HealthStatus,
|
||||
IndexName,
|
||||
IndicesStatsIndexMetadataState,
|
||||
Uuid,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import React from 'react';
|
||||
import type {
|
||||
ReactNode,
|
||||
ChangeEvent,
|
||||
FormEvent,
|
||||
Dispatch as ReactDispatch,
|
||||
SetStateAction as ReactSetStateAction,
|
||||
} from 'react';
|
||||
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
@ -28,7 +34,7 @@ import type {
|
|||
UserConfiguredActionConnector,
|
||||
} from '@kbn/alerts-ui-shared/src/common/types';
|
||||
import type { ServiceProviderKeys } from '@kbn/inference-endpoint-ui-common';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { ChatRequestData, MessageRole, LLMs } from '../common/types';
|
||||
|
||||
export * from '../common/types';
|
||||
|
@ -93,7 +99,7 @@ export interface ChatForm {
|
|||
|
||||
export interface Message {
|
||||
id: string;
|
||||
content: string | React.ReactNode;
|
||||
content: string | ReactNode;
|
||||
createdAt?: Date;
|
||||
annotations?: Annotation[];
|
||||
role: MessageRole;
|
||||
|
@ -205,14 +211,9 @@ export interface UseChatHelpers {
|
|||
stop: () => void;
|
||||
setMessages: (messages: Message[]) => void;
|
||||
input: string;
|
||||
setInput: React.Dispatch<React.SetStateAction<string>>;
|
||||
handleInputChange: (
|
||||
e: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLTextAreaElement>
|
||||
) => void;
|
||||
handleSubmit: (
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
chatRequestOptions?: ChatRequestOptions
|
||||
) => void;
|
||||
setInput: ReactDispatch<ReactSetStateAction<string>>;
|
||||
handleInputChange: (e: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
handleSubmit: (e: FormEvent<HTMLFormElement>, chatRequestOptions?: ChatRequestOptions) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { coreMock } from '@kbn/core/server/mocks';
|
|||
import { MockRouter } from '../__mocks__/router.mock';
|
||||
import { ConversationalChain } from './lib/conversational_chain';
|
||||
import { getChatParams } from './lib/get_chat_params';
|
||||
import { createRetriever, defineRoutes } from './routes';
|
||||
import { parseElasticsearchQuery, defineRoutes } from './routes';
|
||||
import { ContextLimitError } from './lib/errors';
|
||||
|
||||
jest.mock('./lib/get_chat_params', () => ({
|
||||
|
@ -20,16 +20,31 @@ jest.mock('./lib/get_chat_params', () => ({
|
|||
|
||||
jest.mock('./lib/conversational_chain');
|
||||
|
||||
describe('createRetriever', () => {
|
||||
describe('parseElasticsearchQuery', () => {
|
||||
test('works when the question has quotes', () => {
|
||||
const esQuery = '{"query": {"match": {"text": "{query}"}}}';
|
||||
const question = 'How can I "do something" with quotes?';
|
||||
|
||||
const retriever = createRetriever(esQuery);
|
||||
const retriever = parseElasticsearchQuery(esQuery);
|
||||
const result = retriever(question);
|
||||
|
||||
expect(result).toEqual({ query: { match: { text: 'How can I "do something" with quotes?' } } });
|
||||
});
|
||||
test('throws an error when esQuery is invalid JSON', () => {
|
||||
const esQuery = 'invalid json';
|
||||
const question = 'How can I "do something" with quotes?';
|
||||
|
||||
try {
|
||||
parseElasticsearchQuery(esQuery)(question);
|
||||
fail('Expected an error to be thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatchInlineSnapshot(
|
||||
`"Failed to parse the Elasticsearch Query. Check Query to make sure it's valid."`
|
||||
);
|
||||
expect(e.cause).toBeDefined();
|
||||
expect(e.cause).toBeInstanceOf(SyntaxError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search Playground routes', () => {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { IRouter, StartServicesAccessor } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PLUGIN_ID } from '../common';
|
||||
|
@ -19,6 +20,7 @@ import { handleStreamResponse } from './utils/handle_stream_response';
|
|||
import {
|
||||
APIRoutes,
|
||||
ElasticsearchRetrieverContentField,
|
||||
QueryTestResponse,
|
||||
SearchPlaygroundPluginStart,
|
||||
SearchPlaygroundPluginStartDependencies,
|
||||
} from './types';
|
||||
|
@ -28,15 +30,31 @@ import { isNotNullish } from '../common/is_not_nullish';
|
|||
import { MODELS } from '../common/models';
|
||||
import { ContextLimitError } from './lib/errors';
|
||||
import { parseSourceFields } from './utils/parse_source_fields';
|
||||
import { getErrorMessage } from '../common/errors';
|
||||
|
||||
export function createRetriever(esQuery: string) {
|
||||
const EMPTY_INDICES_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.searchPlayground.serverErrors.emptyIndices',
|
||||
{
|
||||
defaultMessage: 'Indices cannot be empty',
|
||||
}
|
||||
);
|
||||
|
||||
export function parseElasticsearchQuery(esQuery: string) {
|
||||
return (question: string) => {
|
||||
try {
|
||||
const replacedQuery = esQuery.replace(/\"{query}\"/g, JSON.stringify(question));
|
||||
const query = JSON.parse(replacedQuery);
|
||||
return query;
|
||||
return query as Partial<SearchRequest>;
|
||||
} catch (e) {
|
||||
throw Error("Failed to parse the Elasticsearch Query. Check Query to make sure it's valid.");
|
||||
throw new Error(
|
||||
i18n.translate('xpack.searchPlayground.serverErrors.parseRetriever', {
|
||||
defaultMessage:
|
||||
"Failed to parse the Elasticsearch Query. Check Query to make sure it's valid.",
|
||||
}),
|
||||
{
|
||||
cause: e,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -143,7 +161,7 @@ export function defineRoutes({
|
|||
model: chatModel,
|
||||
rag: {
|
||||
index: data.indices,
|
||||
retriever: createRetriever(data.elasticsearch_query),
|
||||
retriever: parseElasticsearchQuery(data.elasticsearch_query),
|
||||
content_field: sourceFields,
|
||||
size: Number(data.doc_size),
|
||||
inputTokensLimit: modelPromptLimit,
|
||||
|
@ -269,15 +287,27 @@ export function defineRoutes({
|
|||
if (indices.length === 0) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: 'Indices cannot be empty',
|
||||
message: EMPTY_INDICES_ERROR_MESSAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
let parsedElasticsearchQuery: Partial<SearchRequest>;
|
||||
try {
|
||||
parsedElasticsearchQuery = parseElasticsearchQuery(elasticsearchQuery)(
|
||||
request.body.search_query
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: getErrorMessage(e),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const retriever = createRetriever(elasticsearchQuery)(request.body.search_query);
|
||||
const searchResult = await client.asCurrentUser.search({
|
||||
...parsedElasticsearchQuery,
|
||||
index: indices,
|
||||
retriever: retriever.retriever,
|
||||
from,
|
||||
size,
|
||||
});
|
||||
|
@ -337,7 +367,7 @@ export function defineRoutes({
|
|||
if (indices.length === 0) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: 'Indices cannot be empty',
|
||||
message: EMPTY_INDICES_ERROR_MESSAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -363,4 +393,66 @@ export function defineRoutes({
|
|||
}
|
||||
})
|
||||
);
|
||||
router.post(
|
||||
{
|
||||
path: APIRoutes.POST_QUERY_TEST,
|
||||
options: {
|
||||
access: 'internal',
|
||||
},
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
body: schema.object({
|
||||
query: schema.string(),
|
||||
elasticsearch_query: schema.string(),
|
||||
indices: schema.arrayOf(schema.string()),
|
||||
size: schema.maybe(schema.number({ defaultValue: 10, min: 0 })),
|
||||
from: schema.maybe(schema.number({ defaultValue: 0, min: 0 })),
|
||||
}),
|
||||
},
|
||||
},
|
||||
errorHandler(logger)(async (context, request, response) => {
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
const { elasticsearch_query: elasticsearchQuery, indices, size, from } = request.body;
|
||||
|
||||
if (indices.length === 0) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: EMPTY_INDICES_ERROR_MESSAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let searchQuery: Partial<SearchRequest>;
|
||||
try {
|
||||
searchQuery = parseElasticsearchQuery(elasticsearchQuery)(request.body.query);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: getErrorMessage(e),
|
||||
},
|
||||
});
|
||||
}
|
||||
const searchResponse = await client.asCurrentUser.search({
|
||||
...searchQuery,
|
||||
index: indices,
|
||||
from,
|
||||
size,
|
||||
});
|
||||
const body: QueryTestResponse = {
|
||||
searchResponse,
|
||||
};
|
||||
|
||||
return response.ok({
|
||||
body,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"@kbn/deeplinks-search",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/inference-plugin",
|
||||
"@kbn/code-editor",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -10,6 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context';
|
|||
|
||||
export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const findService = getService('find');
|
||||
const browser = getService('browser');
|
||||
const comboBox = getService('comboBox');
|
||||
const selectIndex = async () => {
|
||||
|
@ -19,6 +20,13 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext)
|
|||
await testSubjects.click('sourceIndex-0');
|
||||
await testSubjects.click('saveButton');
|
||||
};
|
||||
const selectIndexByName = async (indexName: string) => {
|
||||
await testSubjects.existOrFail('addDataSourcesButton');
|
||||
await testSubjects.click('addDataSourcesButton');
|
||||
await testSubjects.existOrFail('selectIndicesFlyout');
|
||||
await findService.clickByCssSelector(`li[title="${indexName}"]`);
|
||||
await testSubjects.click('saveButton');
|
||||
};
|
||||
|
||||
const SESSION_KEY = 'search_playground_session';
|
||||
|
||||
|
@ -55,6 +63,20 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext)
|
|||
expect(state[key]).to.be(value);
|
||||
},
|
||||
},
|
||||
async expectPageSelectorToExist() {
|
||||
await testSubjects.existOrFail('page-mode-select');
|
||||
},
|
||||
async expectPageModeToBeSelected(mode: 'chat' | 'search') {
|
||||
await testSubjects.existOrFail('page-mode-select');
|
||||
const selectedModeText = await testSubjects.getAttribute('page-mode-select', 'value');
|
||||
expect(selectedModeText?.toLowerCase()).to.be(mode);
|
||||
},
|
||||
async selectPageMode(mode: 'chat' | 'search') {
|
||||
await testSubjects.existOrFail('page-mode-select');
|
||||
await testSubjects.selectValue('page-mode-select', mode);
|
||||
const selectedModeText = await testSubjects.getAttribute('page-mode-select', 'value');
|
||||
expect(selectedModeText?.toLowerCase()).to.be(mode);
|
||||
},
|
||||
PlaygroundStartChatPage: {
|
||||
async expectPlaygroundStartChatPageComponentsToExist() {
|
||||
await testSubjects.existOrFail('setupPage');
|
||||
|
@ -250,5 +272,97 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext)
|
|||
await browser.switchTab(0);
|
||||
},
|
||||
},
|
||||
PlaygroundStartSearchPage: {
|
||||
async expectPlaygroundStartSearchPageComponentsToExist() {
|
||||
await testSubjects.existOrFail('setupPage');
|
||||
await testSubjects.existOrFail('addDataSourcesButton');
|
||||
},
|
||||
async expectToSelectIndicesAndLoadSearch(indexName: string) {
|
||||
await selectIndexByName(indexName);
|
||||
await testSubjects.existOrFail('playground-search-section');
|
||||
},
|
||||
},
|
||||
PlaygroundSearchPage: {
|
||||
async expectSearchBarToExist() {
|
||||
await testSubjects.existOrFail('playground-search-section');
|
||||
await testSubjects.existOrFail('searchPlaygroundSearchModeFieldText');
|
||||
},
|
||||
async executeSearchQuery(queryText: string) {
|
||||
await testSubjects.existOrFail('searchPlaygroundSearchModeFieldText');
|
||||
await testSubjects.setValue('searchPlaygroundSearchModeFieldText', `${queryText}`, {
|
||||
typeCharByChar: true,
|
||||
});
|
||||
const searchInput = await testSubjects.find('searchPlaygroundSearchModeFieldText');
|
||||
await searchInput.pressKeys(browser.keys.ENTER);
|
||||
},
|
||||
async expectSearchResultsToExist() {
|
||||
await testSubjects.existOrFail('search-index-documents-result');
|
||||
},
|
||||
async expectSearchResultsNotToExist() {
|
||||
await testSubjects.missingOrFail('search-index-documents-result');
|
||||
},
|
||||
async clearSearchInput() {
|
||||
await testSubjects.existOrFail('searchPlaygroundSearchModeFieldText');
|
||||
await testSubjects.setValue('searchPlaygroundSearchModeFieldText', '');
|
||||
},
|
||||
async hasModeSelectors() {
|
||||
await testSubjects.existOrFail('chatMode');
|
||||
await testSubjects.existOrFail('queryMode');
|
||||
},
|
||||
async expectModeIsSelected(mode: 'chatMode' | 'queryMode') {
|
||||
await testSubjects.existOrFail(mode);
|
||||
const modeSelectedValue = await testSubjects.getAttribute(mode, 'aria-pressed');
|
||||
expect(modeSelectedValue).to.be('true');
|
||||
},
|
||||
async selectPageMode(mode: 'chatMode' | 'queryMode') {
|
||||
await testSubjects.existOrFail(mode);
|
||||
await testSubjects.click(mode);
|
||||
switch (mode) {
|
||||
case 'queryMode':
|
||||
expect(await browser.getCurrentUrl()).contain('/app/search_playground/search/query');
|
||||
break;
|
||||
case 'chatMode':
|
||||
const url = await browser.getCurrentUrl();
|
||||
expect(url).contain('/app/search_playground/search');
|
||||
expect(url).not.contain('/app/search_playground/search/query');
|
||||
break;
|
||||
}
|
||||
},
|
||||
async expectQueryModeComponentsToExist() {
|
||||
await testSubjects.existOrFail('queryModeSection');
|
||||
await testSubjects.existOrFail('RunElasticsearchQueryButton');
|
||||
await testSubjects.existOrFail('ViewElasticsearchQueryResult');
|
||||
await testSubjects.existOrFail('queryModeSection');
|
||||
},
|
||||
async expectQueryModeResultsEmptyState() {
|
||||
await testSubjects.existOrFail('ViewElasticsearchQueryResponseEmptyState');
|
||||
},
|
||||
async expectQueryModeResultsCodeEditor() {
|
||||
await testSubjects.existOrFail('ViewElasticsearchQueryResponse');
|
||||
},
|
||||
async runQueryInQueryMode(queryText: string) {
|
||||
await testSubjects.existOrFail('searchPlaygroundSearchModeFieldText');
|
||||
await testSubjects.setValue('searchPlaygroundSearchModeFieldText', `${queryText}`);
|
||||
await testSubjects.click('RunElasticsearchQueryButton');
|
||||
},
|
||||
async expectFieldToBeSelected(fieldName: string) {
|
||||
await testSubjects.existOrFail(`field-${fieldName}-true`);
|
||||
},
|
||||
async expectFieldNotToBeSelected(fieldName: string) {
|
||||
await testSubjects.existOrFail(`field-${fieldName}-false`);
|
||||
},
|
||||
async clickFieldSwitch(fieldName: string, selected: boolean) {
|
||||
await testSubjects.existOrFail(`field-${fieldName}-${selected}`);
|
||||
await testSubjects.click(`field-${fieldName}-${selected}`);
|
||||
},
|
||||
async getQueryEditorText() {
|
||||
await testSubjects.existOrFail('ViewElasticsearchQueryResult');
|
||||
const result = await testSubjects.getVisibleTextAll('ViewElasticsearchQueryResult');
|
||||
expect(Array.isArray(result)).to.be(true);
|
||||
expect(result.length).to.be(1);
|
||||
expect(typeof result[0]).to.be('string');
|
||||
return result[0];
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -52,5 +52,11 @@ export function SvlSearchNavigationServiceProvider({
|
|||
shouldLoginIfPrompted: false,
|
||||
});
|
||||
},
|
||||
async navigateToSearchPlayground() {
|
||||
await retry.tryForTime(60 * 1000, async () => {
|
||||
await PageObjects.common.navigateToApp('searchPlayground');
|
||||
await testSubjects.existOrFail('svlPlaygroundPage', { timeout: 2000 });
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export default createTestConfig({
|
|||
// add feature flags
|
||||
kbnServerArgs: [
|
||||
`--xpack.cloud.id=ES3_FTR_TESTS:ZmFrZS1kb21haW4uY2xkLmVsc3RjLmNvJGZha2Vwcm9qZWN0aWQuZXMkZmFrZXByb2plY3RpZC5rYg==`,
|
||||
`--uiSettings.overrides.searchPlayground:searchModeEnabled=true`,
|
||||
],
|
||||
// load tests in the index file
|
||||
testFiles: [require.resolve('./index.feature_flags.ts')],
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"aliases": {
|
||||
},
|
||||
"index": "search-playground-books",
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"author": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"name": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
},
|
||||
"page_count": {
|
||||
"type": "long"
|
||||
},
|
||||
"release_date": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
// add tests that require feature flags, defined in config.feature_flags.ts
|
||||
loadTestFile(require.resolve('./search_synonyms/search_synonyms_overview'));
|
||||
loadTestFile(require.resolve('./search_synonyms/search_synonym_detail'));
|
||||
loadTestFile(require.resolve('./search_playground/search_relevance'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const ARCHIVE_INDEX_NAME = 'search-playground-books';
|
||||
const esArchiveIndex =
|
||||
'x-pack/test_serverless/functional/test_suites/search/fixtures/playground_books';
|
||||
|
||||
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||
const pageObjects = getPageObjects([
|
||||
'svlCommonPage',
|
||||
'svlCommonNavigation',
|
||||
'searchPlayground',
|
||||
'embeddedConsole',
|
||||
]);
|
||||
const svlSearchNavigation = getService('svlSearchNavigation');
|
||||
const browser = getService('browser');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const createIndex = async () => await esArchiver.load(esArchiveIndex);
|
||||
|
||||
describe('Serverless Search Relevance Playground', function () {
|
||||
before(async () => {
|
||||
await createIndex();
|
||||
await pageObjects.svlCommonPage.loginWithRole('developer');
|
||||
await pageObjects.searchPlayground.session.clearSession();
|
||||
await svlSearchNavigation.navigateToSearchPlayground();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(esArchiveIndex);
|
||||
});
|
||||
|
||||
it('should be able to navigate to the search relevance playground', async () => {
|
||||
await pageObjects.searchPlayground.expectPageSelectorToExist();
|
||||
// playground defaults to chat mode
|
||||
await pageObjects.searchPlayground.expectPageModeToBeSelected('chat');
|
||||
await pageObjects.searchPlayground.selectPageMode('search');
|
||||
expect(await browser.getCurrentUrl()).contain('/app/search_playground/search');
|
||||
});
|
||||
it('should start with setup page', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundStartSearchPage.expectPlaygroundStartSearchPageComponentsToExist();
|
||||
await pageObjects.searchPlayground.PlaygroundStartSearchPage.expectToSelectIndicesAndLoadSearch(
|
||||
ARCHIVE_INDEX_NAME
|
||||
);
|
||||
});
|
||||
it('should be able to search index', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.hasModeSelectors();
|
||||
// Preview mode enum shares data test subj with chat page mode
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectModeIsSelected('chatMode');
|
||||
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectSearchBarToExist();
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.executeSearchQuery('Neal');
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectSearchResultsToExist();
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.executeSearchQuery('gibberish');
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectSearchResultsNotToExist();
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.clearSearchInput();
|
||||
});
|
||||
it('should have query mode', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.selectPageMode('queryMode');
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectQueryModeComponentsToExist();
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectQueryModeResultsEmptyState();
|
||||
});
|
||||
it('should support changing fields to search', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectFieldToBeSelected('author');
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectFieldNotToBeSelected('name');
|
||||
const queryEditorTextBefore =
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.getQueryEditorText();
|
||||
expect(queryEditorTextBefore).to.contain(`"author"`);
|
||||
expect(queryEditorTextBefore).not.to.contain('"name"');
|
||||
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.clickFieldSwitch('name', false);
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectFieldToBeSelected('name');
|
||||
|
||||
let queryEditorText =
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.getQueryEditorText();
|
||||
expect(queryEditorText).to.contain('"author"');
|
||||
expect(queryEditorText).to.contain('"name"');
|
||||
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.clickFieldSwitch('author', true);
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectFieldNotToBeSelected('author');
|
||||
|
||||
queryEditorText =
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.getQueryEditorText();
|
||||
expect(queryEditorText).not.to.contain('"author"');
|
||||
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.clickFieldSwitch('author', false);
|
||||
});
|
||||
it('should support running query in query mode', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.runQueryInQueryMode('atwood');
|
||||
await pageObjects.searchPlayground.PlaygroundSearchPage.expectQueryModeResultsCodeEditor();
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Reference in a new issue