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}`);