mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.15`: - [[Search][Playground] Fix playground selected fields (#188278)](https://github.com/elastic/kibana/pull/188278) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Yan Savitski","email":"yan.savitski@elastic.co"},"sourceCommit":{"committedDate":"2024-07-15T22:37:41Z","message":"[Search][Playground] Fix playground selected fields (#188278)\n\nWhen user selected fields in query mode, goes to chat mode and then back\r\nto query mode. Some fields may return to default value\r\n\r\n---------\r\n\r\nCo-authored-by: Joseph McElroy <joseph.mcelroy@elastic.co>","sha":"37845b04e8df7e61a2606634df28b6800cf55ed7","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:EnterpriseSearch","v8.15.0","v8.16.0"],"title":"[Search][Playground] Fix playground selected fields","number":188278,"url":"https://github.com/elastic/kibana/pull/188278","mergeCommit":{"message":"[Search][Playground] Fix playground selected fields (#188278)\n\nWhen user selected fields in query mode, goes to chat mode and then back\r\nto query mode. Some fields may return to default value\r\n\r\n---------\r\n\r\nCo-authored-by: Joseph McElroy <joseph.mcelroy@elastic.co>","sha":"37845b04e8df7e61a2606634df28b6800cf55ed7"}},"sourceBranch":"main","suggestedTargetBranches":["8.15"],"targetPullRequestStates":[{"branch":"8.15","label":"v8.15.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/188278","number":188278,"mergeCommit":{"message":"[Search][Playground] Fix playground selected fields (#188278)\n\nWhen user selected fields in query mode, goes to chat mode and then back\r\nto query mode. Some fields may return to default value\r\n\r\n---------\r\n\r\nCo-authored-by: Joseph McElroy <joseph.mcelroy@elastic.co>","sha":"37845b04e8df7e61a2606634df28b6800cf55ed7"}}]}] BACKPORT--> Co-authored-by: Yan Savitski <yan.savitski@elastic.co>
This commit is contained in:
parent
cc7df3dce5
commit
c2ae24f1bd
22 changed files with 494 additions and 311 deletions
|
@ -7,8 +7,6 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiPageTemplate } from '@elastic/eui';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { queryClient } from './utils/query_client';
|
||||
import { PlaygroundProvider } from './providers/playground_provider';
|
||||
|
||||
import { App } from './components/app';
|
||||
|
@ -25,19 +23,17 @@ export const ChatPlaygroundOverview: React.FC = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PlaygroundProvider>
|
||||
<EuiPageTemplate
|
||||
offset={0}
|
||||
restrictWidth={false}
|
||||
data-test-subj="svlPlaygroundPage"
|
||||
grow={false}
|
||||
panelled={false}
|
||||
>
|
||||
<App showDocs />
|
||||
{embeddableConsole}
|
||||
</EuiPageTemplate>
|
||||
</PlaygroundProvider>
|
||||
</QueryClientProvider>
|
||||
<PlaygroundProvider>
|
||||
<EuiPageTemplate
|
||||
offset={0}
|
||||
restrictWidth={false}
|
||||
data-test-subj="svlPlaygroundPage"
|
||||
grow={false}
|
||||
panelled={false}
|
||||
>
|
||||
<App showDocs />
|
||||
{embeddableConsole}
|
||||
</EuiPageTemplate>
|
||||
</PlaygroundProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { QueryMode } from './query_mode/query_mode';
|
||||
import { SetupPage } from './setup_page/setup_page';
|
||||
import { Header } from './header';
|
||||
|
@ -28,14 +28,17 @@ export enum ViewMode {
|
|||
export const App: React.FC<AppProps> = ({ showDocs = false }) => {
|
||||
const [showSetupPage, setShowSetupPage] = useState(true);
|
||||
const [selectedMode, setSelectedMode] = useState<ViewMode>(ViewMode.chat);
|
||||
const { watch } = useFormContext<ChatForm>();
|
||||
const { data: connectors } = useLoadConnectors();
|
||||
const hasSelectedIndices = watch(ChatFormFields.indices).length;
|
||||
const hasSelectedIndices = useWatch<ChatForm, ChatFormFields.indices>({
|
||||
name: ChatFormFields.indices,
|
||||
}).length;
|
||||
const handleModeChange = (id: string) => setSelectedMode(id as ViewMode);
|
||||
|
||||
useEffect(() => {
|
||||
if (showSetupPage && connectors?.length && hasSelectedIndices) {
|
||||
setShowSetupPage(false);
|
||||
} else if (!showSetupPage && (!connectors?.length || !hasSelectedIndices)) {
|
||||
setShowSetupPage(true);
|
||||
}
|
||||
}, [connectors, hasSelectedIndices, showSetupPage]);
|
||||
|
||||
|
|
|
@ -51,6 +51,16 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
|
|||
index1: ['field1'],
|
||||
index2: ['field1'],
|
||||
},
|
||||
[ChatFormFields.elasticsearchQuery]: {
|
||||
retriever: {
|
||||
rrf: {
|
||||
retrievers: [
|
||||
{ standard: { query: { multi_match: { query: '{query}', fields: ['field1'] } } } },
|
||||
{ standard: { query: { multi_match: { query: '{query}', fields: ['field1'] } } } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
|
|
|
@ -40,7 +40,7 @@ const isQueryFieldSelected = (
|
|||
export const QueryMode: React.FC = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const usageTracker = useUsageTracker();
|
||||
const { fields, isFieldsLoading } = useSourceIndicesFields();
|
||||
const { fields } = useSourceIndicesFields();
|
||||
const sourceFields = useWatch<ChatForm, ChatFormFields.sourceFields>({
|
||||
name: ChatFormFields.sourceFields,
|
||||
});
|
||||
|
@ -49,10 +49,9 @@ export const QueryMode: React.FC = () => {
|
|||
} = useController<ChatForm, ChatFormFields.queryFields>({
|
||||
name: ChatFormFields.queryFields,
|
||||
});
|
||||
|
||||
const {
|
||||
field: { onChange: elasticsearchQueryChange },
|
||||
} = useController({
|
||||
field: { onChange: elasticsearchQueryChange, value: elasticsearchQuery },
|
||||
} = useController<ChatForm, ChatFormFields.elasticsearchQuery>({
|
||||
name: ChatFormFields.elasticsearchQuery,
|
||||
});
|
||||
|
||||
|
@ -70,12 +69,7 @@ export const QueryMode: React.FC = () => {
|
|||
useEffect(() => {
|
||||
usageTracker?.load(AnalyticsEvents.queryModeLoaded);
|
||||
}, [usageTracker]);
|
||||
|
||||
const query = useMemo(
|
||||
() =>
|
||||
!isFieldsLoading && JSON.stringify(createQuery(queryFields, sourceFields, fields), null, 2),
|
||||
[isFieldsLoading, queryFields, sourceFields, fields]
|
||||
);
|
||||
const query = useMemo(() => JSON.stringify(elasticsearchQuery, null, 2), [elasticsearchQuery]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
|
@ -158,6 +152,7 @@ export const QueryMode: React.FC = () => {
|
|||
checked={checked}
|
||||
onChange={(e) => updateFields(index, field.name, e.target.checked)}
|
||||
compressed
|
||||
data-test-subj={`field-${field.name}-${checked}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -41,7 +41,6 @@ describe('SelectIndicesFlyout', () => {
|
|||
indices: ['index1', 'index2'],
|
||||
setIndices: jest.fn(),
|
||||
fields: {},
|
||||
loading: false,
|
||||
addIndex: () => {},
|
||||
removeIndex: () => {},
|
||||
isFieldsLoading: false,
|
||||
|
@ -96,7 +95,6 @@ describe('SelectIndicesFlyout', () => {
|
|||
indices: [],
|
||||
setIndices: jest.fn(),
|
||||
fields: {},
|
||||
loading: false,
|
||||
addIndex: () => {},
|
||||
removeIndex: () => {},
|
||||
isFieldsLoading: false,
|
||||
|
|
|
@ -13,13 +13,11 @@ import {
|
|||
EuiLoadingSpinner,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { CreateIndexButton } from './create_index_button';
|
||||
import { useQueryIndices } from '../../hooks/use_query_indices';
|
||||
import { docLinks } from '../../../common/doc_links';
|
||||
import { useSourceIndicesFields } from '../../hooks/use_source_indices_field';
|
||||
import { useUsageTracker } from '../../hooks/use_usage_tracker';
|
||||
import { AnalyticsEvents } from '../../analytics/constants';
|
||||
import { ConnectLLMButton } from './connect_llm_button';
|
||||
|
@ -27,16 +25,7 @@ import { AddDataSources } from './add_data_sources';
|
|||
|
||||
export const SetupPage: React.FC = () => {
|
||||
const usageTracker = useUsageTracker();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { indices, isLoading: isIndicesLoading } = useQueryIndices();
|
||||
const index = useMemo(() => searchParams.get('default-index'), [searchParams]);
|
||||
const { addIndex } = useSourceIndicesFields();
|
||||
|
||||
useEffect(() => {
|
||||
if (index) {
|
||||
addIndex(index);
|
||||
}
|
||||
}, [index, addIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
usageTracker?.load(AnalyticsEvents.setupChatPageLoaded);
|
||||
|
|
|
@ -9,9 +9,7 @@ import React from 'react';
|
|||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { AppPluginStartDependencies } from './types';
|
||||
import { queryClient } from './utils/query_client';
|
||||
import { AppProps } from './components/app';
|
||||
|
||||
export const Playground = dynamic<React.FC<AppProps>>(async () => ({
|
||||
|
@ -31,8 +29,6 @@ export const getPlaygroundProvider =
|
|||
(props: React.ComponentProps<typeof PlaygroundProvider>) =>
|
||||
(
|
||||
<KibanaContextProvider services={{ ...core, ...services }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PlaygroundProvider {...props} />
|
||||
</QueryClientProvider>
|
||||
<PlaygroundProvider {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { useLoadFieldsByIndices } from './use_load_fields_by_indices';
|
||||
import { useUsageTracker } from './use_usage_tracker';
|
||||
import { useIndicesFields } from './use_indices_fields';
|
||||
import { createQuery, getDefaultQueryFields, getDefaultSourceFields } from '../utils/create_query';
|
||||
import { AnalyticsEvents } from '../analytics/constants';
|
||||
import { ChatFormFields } from '../types';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('./use_usage_tracker');
|
||||
jest.mock('./use_indices_fields');
|
||||
jest.mock('../utils/create_query');
|
||||
|
||||
describe('useLoadFieldsByIndices', () => {
|
||||
const mockSetValue = jest.fn();
|
||||
const mockGetValues = jest.fn();
|
||||
const mockWatch = jest.fn();
|
||||
const mockUsageTracker = { count: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useUsageTracker as jest.Mock).mockReturnValue(mockUsageTracker);
|
||||
(useIndicesFields as jest.Mock).mockReturnValue({ fields: {} });
|
||||
(getDefaultQueryFields as jest.Mock).mockReturnValue({ newIndex: ['title', 'body'] });
|
||||
(getDefaultSourceFields as jest.Mock).mockReturnValue({ testIndex: ['content'] });
|
||||
(createQuery as jest.Mock).mockReturnValue('mocked query');
|
||||
});
|
||||
|
||||
const setup = () => {
|
||||
return renderHook(() =>
|
||||
useLoadFieldsByIndices({
|
||||
watch: mockWatch,
|
||||
setValue: mockSetValue,
|
||||
getValues: mockGetValues,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
it('sets values and tracks usage on fields change', () => {
|
||||
(useIndicesFields as jest.Mock).mockReturnValue({ fields: { newIndex: {}, testIndex: {} } });
|
||||
mockGetValues.mockReturnValueOnce([{}, {}]);
|
||||
mockWatch.mockReturnValue(['index1']);
|
||||
|
||||
setup();
|
||||
|
||||
expect(mockSetValue).toHaveBeenCalledWith(ChatFormFields.elasticsearchQuery, 'mocked query');
|
||||
expect(mockSetValue).toHaveBeenCalledWith(ChatFormFields.queryFields, {
|
||||
newIndex: ['title', 'body'],
|
||||
});
|
||||
expect(mockSetValue).toHaveBeenCalledWith(ChatFormFields.sourceFields, {
|
||||
testIndex: ['content'],
|
||||
});
|
||||
expect(mockUsageTracker.count).toHaveBeenCalledWith(AnalyticsEvents.sourceFieldsLoaded, 2);
|
||||
});
|
||||
|
||||
describe('merge fields', () => {
|
||||
it('save changed fields', () => {
|
||||
(getDefaultQueryFields as jest.Mock).mockReturnValue({ index: ['title', 'body'] });
|
||||
(getDefaultSourceFields as jest.Mock).mockReturnValue({ index: ['title'] });
|
||||
mockGetValues.mockReturnValueOnce([{ index: [] }, { index: ['body'] }]);
|
||||
|
||||
setup();
|
||||
|
||||
expect(mockSetValue).toHaveBeenNthCalledWith(2, ChatFormFields.queryFields, {
|
||||
index: [],
|
||||
});
|
||||
expect(mockSetValue).toHaveBeenNthCalledWith(3, ChatFormFields.sourceFields, {
|
||||
index: ['body'],
|
||||
});
|
||||
});
|
||||
|
||||
it('remove old indices from fields', () => {
|
||||
(getDefaultQueryFields as jest.Mock).mockReturnValue({ index: ['title', 'body'] });
|
||||
(getDefaultSourceFields as jest.Mock).mockReturnValue({ index: ['title'] });
|
||||
mockGetValues.mockReturnValueOnce([
|
||||
{ index: [], oldIndex: ['title'] },
|
||||
{ index: ['body'], oldIndex: ['title'] },
|
||||
]);
|
||||
|
||||
setup();
|
||||
|
||||
expect(mockSetValue).toHaveBeenNthCalledWith(2, ChatFormFields.queryFields, {
|
||||
index: [],
|
||||
});
|
||||
expect(mockSetValue).toHaveBeenNthCalledWith(3, ChatFormFields.sourceFields, {
|
||||
index: ['body'],
|
||||
});
|
||||
});
|
||||
|
||||
it('add new indices to fields', () => {
|
||||
(getDefaultQueryFields as jest.Mock).mockReturnValue({
|
||||
index: ['title', 'body'],
|
||||
newIndex: ['content'],
|
||||
});
|
||||
(getDefaultSourceFields as jest.Mock).mockReturnValue({
|
||||
index: ['title'],
|
||||
newIndex: ['content'],
|
||||
});
|
||||
mockGetValues.mockReturnValueOnce([
|
||||
{ index: [], oldIndex: ['title'] },
|
||||
{ index: ['body'], oldIndex: ['title'] },
|
||||
]);
|
||||
|
||||
setup();
|
||||
|
||||
expect(mockSetValue).toHaveBeenNthCalledWith(2, ChatFormFields.queryFields, {
|
||||
index: [],
|
||||
newIndex: ['content'],
|
||||
});
|
||||
expect(mockSetValue).toHaveBeenNthCalledWith(3, ChatFormFields.sourceFields, {
|
||||
index: ['body'],
|
||||
newIndex: ['content'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form/dist/types';
|
||||
import { useUsageTracker } from './use_usage_tracker';
|
||||
import { ChatForm, ChatFormFields } from '../types';
|
||||
import { useIndicesFields } from './use_indices_fields';
|
||||
import {
|
||||
createQuery,
|
||||
getDefaultQueryFields,
|
||||
getDefaultSourceFields,
|
||||
IndexFields,
|
||||
} from '../utils/create_query';
|
||||
import { AnalyticsEvents } from '../analytics/constants';
|
||||
|
||||
const mergeDefaultAndCurrentValues = (
|
||||
defaultFields: IndexFields,
|
||||
currentFields: IndexFields
|
||||
): IndexFields =>
|
||||
Object.keys(defaultFields).reduce<IndexFields>((result, key) => {
|
||||
result[key] = currentFields?.[key] ?? defaultFields[key];
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
export const useLoadFieldsByIndices = ({
|
||||
watch,
|
||||
setValue,
|
||||
getValues,
|
||||
}: Pick<UseFormReturn<ChatForm>, 'watch' | 'getValues' | 'setValue'>) => {
|
||||
const usageTracker = useUsageTracker();
|
||||
const selectedIndices = watch(ChatFormFields.indices);
|
||||
const { fields } = useIndicesFields(selectedIndices);
|
||||
|
||||
useEffect(() => {
|
||||
const [queryFields, sourceFields] = getValues([
|
||||
ChatFormFields.queryFields,
|
||||
ChatFormFields.sourceFields,
|
||||
]);
|
||||
const defaultFields = getDefaultQueryFields(fields);
|
||||
const defaultSourceFields = getDefaultSourceFields(fields);
|
||||
const mergedQueryFields = mergeDefaultAndCurrentValues(defaultFields, queryFields);
|
||||
const mergedSourceFields = mergeDefaultAndCurrentValues(defaultSourceFields, sourceFields);
|
||||
|
||||
setValue(
|
||||
ChatFormFields.elasticsearchQuery,
|
||||
createQuery(mergedQueryFields, mergedSourceFields, fields)
|
||||
);
|
||||
setValue(ChatFormFields.queryFields, mergedQueryFields);
|
||||
setValue(ChatFormFields.sourceFields, mergedSourceFields);
|
||||
|
||||
usageTracker?.count(AnalyticsEvents.sourceFieldsLoaded, Object.values(fields)?.flat()?.length);
|
||||
}, [fields, getValues, setValue, usageTracker]);
|
||||
};
|
|
@ -7,86 +7,24 @@
|
|||
|
||||
import { useController } from 'react-hook-form';
|
||||
import { IndexName } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { merge } from 'lodash';
|
||||
import { useCallback } from 'react';
|
||||
import { useIndicesFields } from './use_indices_fields';
|
||||
import { ChatForm, ChatFormFields } from '../types';
|
||||
import {
|
||||
createQuery,
|
||||
getDefaultQueryFields,
|
||||
getDefaultSourceFields,
|
||||
IndexFields,
|
||||
} from '../utils/create_query';
|
||||
import { ChatFormFields } from '../types';
|
||||
import { useUsageTracker } from './use_usage_tracker';
|
||||
import { AnalyticsEvents } from '../analytics/constants';
|
||||
|
||||
export const getIndicesWithNoSourceFields = (
|
||||
defaultSourceFields: IndexFields
|
||||
): string | undefined => {
|
||||
const indices: string[] = [];
|
||||
Object.keys(defaultSourceFields).forEach((index: string) => {
|
||||
if (defaultSourceFields[index].length === 0) {
|
||||
indices.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
return indices.length === 0 ? undefined : indices.join();
|
||||
};
|
||||
|
||||
export const useSourceIndicesFields = () => {
|
||||
const usageTracker = useUsageTracker();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
field: { value: selectedIndices, onChange: onIndicesChange },
|
||||
} = useController({
|
||||
name: ChatFormFields.indices,
|
||||
});
|
||||
|
||||
const {
|
||||
field: { onChange: onElasticsearchQueryChange },
|
||||
} = useController({
|
||||
name: ChatFormFields.elasticsearchQuery,
|
||||
defaultValue: {},
|
||||
});
|
||||
|
||||
const {
|
||||
field: { onChange: onQueryFieldsOnChange, value: queryFields },
|
||||
} = useController<ChatForm, ChatFormFields.queryFields>({
|
||||
name: ChatFormFields.queryFields,
|
||||
});
|
||||
|
||||
const {
|
||||
field: { onChange: onSourceFieldsChange, value: sourceFields },
|
||||
} = useController({
|
||||
name: ChatFormFields.sourceFields,
|
||||
});
|
||||
const { fields, isLoading: isFieldsLoading } = useIndicesFields(selectedIndices);
|
||||
|
||||
useEffect(() => {
|
||||
if (fields) {
|
||||
const defaultFields = getDefaultQueryFields(fields);
|
||||
const defaultSourceFields = getDefaultSourceFields(fields);
|
||||
const mergedQueryFields = merge(defaultFields, queryFields);
|
||||
const mergedSourceFields = merge(defaultSourceFields, sourceFields);
|
||||
|
||||
onElasticsearchQueryChange(createQuery(mergedQueryFields, mergedSourceFields, fields));
|
||||
onQueryFieldsOnChange(mergedQueryFields);
|
||||
|
||||
onSourceFieldsChange(mergedSourceFields);
|
||||
usageTracker?.count(
|
||||
AnalyticsEvents.sourceFieldsLoaded,
|
||||
Object.values(fields)?.flat()?.length
|
||||
);
|
||||
}
|
||||
setLoading(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fields]);
|
||||
|
||||
const addIndex = useCallback(
|
||||
(newIndex: IndexName) => {
|
||||
const newIndices = [...selectedIndices, newIndex];
|
||||
setLoading(true);
|
||||
onIndicesChange(newIndices);
|
||||
usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, newIndices.length);
|
||||
},
|
||||
|
@ -96,7 +34,6 @@ export const useSourceIndicesFields = () => {
|
|||
const removeIndex = useCallback(
|
||||
(index: IndexName) => {
|
||||
const newIndices = selectedIndices.filter((indexName: string) => indexName !== index);
|
||||
setLoading(true);
|
||||
onIndicesChange(newIndices);
|
||||
usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, newIndices.length);
|
||||
},
|
||||
|
@ -105,7 +42,6 @@ export const useSourceIndicesFields = () => {
|
|||
|
||||
const setIndices = useCallback(
|
||||
(indices: IndexName[]) => {
|
||||
setLoading(true);
|
||||
onIndicesChange(indices);
|
||||
usageTracker?.count(AnalyticsEvents.sourceIndexUpdated, indices.length);
|
||||
},
|
||||
|
@ -115,7 +51,6 @@ export const useSourceIndicesFields = () => {
|
|||
return {
|
||||
indices: selectedIndices,
|
||||
fields,
|
||||
loading,
|
||||
isFieldsLoading,
|
||||
addIndex,
|
||||
removeIndex,
|
||||
|
|
|
@ -14,14 +14,16 @@ import * as ReactHookForm from 'react-hook-form';
|
|||
jest.mock('./use_kibana', () => ({
|
||||
useKibana: jest.fn(),
|
||||
}));
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
useSearchParams: jest.fn(() => [{ get: jest.fn() }]),
|
||||
}));
|
||||
|
||||
let formHookSpy: jest.SpyInstance;
|
||||
|
||||
import { getIndicesWithNoSourceFields, useSourceIndicesFields } from './use_source_indices_field';
|
||||
import { useSourceIndicesFields } from './use_source_indices_field';
|
||||
import { IndicesQuerySourceFields } from '../types';
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/181102
|
||||
describe.skip('useSourceIndicesFields Hook', () => {
|
||||
describe('useSourceIndicesFields Hook', () => {
|
||||
let postMock: jest.Mock;
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
|
@ -53,6 +55,9 @@ describe.skip('useSourceIndicesFields Hook', () => {
|
|||
services: {
|
||||
http: {
|
||||
post: postMock,
|
||||
get: jest.fn(() => {
|
||||
return [];
|
||||
}),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
@ -62,25 +67,7 @@ describe.skip('useSourceIndicesFields Hook', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getIndicesWithNoSourceFields', () => {
|
||||
it('should return undefined if all indices have source fields', () => {
|
||||
const defaultSourceFields = {
|
||||
index1: ['field1'],
|
||||
index2: ['field2'],
|
||||
};
|
||||
expect(getIndicesWithNoSourceFields(defaultSourceFields)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return indices with no source fields', () => {
|
||||
const defaultSourceFields = {
|
||||
index1: ['field1'],
|
||||
index2: [],
|
||||
};
|
||||
expect(getIndicesWithNoSourceFields(defaultSourceFields)).toBe('index2');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle addIndex correctly changing indices and updating loading state', async () => {
|
||||
it('should handle addIndex correctly changing indices', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSourceIndicesFields(), { wrapper });
|
||||
const { getValues } = formHookSpy.mock.results[0].value;
|
||||
|
||||
|
@ -89,10 +76,20 @@ describe.skip('useSourceIndicesFields Hook', () => {
|
|||
expect(getValues()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"doc_size": 3,
|
||||
"elasticsearch_query": Object {},
|
||||
"elasticsearch_query": Object {
|
||||
"retriever": Object {
|
||||
"standard": Object {
|
||||
"query": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"indices": Array [],
|
||||
"prompt": "You are an assistant for question-answering tasks.",
|
||||
"query_fields": Object {},
|
||||
"source_fields": Object {},
|
||||
"summarization_model": undefined,
|
||||
}
|
||||
`);
|
||||
result.current.addIndex('newIndex');
|
||||
|
@ -101,13 +98,11 @@ describe.skip('useSourceIndicesFields Hook', () => {
|
|||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
expect(result.current.indices).toEqual(['newIndex']);
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
expect(postMock).toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(getValues()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"doc_size": 3,
|
||||
|
@ -128,115 +123,17 @@ describe.skip('useSourceIndicesFields Hook', () => {
|
|||
"newIndex",
|
||||
],
|
||||
"prompt": "You are an assistant for question-answering tasks.",
|
||||
"query_fields": Object {
|
||||
"newIndex": Array [
|
||||
"field1",
|
||||
],
|
||||
},
|
||||
"source_fields": Object {
|
||||
"newIndex": Array [
|
||||
"field1",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide warning message for adding an index without any fields', async () => {
|
||||
const querySourceFields: IndicesQuerySourceFields = {
|
||||
missing_fields_index: {
|
||||
elser_query_fields: [],
|
||||
dense_vector_query_fields: [],
|
||||
bm25_query_fields: [],
|
||||
source_fields: [],
|
||||
skipped_fields: 0,
|
||||
semantic_fields: [],
|
||||
},
|
||||
};
|
||||
|
||||
postMock.mockResolvedValue(querySourceFields);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSourceIndicesFields(), { wrapper });
|
||||
const { getValues } = formHookSpy.mock.results[0].value;
|
||||
|
||||
await act(async () => {
|
||||
result.current.addIndex('missing_fields_index');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(postMock).toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(getValues()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"doc_size": 3,
|
||||
"elasticsearch_query": Object {
|
||||
"retriever": Object {
|
||||
"standard": Object {
|
||||
"query": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"indices": Array [
|
||||
"missing_fields_index",
|
||||
],
|
||||
"prompt": "You are an assistant for question-answering tasks.",
|
||||
"source_fields": Object {
|
||||
"missing_fields_index": Array [],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not provide any warning message for adding and then removing an index without any fields', async () => {
|
||||
const querySourceFields: IndicesQuerySourceFields = {
|
||||
missing_fields_index: {
|
||||
elser_query_fields: [],
|
||||
dense_vector_query_fields: [],
|
||||
bm25_query_fields: [],
|
||||
source_fields: [],
|
||||
skipped_fields: 0,
|
||||
semantic_fields: [],
|
||||
},
|
||||
};
|
||||
|
||||
postMock.mockResolvedValue(querySourceFields);
|
||||
|
||||
const { result } = renderHook(() => useSourceIndicesFields(), { wrapper });
|
||||
const { getValues } = formHookSpy.mock.results[0].value;
|
||||
|
||||
await act(async () => {
|
||||
result.current.addIndex('missing_fields_index');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.removeIndex('missing_fields_index');
|
||||
});
|
||||
|
||||
expect(postMock).toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(getValues()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"doc_size": 3,
|
||||
"elasticsearch_query": Object {
|
||||
"retriever": Object {
|
||||
"standard": Object {
|
||||
"query": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"indices": Array [],
|
||||
"prompt": "You are an assistant for question-answering tasks.",
|
||||
"source_fields": Object {
|
||||
"missing_fields_index": Array [],
|
||||
},
|
||||
"summarization_model": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { FormProvider } from './form_provider';
|
||||
import { useLoadFieldsByIndices } from '../hooks/use_load_fields_by_indices';
|
||||
import { useLLMsModels } from '../hooks/use_llms_models';
|
||||
import * as ReactHookForm from 'react-hook-form';
|
||||
import { ChatFormFields } from '../types';
|
||||
|
||||
jest.mock('../hooks/use_load_fields_by_indices');
|
||||
jest.mock('../hooks/use_llms_models');
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
useSearchParams: jest.fn(() => [{ get: jest.fn() }]),
|
||||
}));
|
||||
|
||||
let formHookSpy: jest.SpyInstance;
|
||||
|
||||
const mockUseLoadFieldsByIndices = useLoadFieldsByIndices as jest.Mock;
|
||||
const mockUseLLMsModels = useLLMsModels as jest.Mock;
|
||||
|
||||
describe('FormProvider', () => {
|
||||
beforeEach(() => {
|
||||
formHookSpy = jest.spyOn(ReactHookForm, 'useForm');
|
||||
mockUseLLMsModels.mockReturnValue([]);
|
||||
mockUseLoadFieldsByIndices.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the form provider with initial values, no default model', async () => {
|
||||
render(
|
||||
<FormProvider>
|
||||
<div>Test Child Component</div>
|
||||
</FormProvider>
|
||||
);
|
||||
|
||||
const { getValues } = formHookSpy.mock.results[0].value;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getValues()).toEqual({
|
||||
doc_size: 3,
|
||||
indices: [],
|
||||
prompt: 'You are an assistant for question-answering tasks.',
|
||||
source_fields: {},
|
||||
summarization_model: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the default summarization model with models available', async () => {
|
||||
const mockModels = [
|
||||
{ id: 'model1', name: 'Model 1', disabled: false },
|
||||
{ id: 'model2', name: 'Model 2', disabled: true },
|
||||
];
|
||||
|
||||
mockUseLLMsModels.mockReturnValueOnce(mockModels);
|
||||
|
||||
render(
|
||||
<FormProvider>
|
||||
<div>Test Child Component</div>
|
||||
</FormProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseLoadFieldsByIndices).toHaveBeenCalled();
|
||||
const defaultModel = mockModels.find((model) => !model.disabled);
|
||||
const { getValues } = formHookSpy.mock.results[0].value;
|
||||
|
||||
expect(getValues(ChatFormFields.summarizationModel)).toEqual(defaultModel);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not set a disabled model as the default summarization model', async () => {
|
||||
const modelsWithAllDisabled = [
|
||||
{ id: 'model1', name: 'Model 1', disabled: true },
|
||||
{ id: 'model2', name: 'Model 2', disabled: true },
|
||||
];
|
||||
|
||||
mockUseLLMsModels.mockReturnValueOnce(modelsWithAllDisabled);
|
||||
|
||||
render(
|
||||
<FormProvider>
|
||||
<div>Test Child Component</div>
|
||||
</FormProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseLoadFieldsByIndices).toHaveBeenCalled();
|
||||
expect(modelsWithAllDisabled.find((model) => !model.disabled)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { FormProvider as ReactHookFormProvider, useForm } from 'react-hook-form';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { useLoadFieldsByIndices } from '../hooks/use_load_fields_by_indices';
|
||||
import { ChatForm, ChatFormFields } from '../types';
|
||||
import { useLLMsModels } from '../hooks/use_llms_models';
|
||||
|
||||
export const FormProvider: React.FC = ({ children }) => {
|
||||
const models = useLLMsModels();
|
||||
const [searchParams] = useSearchParams();
|
||||
const index = useMemo(() => searchParams.get('default-index'), [searchParams]);
|
||||
const form = useForm<ChatForm>({
|
||||
defaultValues: {
|
||||
prompt: 'You are an assistant for question-answering tasks.',
|
||||
doc_size: 3,
|
||||
source_fields: {},
|
||||
indices: index ? [index] : [],
|
||||
summarization_model: undefined,
|
||||
},
|
||||
});
|
||||
useLoadFieldsByIndices({ watch: form.watch, setValue: form.setValue, getValues: form.getValues });
|
||||
|
||||
useEffect(() => {
|
||||
const defaultModel = models.find((model) => !model.disabled);
|
||||
|
||||
if (defaultModel && !form.getValues(ChatFormFields.summarizationModel)) {
|
||||
form.setValue(ChatFormFields.summarizationModel, defaultModel);
|
||||
}
|
||||
}, [form, models]);
|
||||
|
||||
return <ReactHookFormProvider {...form}>{children}</ReactHookFormProvider>;
|
||||
};
|
|
@ -5,30 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useLLMsModels } from '../hooks/use_llms_models';
|
||||
import { ChatForm, ChatFormFields } from '../types';
|
||||
import React, { FC } from 'react';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { queryClient } from '../utils/query_client';
|
||||
import { FormProvider } from './form_provider';
|
||||
|
||||
export const PlaygroundProvider: FC = ({ children }) => {
|
||||
const models = useLLMsModels();
|
||||
const form = useForm<ChatForm>({
|
||||
defaultValues: {
|
||||
prompt: 'You are an assistant for question-answering tasks.',
|
||||
doc_size: 3,
|
||||
source_fields: {},
|
||||
indices: [],
|
||||
summarization_model: {},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const defaultModel = models.find((model) => !model.disabled);
|
||||
|
||||
if (defaultModel) {
|
||||
form.setValue(ChatFormFields.summarizationModel, defaultModel);
|
||||
}
|
||||
}, [form, models]);
|
||||
|
||||
return <FormProvider {...form}>{children}</FormProvider>;
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<FormProvider>{children}</FormProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
HealthStatus,
|
||||
IndexName,
|
||||
IndicesStatsIndexMetadataState,
|
||||
QueryDslQueryContainer,
|
||||
Uuid,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
|
@ -74,7 +73,7 @@ export interface ChatForm {
|
|||
[ChatFormFields.citations]: boolean;
|
||||
[ChatFormFields.indices]: string[];
|
||||
[ChatFormFields.summarizationModel]: LLMModel;
|
||||
[ChatFormFields.elasticsearchQuery]: { query: QueryDslQueryContainer };
|
||||
[ChatFormFields.elasticsearchQuery]: { retriever: unknown }; // RetrieverContainer leads to "Type instantiation is excessively deep and possibly infinite" error
|
||||
[ChatFormFields.sourceFields]: { [index: string]: string[] };
|
||||
[ChatFormFields.docSize]: number;
|
||||
[ChatFormFields.queryFields]: { [index: string]: string[] };
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
import { IndicesQuerySourceFields } from '../types';
|
||||
import { createQuery, getDefaultQueryFields, getDefaultSourceFields } from './create_query';
|
||||
import {
|
||||
createQuery,
|
||||
getDefaultQueryFields,
|
||||
getDefaultSourceFields,
|
||||
getIndicesWithNoSourceFields,
|
||||
} from './create_query';
|
||||
|
||||
describe('create_query', () => {
|
||||
const sourceFields = { index1: [], index2: [] };
|
||||
|
@ -965,4 +970,28 @@ describe('create_query', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIndicesWithNoSourceFields', () => {
|
||||
it('should return undefined if all indices have source fields', () => {
|
||||
const fieldDescriptors: IndicesQuerySourceFields = {
|
||||
empty_index: {
|
||||
elser_query_fields: [],
|
||||
dense_vector_query_fields: [],
|
||||
bm25_query_fields: [],
|
||||
source_fields: [],
|
||||
skipped_fields: 0,
|
||||
semantic_fields: [],
|
||||
},
|
||||
non_empty_index: {
|
||||
elser_query_fields: [],
|
||||
dense_vector_query_fields: [],
|
||||
bm25_query_fields: ['field2'],
|
||||
source_fields: ['field1'],
|
||||
skipped_fields: 0,
|
||||
semantic_fields: [],
|
||||
},
|
||||
};
|
||||
expect(getIndicesWithNoSourceFields(fieldDescriptors)).toBe('empty_index');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RetrieverContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { IndicesQuerySourceFields, QuerySourceFields } from '../types';
|
||||
|
||||
export type IndexFields = Record<string, string[]>;
|
||||
|
@ -51,7 +52,7 @@ export function createQuery(
|
|||
rerankOptions: ReRankOptions = {
|
||||
rrf: true,
|
||||
}
|
||||
) {
|
||||
): { retriever: RetrieverContainer } {
|
||||
const indices = Object.keys(fieldDescriptors);
|
||||
const boolMatches = Object.keys(fields).reduce<Matches>(
|
||||
(acc, index) => {
|
||||
|
|
|
@ -62,10 +62,10 @@ describe('fetch_query_source_fields', () => {
|
|||
).toEqual({
|
||||
workplace_index: {
|
||||
bm25_query_fields: [
|
||||
'metadata.summary',
|
||||
'metadata.rolePermissions',
|
||||
'text',
|
||||
'metadata.name',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.summary',
|
||||
'text',
|
||||
],
|
||||
dense_vector_query_fields: [],
|
||||
elser_query_fields: [
|
||||
|
@ -77,16 +77,16 @@ describe('fetch_query_source_fields', () => {
|
|||
},
|
||||
],
|
||||
skipped_fields: 8,
|
||||
source_fields: ['metadata.summary', 'metadata.rolePermissions', 'text', 'metadata.name'],
|
||||
source_fields: ['metadata.name', 'metadata.rolePermissions', 'metadata.summary', 'text'],
|
||||
semantic_fields: [],
|
||||
},
|
||||
workplace_index2: {
|
||||
semantic_fields: [],
|
||||
bm25_query_fields: [
|
||||
'metadata.summary',
|
||||
'content',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.name',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.summary',
|
||||
],
|
||||
dense_vector_query_fields: [],
|
||||
skipped_fields: 8,
|
||||
|
@ -99,10 +99,10 @@ describe('fetch_query_source_fields', () => {
|
|||
},
|
||||
],
|
||||
source_fields: [
|
||||
'metadata.summary',
|
||||
'content',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.name',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.summary',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
@ -125,19 +125,19 @@ describe('fetch_query_source_fields', () => {
|
|||
'search-example-main': {
|
||||
semantic_fields: [],
|
||||
bm25_query_fields: [
|
||||
'page_content_key',
|
||||
'title',
|
||||
'main_button.button_title',
|
||||
'page_notification',
|
||||
'bread_crumbs',
|
||||
'url',
|
||||
'page_content_text',
|
||||
'buttons.button_title',
|
||||
'filter_list',
|
||||
'buttons.button_link',
|
||||
'buttons.button_new_tab',
|
||||
'title_text',
|
||||
'buttons.button_title',
|
||||
'filter_list',
|
||||
'main_button.button_link',
|
||||
'main_button.button_title',
|
||||
'page_content_key',
|
||||
'page_content_text',
|
||||
'page_notification',
|
||||
'title',
|
||||
'title_text',
|
||||
'url',
|
||||
],
|
||||
dense_vector_query_fields: [
|
||||
{
|
||||
|
@ -149,19 +149,19 @@ describe('fetch_query_source_fields', () => {
|
|||
elser_query_fields: [],
|
||||
skipped_fields: 30,
|
||||
source_fields: [
|
||||
'page_content_key',
|
||||
'title',
|
||||
'main_button.button_title',
|
||||
'page_notification',
|
||||
'bread_crumbs',
|
||||
'url',
|
||||
'page_content_text',
|
||||
'buttons.button_title',
|
||||
'filter_list',
|
||||
'buttons.button_link',
|
||||
'buttons.button_new_tab',
|
||||
'title_text',
|
||||
'buttons.button_title',
|
||||
'filter_list',
|
||||
'main_button.button_link',
|
||||
'main_button.button_title',
|
||||
'page_content_key',
|
||||
'page_content_text',
|
||||
'page_notification',
|
||||
'title',
|
||||
'title_text',
|
||||
'url',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
@ -216,27 +216,27 @@ describe('fetch_query_source_fields', () => {
|
|||
).toEqual({
|
||||
workplace_index_nested: {
|
||||
bm25_query_fields: [
|
||||
'metadata.category',
|
||||
'content',
|
||||
'metadata.url',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.name',
|
||||
'passages.text',
|
||||
'metadata.summary',
|
||||
'metadata.category',
|
||||
'metadata.content',
|
||||
'metadata.name',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.summary',
|
||||
'metadata.url',
|
||||
'passages.text',
|
||||
],
|
||||
dense_vector_query_fields: [],
|
||||
elser_query_fields: [],
|
||||
semantic_fields: [],
|
||||
source_fields: [
|
||||
'metadata.category',
|
||||
'content',
|
||||
'metadata.url',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.name',
|
||||
'passages.text',
|
||||
'metadata.summary',
|
||||
'metadata.category',
|
||||
'metadata.content',
|
||||
'metadata.name',
|
||||
'metadata.rolePermissions',
|
||||
'metadata.summary',
|
||||
'metadata.url',
|
||||
'passages.text',
|
||||
],
|
||||
skipped_fields: 18,
|
||||
},
|
||||
|
|
|
@ -164,11 +164,20 @@ const isFieldInIndex = (
|
|||
);
|
||||
};
|
||||
|
||||
const sortFields = (fields: FieldCapsResponse['fields']): FieldCapsResponse['fields'] => {
|
||||
const entries = Object.entries(fields);
|
||||
|
||||
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
export const parseFieldsCapabilities = (
|
||||
fieldCapsResponse: FieldCapsResponse,
|
||||
aggMappingDocs: Array<{ index: string; doc: SearchResponse; mapping: IndicesGetMappingResponse }>
|
||||
): IndicesQuerySourceFields => {
|
||||
const { fields, indices: indexOrIndices } = fieldCapsResponse;
|
||||
const { indices: indexOrIndices } = fieldCapsResponse;
|
||||
const fields = sortFields(fieldCapsResponse.fields);
|
||||
const indices = Array.isArray(indexOrIndices) ? indexOrIndices : [indexOrIndices];
|
||||
|
||||
const indexModelIdFields = aggMappingDocs.map<IndexFieldModel>((aggDoc) => {
|
||||
|
|
|
@ -170,6 +170,10 @@ export default function (ftrContext: FtrProviderContext) {
|
|||
it('show edit context', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundChatPage.expectEditContextOpens();
|
||||
});
|
||||
|
||||
it('save selected fields between modes', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundChatPage.expectSaveFieldsBetweenModes();
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
|
|
@ -146,6 +146,8 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext)
|
|||
expect(code.replace(/ /g, '')).to.be(
|
||||
'{\n"retriever":{\n"standard":{\n"query":{\n"multi_match":{\n"query":"{query}",\n"fields":[\n"baz"\n]\n}\n}\n}\n}\n}'
|
||||
);
|
||||
|
||||
await testSubjects.click('chatMode');
|
||||
},
|
||||
|
||||
async expectEditContextOpens() {
|
||||
|
@ -157,6 +159,16 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext)
|
|||
|
||||
expect(fields.length).to.be(1);
|
||||
},
|
||||
|
||||
async expectSaveFieldsBetweenModes() {
|
||||
await testSubjects.click('queryMode');
|
||||
await testSubjects.existOrFail('field-baz-true');
|
||||
await testSubjects.click('field-baz-true');
|
||||
await testSubjects.existOrFail('field-baz-false');
|
||||
await testSubjects.click('chatMode');
|
||||
await testSubjects.click('queryMode');
|
||||
await testSubjects.existOrFail('field-baz-false');
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -176,6 +176,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it('show edit context', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundChatPage.expectEditContextOpens();
|
||||
});
|
||||
|
||||
it('save selected fields between modes', async () => {
|
||||
await pageObjects.searchPlayground.PlaygroundChatPage.expectSaveFieldsBetweenModes();
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue