From 6bf02e4c1317873281b74e9f3722cb2ee5f2cc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Gonz=C3=A1lez?= Date: Mon, 9 Dec 2024 15:51:10 +0100 Subject: [PATCH] [ES3][Connectors]Using EuiComboBox with searchable capability as we do in ESS (#199464) ## Summary This PR swaps the `EuiSelectable` by the `EuiComboBox` component as we have in ESS. ### Context: This PR belongs to this initiative https://github.com/elastic/search-team/issues/8000 where we agreed on bring low hanging fruit artefacts from ESS to ES3 before being replace completly with the full new experience which will be later. Therefor we are not investing effort on make the code scalable and reusable at this point. ![CleanShot 2024-11-08 at 12 59 47](https://github.com/user-attachments/assets/a73884c2-7ea4-412c-a5de-386003733d0a) --------- Co-authored-by: Elastic Machine Co-authored-by: Navarone Feekery <13634519+navarone-feekery@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Rodney Norris --- .../serverless_search/common/i18n_string.ts | 8 + .../connectors/edit_service_type.tsx | 194 +++++++++++++++--- .../svl_search_connectors_page.ts | 4 +- 3 files changed, 174 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/serverless_search/common/i18n_string.ts b/x-pack/plugins/serverless_search/common/i18n_string.ts index d77998bc8cc53..a6597ca915b60 100644 --- a/x-pack/plugins/serverless_search/common/i18n_string.ts +++ b/x-pack/plugins/serverless_search/common/i18n_string.ts @@ -51,6 +51,14 @@ export const DISABLED_LABEL: string = i18n.translate('xpack.serverlessSearch.dis defaultMessage: 'Disabled', }); +export const BETA_LABEL: string = i18n.translate('xpack.serverlessSearch.beta', { + defaultMessage: 'Beta', +}); + +export const TECH_PREVIEW_LABEL: string = i18n.translate('xpack.serverlessSearch.techPreview', { + defaultMessage: 'Tech preview', +}); + export const INVALID_JSON_ERROR: string = i18n.translate( 'xpack.serverlessSearch.invalidJsonError', { diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx index af7e15fa372ec..ab50acbbfed2f 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx @@ -6,10 +6,30 @@ */ import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { EuiFlexItem, EuiFlexGroup, EuiIcon, EuiFormRow, EuiSuperSelect } from '@elastic/eui'; +import React, { useMemo, useCallback } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiIcon, + EuiFormRow, + EuiComboBox, + EuiBadge, + EuiComboBoxOptionOption, + EuiText, + useEuiTheme, + EuiTextTruncate, + EuiBadgeGroup, +} from '@elastic/eui'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { Connector } from '@kbn/search-connectors'; +import { Connector as BaseConnector } from '@kbn/search-connectors'; +import { css } from '@emotion/react'; +import { useAssetBasePath } from '../../hooks/use_asset_base_path'; + +import { BETA_LABEL, TECH_PREVIEW_LABEL } from '../../../../common/i18n_string'; + +interface Connector extends BaseConnector { + iconPath?: string; +} import { useKibanaServices } from '../../hooks/use_kibana'; import { useConnectorTypes } from '../../hooks/api/use_connector_types'; import { useConnector } from '../../hooks/api/use_connector'; @@ -18,6 +38,13 @@ interface EditServiceTypeProps { connector: Connector; isDisabled?: boolean; } +interface ConnectorDataSource { + _icon: React.ReactNode[]; + _badges: React.ReactNode; + serviceType: string; +} + +type ExpandedComboBoxOption = EuiComboBoxOptionOption; interface GeneratedConnectorNameResult { connectorName: string; @@ -29,30 +56,18 @@ export const EditServiceType: React.FC = ({ connector, isD const connectorTypes = useConnectorTypes(); const queryClient = useQueryClient(); const { queryKey } = useConnector(connector.id); + const assetBasePath = useAssetBasePath(); - const options = - connectorTypes.map((connectorType) => ({ - inputDisplay: ( - - - - - {connectorType.name} - - ), - value: connectorType.serviceType, - })) || []; + const allConnectors = useMemo( + () => connectorTypes.sort((a, b) => a.name.localeCompare(b.name)), + [connectorTypes] + ); const { isLoading, mutate } = useMutation({ mutationFn: async (inputServiceType: string) => { + if (inputServiceType === null || inputServiceType === '') { + return { serviceType: inputServiceType, name: connector.name }; + } const body = { service_type: inputServiceType }; await http.post(`/internal/serverless_search/connectors/${connector.id}/service_type`, { body: JSON.stringify(body), @@ -99,6 +114,104 @@ export const EditServiceType: React.FC = ({ connector, isD }, }); + const getInitialOptions = (): ExpandedComboBoxOption[] => { + return allConnectors.map((conn, key) => { + const _icon: React.ReactNode[] = []; + let _ariaLabelAppend = ''; + if (conn.isTechPreview) { + _icon.push( + + {i18n.translate( + 'xpack.serverlessSearch.connectors.chooseConnectorSelectable.thechPreviewBadgeLabel', + { defaultMessage: 'Tech preview' } + )} + + ); + _ariaLabelAppend += ` ${TECH_PREVIEW_LABEL}`; + } + if (conn.isBeta) { + _icon.push( + + {BETA_LABEL} + + ); + _ariaLabelAppend += ` ${BETA_LABEL}`; + } + return { + key: key.toString(), + label: conn.name, + value: { + _icon, + _badges: , + serviceType: conn.serviceType, + }, + 'aria-label': conn.name + _ariaLabelAppend, + }; + }); + }; + + const initialOptions = getInitialOptions(); + const { euiTheme } = useEuiTheme(); + + const renderOption = ( + option: ExpandedComboBoxOption, + searchValue: string, + contentClassName: string + ) => { + const { + value: { _icon, _badges, serviceType } = { _icon: [], _badges: null, serviceType: '' }, + key, + label, + } = option; + return ( + + {_badges} + + + + + + + {_icon} + + + ); + }; + + const onSelectedOptionChange = useCallback( + (selectedItem: Array>) => { + if (selectedItem.length === 0) { + return; + } + const keySelected = Number(selectedItem[0].key); + mutate(allConnectors[keySelected].serviceType); + }, + [mutate, allConnectors] + ); + const selectedOptions = useMemo(() => { + const selectedOption = initialOptions.find( + (option) => option.value?.serviceType === connector.service_type + ); + return selectedOption ? [selectedOption] : []; + }, [initialOptions, connector.service_type]); + return ( = ({ connector, isD data-test-subj="serverlessSearchEditConnectorType" fullWidth > - + aria-label={i18n.translate( + 'xpack.serverlessSearch.connectors.chooseConnectorSelectable.euiComboBox.accessibleScreenReaderLabelLabel', + { defaultMessage: 'Select a data source for your connector to use.' } + )} + isDisabled={Boolean(connector.service_type) || isDisabled} isLoading={isLoading} - onChange={(event) => mutate(event)} - options={options} - valueOfSelected={connector.service_type || undefined} + data-test-subj="serverlessSearchEditConnectorTypeChoices" + prepend={ + conn.serviceType === connector.service_type) + ?.iconPath ?? '' + : `${assetBasePath}/connectors.svg` + } + size="l" + /> + } + singleSelection={{ asPlainText: true }} fullWidth + placeholder={i18n.translate( + 'xpack.serverlessSearch.connectors.chooseConnectorSelectable.placeholder.text', + { defaultMessage: 'Choose a data source' } + )} + options={initialOptions} + selectedOptions={selectedOptions} + onChange={onSelectedOptionChange} + renderOption={renderOption} + rowHeight={(euiTheme.base / 2) * 5} /> ); diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts index 8279c89f05d03..2e754337c03fe 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts @@ -64,9 +64,9 @@ export function SvlSearchConnectorsPageProvider({ getService }: FtrProviderConte await testSubjects.existOrFail('serverlessSearchEditConnectorType'); await testSubjects.existOrFail('serverlessSearchEditConnectorTypeChoices'); await testSubjects.click('serverlessSearchEditConnectorTypeChoices'); - await testSubjects.exists('serverlessSearchConnectorServiceType-zoom'); + await testSubjects.setValue('serverlessSearchEditConnectorTypeChoices', type); + await testSubjects.exists(`serverlessSearchConnectorServiceType-${type}`); await testSubjects.click(`serverlessSearchConnectorServiceType-${type}`); - await testSubjects.existOrFail('serverlessSearchConnectorServiceType-zoom'); }, async expectConnectorIdToMatchUrl(connectorId: string) { expect(await browser.getCurrentUrl()).contain(`/app/connectors/${connectorId}`);