mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
a9c9354382
commit
5ee5b35cf9
4 changed files with 150 additions and 8 deletions
|
@ -10,3 +10,7 @@ import type { IndexSourceDefinition } from '@kbn/wci-common';
|
|||
export interface GenerateConfigurationResponse {
|
||||
definition: IndexSourceDefinition;
|
||||
}
|
||||
|
||||
export interface SearchIndicesResponse {
|
||||
indexNames: string[];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue