Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Search][Playground] Support Multiple Context Fields #210703

Merged
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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, useCallback } from 'react';

import { EuiCallOut, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { QuerySourceFields } from '../../types';

export interface ContextFieldsSelectProps {
indexName: string;
indexFields: QuerySourceFields;
selectedContextFields?: string[];
updateSelectedContextFields: (index: string, value: string[]) => void;
}

export const ContextFieldsSelect = ({
indexName,
indexFields,
selectedContextFields,
updateSelectedContextFields,
}: ContextFieldsSelectProps) => {
const { options: selectOptions, selectedOptions } = useMemo(() => {
if (!indexFields.source_fields?.length) return { options: [], selectedOptions: [] };

const options: Array<EuiComboBoxOptionOption<unknown>> = indexFields.source_fields.map(
(field) => ({
label: field,
'data-test-subj': `contextField-${field}`,
})
);
const selected: Array<EuiComboBoxOptionOption<unknown>> =
selectedContextFields
?.map((field) => options.find((opt) => opt.label === field))
?.filter(
(
val: EuiComboBoxOptionOption<unknown> | undefined
): val is EuiComboBoxOptionOption<unknown> => val !== undefined
) ?? [];
return {
options,
selectedOptions: selected,
};
}, [indexFields.source_fields, selectedContextFields]);
const onSelectFields = useCallback(
(updatedSelectedOptions: Array<EuiComboBoxOptionOption<unknown>>) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we avoid defining unknown and any as a type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should avoid any, uknown is perfectly fine.

Using unknown TS will make you do type checks to validate what the type is. But in this case thats the option data type and what we need to access is for the EuiComboBoxOptionOption. So the option type is not needed in this callback currently.

// always require at least 1 selected field
if (updatedSelectedOptions.length === 0) return;
updateSelectedContextFields(
indexName,
updatedSelectedOptions.map((opt) => opt.label)
);
},
[indexName, updateSelectedContextFields]
);

if (selectOptions.length === 0) {
return (
<EuiCallOut
title={i18n.translate('xpack.searchPlayground.editContext.noSourceFieldWarning', {
defaultMessage: 'No source fields found',
})}
color="warning"
iconType="warning"
size="s"
/>
);
}

return (
<EuiComboBox
data-test-subj={`contextFieldsSelectable-${indexName}`}
options={selectOptions}
selectedOptions={selectedOptions}
onChange={onSelectFields}
isClearable={false}
fullWidth
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ jest.mock('../../hooks/use_source_indices_field', () => ({
elser_query_fields: [],
dense_vector_query_fields: [],
bm25_query_fields: ['field1', 'field2'],
source_fields: ['context_field1', 'context_field2'],
source_fields: ['title', 'description'],
semantic_fields: [],
},
index2: {
elser_query_fields: [],
dense_vector_query_fields: [],
bm25_query_fields: ['field1', 'field2'],
source_fields: ['context_field1', 'context_field2'],
bm25_query_fields: ['foo', 'bar'],
source_fields: ['body'],
semantic_fields: [],
},
},
Expand All @@ -47,8 +47,8 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => {
[ChatFormFields.indices]: ['index1'],
[ChatFormFields.docSize]: 1,
[ChatFormFields.sourceFields]: {
index1: ['context_field1'],
index2: ['context_field2'],
index1: ['title'],
index2: ['body'],
},
},
});
Expand All @@ -67,9 +67,15 @@ describe('EditContextFlyout component tests', () => {
});

it('should see the context fields', async () => {
expect(screen.getByTestId('contextFieldsSelectable-0')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('contextFieldsSelectable-0'));
const fields = await screen.findAllByTestId('contextField');
expect(fields.length).toBe(2);
expect(screen.getByTestId('contextFieldsSelectable-index1')).toBeInTheDocument();
const listButton = screen
.getByTestId('contextFieldsSelectable-index1')
.querySelector('[data-test-subj="comboBoxToggleListButton"]');
expect(listButton).not.toBeNull();
fireEvent.click(listButton!);

for (const field of ['title', 'description']) {
expect(screen.getByTestId(`contextField-${field}`)).toBeInTheDocument();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,16 @@
* 2.0.
*/

import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPanel,
EuiSelect,
EuiSuperSelect,
EuiText,
EuiCallOut,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiPanel, EuiSelect, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import React, { useCallback } from 'react';
import { useController } 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 { ContextFieldsSelect } from './context_fields_select';

export const EditContextPanel: React.FC = () => {
const usageTracker = useUsageTracker();
Expand All @@ -40,13 +32,16 @@ export const EditContextPanel: React.FC = () => {
name: ChatFormFields.sourceFields,
});

const updateSourceField = (index: string, field: string) => {
onChangeSourceFields({
...sourceFields,
[index]: [field],
});
usageTracker?.click(AnalyticsEvents.editContextFieldToggled);
};
const updateSourceField = useCallback(
(index: string, contextFields: string[]) => {
onChangeSourceFields({
...sourceFields,
[index]: contextFields,
});
usageTracker?.click(AnalyticsEvents.editContextFieldToggled);
},
[onChangeSourceFields, sourceFields, usageTracker]
);

const handleDocSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
usageTracker?.click(AnalyticsEvents.editContextDocSizeChanged);
Expand All @@ -64,6 +59,7 @@ export const EditContextPanel: React.FC = () => {
fullWidth
>
<EuiSelect
data-test-subj="contextPanelDocumentNumberSelect"
options={[
{
value: 1,
Expand Down Expand Up @@ -100,32 +96,15 @@ export const EditContextPanel: React.FC = () => {
</h5>
</EuiText>
</EuiFlexItem>
{Object.entries(fields).map(([index, group], indexNum) => (
{Object.entries(fields).map(([index, group]) => (
<EuiFlexItem grow={false} key={index}>
<EuiFormRow label={index} fullWidth>
{!!group.source_fields?.length ? (
<EuiSuperSelect
data-test-subj={`contextFieldsSelectable-${indexNum}`}
options={group.source_fields.map((field) => ({
value: field,
inputDisplay: field,
'data-test-subj': 'contextField',
}))}
valueOfSelected={sourceFields[index]?.[0]}
onChange={(value) => updateSourceField(index, value)}
fullWidth
/>
) : (
<EuiCallOut
title={i18n.translate(
'xpack.searchPlayground.editContext.noSourceFieldWarning',
{ defaultMessage: 'No source fields found' }
)}
color="warning"
iconType="warning"
size="s"
/>
)}
<ContextFieldsSelect
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neat!

indexName={index}
indexFields={group}
selectedContextFields={sourceFields[index] ?? []}
updateSelectedContextFields={updateSourceField}
/>
</EuiFormRow>
</EuiFlexItem>
))}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading