diff --git a/x-pack/solutions/chat/plugins/wci-index-source/common/http_api/configuration.ts b/x-pack/solutions/chat/plugins/wci-index-source/common/http_api/configuration.ts index 02e53ff34690b..d987fefcf407c 100644 --- a/x-pack/solutions/chat/plugins/wci-index-source/common/http_api/configuration.ts +++ b/x-pack/solutions/chat/plugins/wci-index-source/common/http_api/configuration.ts @@ -10,3 +10,7 @@ import type { IndexSourceDefinition } from '@kbn/wci-common'; export interface GenerateConfigurationResponse { definition: IndexSourceDefinition; } + +export interface SearchIndicesResponse { + indexNames: string[]; +} diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/hooks/use_index_name_autocomplete.ts b/x-pack/solutions/chat/plugins/wci-index-source/public/hooks/use_index_name_autocomplete.ts new file mode 100644 index 0000000000000..5a979529f3677 --- /dev/null +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/hooks/use_index_name_autocomplete.ts @@ -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(); + + const [debouncedQuery, setDebounceQuery] = useState(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( + `/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, + }; +}; diff --git a/x-pack/solutions/chat/plugins/wci-index-source/public/integration/configuration.tsx b/x-pack/solutions/chat/plugins/wci-index-source/public/integration/configuration.tsx index b2af236828620..85b600df6268f 100644 --- a/x-pack/solutions/chat/plugins/wci-index-source/public/integration/configuration.tsx +++ b/x-pack/solutions/chat/plugins/wci-index-source/public/integration/configuration.tsx @@ -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 = ({ form, @@ -35,6 +38,7 @@ export const IndexSourceConfigurationForm: React.FC([]); + + 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 ( - ({ label: option, key: option }))} + onChange={onIndexNameChange} + fullWidth={true} + onSearchChange={onSearchChange} append={ { - 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 ) : ( filterFieldsArray.fields.map((filterField, index) => ( - + @@ -275,7 +301,7 @@ export const IndexSourceConfigurationForm: React.FC ) : ( contextFieldsArray.fields.map((contextField, index) => ( - + diff --git a/x-pack/solutions/chat/plugins/wci-index-source/server/routes/configuration.ts b/x-pack/solutions/chat/plugins/wci-index-source/server/routes/configuration.ts index 442737b344ce7..ea8ca67b74fe1 100644 --- a/x-pack/solutions/chat/plugins/wci-index-source/server/routes/configuration.ts +++ b/x-pack/solutions/chat/plugins/wci-index-source/server/routes/configuration.ts @@ -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({ + 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({ + body: { + indexNames: [], + }, + }); + } + throw e; + } + } + ); };