[WorkChat] Change index selector to a ComboBox for the "Index Source" integration (#216998)

## Closes https://github.com/elastic/search-team/issues/9656

## Summary

This PR adds changes the input that allows user enter the index when
configuring a WorkChat integration with "Index Source".

The video is better than a thousand words:

Before:


https://github.com/user-attachments/assets/0e175c55-fb54-436b-9b87-7831d8d2db2f

After:


https://github.com/user-attachments/assets/54f13122-55c0-4c3f-a25a-d68f080cd30c

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [ ] 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
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] 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:
Artem Shelkovnikov 2025-04-14 18:15:54 +02:00 committed by GitHub
parent a9c9354382
commit 5ee5b35cf9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 150 additions and 8 deletions

View file

@ -10,3 +10,7 @@ import type { IndexSourceDefinition } from '@kbn/wci-common';
export interface GenerateConfigurationResponse {
definition: IndexSourceDefinition;
}
export interface SearchIndicesResponse {
indexNames: string[];
}

View file

@ -0,0 +1,56 @@
/*
* 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 { useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import type { CoreStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { SearchIndicesResponse } from '../../common/http_api/configuration';
export const useIndexNameAutocomplete = ({ query }: { query: string }) => {
const {
services: { http, notifications },
} = useKibana<CoreStart>();
const [debouncedQuery, setDebounceQuery] = useState<string>(query);
useDebounce(
() => {
setDebounceQuery(query);
},
250,
[query]
);
const { isLoading, data } = useQuery({
queryKey: ['index-name-autocomplete', debouncedQuery],
queryFn: async () => {
if (query.length < 3) {
return [];
}
const response = await http.get<SearchIndicesResponse>(
`/internal/wci-index-source/indices-autocomplete`,
{
query: {
index: query,
},
}
);
return response.indexNames;
},
initialData: [],
onError: (err: any) => {
notifications.toasts.addError(err, { title: 'Error fetching indices' });
},
});
return {
isLoading,
data,
};
};

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { useFieldArray, Controller } from 'react-hook-form';
import {
EuiTextArea,
EuiComboBox,
EuiFormRow,
EuiDescribedFormGroup,
EuiFieldText,
@ -21,11 +22,13 @@ import {
EuiFlexItem,
EuiCallOut,
EuiButtonEmpty,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import type { IndexSourceDefinition } from '@kbn/wci-common';
import { IntegrationConfigurationFormProps } from '@kbn/wci-browser';
import type { WCIIndexSourceFilterField, WCIIndexSourceContextField } from '../../common/types';
import { useGenerateSchema } from '../hooks/use_generate_schema';
import { useIndexNameAutocomplete } from '../hooks/use_index_name_autocomplete';
export const IndexSourceConfigurationForm: React.FC<IntegrationConfigurationFormProps> = ({
form,
@ -35,6 +38,7 @@ export const IndexSourceConfigurationForm: React.FC<IntegrationConfigurationForm
control,
name: 'configuration.fields.filterFields',
});
const [query, setQuery] = useState('');
const contextFieldsArray = useFieldArray({
control,
@ -50,6 +54,16 @@ export const IndexSourceConfigurationForm: React.FC<IntegrationConfigurationForm
];
const { generateSchema } = useGenerateSchema();
const { isLoading, data } = useIndexNameAutocomplete({ query });
const [selectedOptions, setSelected] = useState<EuiComboBoxOptionOption[]>([]);
const onIndexNameChange = (onChangeSelectedOptions: EuiComboBoxOptionOption[]) => {
setSelected(onChangeSelectedOptions);
};
const onSearchChange = (searchValue: string) => {
setQuery(searchValue);
};
const onSchemaGenerated = useCallback(
(definition: IndexSourceDefinition) => {
@ -116,16 +130,28 @@ export const IndexSourceConfigurationForm: React.FC<IntegrationConfigurationForm
name="configuration.index"
control={control}
render={({ field }) => (
<EuiFieldText
<EuiComboBox
data-test-subj="workchatAppIntegrationEditViewIndex"
placeholder="Enter index name"
{...field}
placeholder="Select an index"
isLoading={isLoading}
selectedOptions={selectedOptions}
singleSelection={{ asPlainText: true }}
options={data.map((option) => ({ label: option, key: option }))}
onChange={onIndexNameChange}
fullWidth={true}
onSearchChange={onSearchChange}
append={
<EuiButtonEmpty
size="xs"
iconType="gear"
onClick={() => {
generateSchema({ indexName: field.value }, { onSuccess: onSchemaGenerated });
if (selectedOptions.length === 0) return;
if (!selectedOptions[0].key) return;
generateSchema(
{ indexName: selectedOptions[0].key },
{ onSuccess: onSchemaGenerated }
);
}}
>
Generate configuration
@ -180,7 +206,7 @@ export const IndexSourceConfigurationForm: React.FC<IntegrationConfigurationForm
</EuiFormRow>
) : (
filterFieldsArray.fields.map((filterField, index) => (
<EuiPanel paddingSize="s" key={filterField.id} style={{ marginBottom: '8px' }}>
<EuiPanel paddingSize="s" key={filterField.id} css={{ marginBottom: '8px' }}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFormRow label="Field name">
@ -275,7 +301,7 @@ export const IndexSourceConfigurationForm: React.FC<IntegrationConfigurationForm
</EuiFormRow>
) : (
contextFieldsArray.fields.map((contextField, index) => (
<EuiPanel paddingSize="s" key={contextField.id} style={{ marginBottom: '8px' }}>
<EuiPanel paddingSize="s" key={contextField.id} css={{ marginBottom: '8px' }}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFormRow label="Field name">

View file

@ -9,7 +9,10 @@ import { schema } from '@kbn/config-schema';
import { apiCapabilities } from '@kbn/workchat-app/common/features';
import { buildSchema } from '@kbn/wc-index-schema-builder';
import { getConnectorList, getDefaultConnector } from '@kbn/wc-genai-utils';
import type { GenerateConfigurationResponse } from '../../common/http_api/configuration';
import type {
GenerateConfigurationResponse,
SearchIndicesResponse,
} from '../../common/http_api/configuration';
import type { RouteDependencies } from './types';
export const registerConfigurationRoutes = ({ router, core, logger }: RouteDependencies) => {
@ -61,4 +64,57 @@ export const registerConfigurationRoutes = ({ router, core, logger }: RouteDepen
}
}
);
router.get(
{
path: '/internal/wci-index-source/indices-autocomplete',
security: {
authz: {
requiredPrivileges: [apiCapabilities.manageWorkchat],
},
},
validate: {
query: schema.object({
index: schema.maybe(schema.string()),
}),
},
},
async (ctx, request, res) => {
const { elasticsearch } = await ctx.core;
let pattern = request.query.index || '';
if (pattern.length >= 3) {
pattern = `${pattern}*`;
}
const esClient = elasticsearch.client.asCurrentUser;
try {
const response = await esClient.cat.indices({
index: [pattern],
h: 'index',
expand_wildcards: 'open',
format: 'json',
});
return res.ok<SearchIndicesResponse>({
body: {
indexNames: response
.map((indexRecord) => indexRecord.index)
.filter((index) => !!index) as string[],
},
});
} catch (e) {
// TODO: sigh, is there a better way?
if (e?.meta?.body?.error?.type === 'index_not_found_exception') {
return res.ok<SearchIndicesResponse>({
body: {
indexNames: [],
},
});
}
throw e;
}
}
);
};