Skip to content

Commit

Permalink
[ES3][Connectors]Using EuiComboBox with searchable capability as we d…
Browse files Browse the repository at this point in the history
…o in ESS (elastic#199464)

## Summary

This PR swaps the `EuiSelectable` by the `EuiComboBox` component as we
have in ESS.
### Context:
This PR belongs to this initiative
elastic/search-team#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 <[email protected]>
Co-authored-by: Navarone Feekery <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Rodney Norris <[email protected]>
  • Loading branch information
5 people authored Dec 9, 2024
1 parent 55d5c7a commit 6bf02e4
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 32 deletions.
8 changes: 8 additions & 0 deletions x-pack/plugins/serverless_search/common/i18n_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,6 +38,13 @@ interface EditServiceTypeProps {
connector: Connector;
isDisabled?: boolean;
}
interface ConnectorDataSource {
_icon: React.ReactNode[];
_badges: React.ReactNode;
serviceType: string;
}

type ExpandedComboBoxOption = EuiComboBoxOptionOption<ConnectorDataSource>;

interface GeneratedConnectorNameResult {
connectorName: string;
Expand All @@ -29,30 +56,18 @@ export const EditServiceType: React.FC<EditServiceTypeProps> = ({ connector, isD
const connectorTypes = useConnectorTypes();
const queryClient = useQueryClient();
const { queryKey } = useConnector(connector.id);
const assetBasePath = useAssetBasePath();

const options =
connectorTypes.map((connectorType) => ({
inputDisplay: (
<EuiFlexGroup direction="row" alignItems="center">
<EuiFlexItem
grow={false}
data-test-subj={`serverlessSearchConnectorServiceType-${connectorType.serviceType}`}
>
<EuiIcon
size="l"
title={connectorType.name}
id={connectorType.serviceType}
type={connectorType.iconPath}
/>
</EuiFlexItem>
<EuiFlexItem>{connectorType.name}</EuiFlexItem>
</EuiFlexGroup>
),
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),
Expand Down Expand Up @@ -99,6 +114,104 @@ export const EditServiceType: React.FC<EditServiceTypeProps> = ({ connector, isD
},
});

const getInitialOptions = (): ExpandedComboBoxOption[] => {
return allConnectors.map((conn, key) => {
const _icon: React.ReactNode[] = [];
let _ariaLabelAppend = '';
if (conn.isTechPreview) {
_icon.push(
<EuiBadge
aria-label={TECH_PREVIEW_LABEL}
key={key + '-preview'}
iconType="beaker"
color="hollow"
>
{i18n.translate(
'xpack.serverlessSearch.connectors.chooseConnectorSelectable.thechPreviewBadgeLabel',
{ defaultMessage: 'Tech preview' }
)}
</EuiBadge>
);
_ariaLabelAppend += ` ${TECH_PREVIEW_LABEL}`;
}
if (conn.isBeta) {
_icon.push(
<EuiBadge aria-label={BETA_LABEL} key={key + '-beta'} iconType={'beta'} color="hollow">
{BETA_LABEL}
</EuiBadge>
);
_ariaLabelAppend += ` ${BETA_LABEL}`;
}
return {
key: key.toString(),
label: conn.name,
value: {
_icon,
_badges: <EuiIcon size="l" type={conn.iconPath} />,
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 (
<EuiFlexGroup
className={contentClassName}
key={key + '-span'}
gutterSize="m"
responsive={false}
direction="row"
>
<EuiFlexItem grow={false}>{_badges}</EuiFlexItem>
<EuiFlexItem
css={css`
overflow: auto;
`}
grow
data-test-subj={`serverlessSearchConnectorServiceType-${serviceType}`}
>
<EuiText textAlign="left" size="s">
<EuiTextTruncate text={label} truncation="end" />
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadgeGroup gutterSize="xs">{_icon}</EuiBadgeGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};

const onSelectedOptionChange = useCallback(
(selectedItem: Array<EuiComboBoxOptionOption<ConnectorDataSource>>) => {
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 (
<EuiFormRow
label={i18n.translate('xpack.serverlessSearch.connectors.serviceTypeLabel', {
Expand All @@ -107,15 +220,36 @@ export const EditServiceType: React.FC<EditServiceTypeProps> = ({ connector, isD
data-test-subj="serverlessSearchEditConnectorType"
fullWidth
>
<EuiSuperSelect
// We only want to allow people to set the service type once to avoid weird conflicts
disabled={Boolean(connector.service_type) || isDisabled}
data-test-subj="serverlessSearchEditConnectorTypeChoices"
<EuiComboBox<ConnectorDataSource>
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={
<EuiIcon
type={
connector.service_type
? connectorTypes.find((conn) => 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}
/>
</EuiFormRow>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down

0 comments on commit 6bf02e4

Please sign in to comment.