[Search] Update connector creation logic (#176434)

## Summary

Updates create index flows and attach index flow for connector.


13fb1830-e330-4b34-9ae9-89b892b8a953





### Checklist

Delete any items that are not applicable to this PR.

- [ ] 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/packages/kbn-i18n/README.md)
- [ ] [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Efe Gürkan YALAMAN 2024-02-13 21:00:58 +01:00 committed by GitHub
parent ec8e464251
commit 5b4851b320
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1501 additions and 159 deletions

View file

@ -24,12 +24,21 @@ describe('addConnectorApiLogic', () => {
indexName: 'indexName',
isNative: false,
language: 'en',
name: 'indexName',
});
await nextTick();
expect(http.post).toHaveBeenCalledWith('/internal/enterprise_search/connectors', {
body: JSON.stringify({ index_name: 'indexName', is_native: false, language: 'en' }),
body: JSON.stringify({
index_name: 'indexName',
is_native: false,
language: 'en',
name: 'indexName',
}),
});
await expect(result).resolves.toEqual({
id: 'unique id',
indexName: 'indexName',
});
await expect(result).resolves.toEqual({ id: 'unique id', indexName: 'indexName' });
});
it('adds delete param if specific', async () => {
const promise = Promise.resolve({ id: 'unique id', index_name: 'indexName' });
@ -39,6 +48,7 @@ describe('addConnectorApiLogic', () => {
indexName: 'indexName',
isNative: false,
language: null,
name: 'indexName',
});
await nextTick();
expect(http.post).toHaveBeenCalledWith('/internal/enterprise_search/connectors', {
@ -47,9 +57,13 @@ describe('addConnectorApiLogic', () => {
index_name: 'indexName',
is_native: false,
language: null,
name: 'indexName',
}),
});
await expect(result).resolves.toEqual({ id: 'unique id', indexName: 'indexName' });
await expect(result).resolves.toEqual({
id: 'unique id',
indexName: 'indexName',
});
});
});
});

View file

@ -15,9 +15,10 @@ interface AddConnectorValue {
export interface AddConnectorApiLogicArgs {
deleteExistingConnector?: boolean;
indexName: string;
indexName?: string;
isNative: boolean;
language: string | null;
name: string;
serviceType?: string;
}
@ -29,6 +30,7 @@ export interface AddConnectorApiLogicResponse {
export const addConnector = async ({
deleteExistingConnector,
indexName,
name,
isNative,
language,
serviceType,
@ -43,6 +45,7 @@ export const addConnector = async ({
index_name: indexName,
is_native: isNative,
language,
name,
service_type: serviceType,
};
const result = await HttpLogic.values.http.post<AddConnectorValue>(route, {

View file

@ -0,0 +1,27 @@
/*
* 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 { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
export interface AttachIndexApiLogicArgs {
connectorId: string;
indexName: string;
}
export const attachIndex = async ({
connectorId,
indexName,
}: AttachIndexApiLogicArgs): Promise<void> => {
const route = `/internal/enterprise_search/connectors/${connectorId}/index_name/${indexName}`;
await HttpLogic.values.http.put(route);
};
export const AttachIndexApiLogic = createApiLogic(['add_connector_api_logic'], attachIndex);
export type AttachIndexApiLogicActions = Actions<AttachIndexApiLogicArgs, {}>;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
interface CreateApiIndexValue {
@ -43,3 +43,8 @@ export const CreateApiIndexApiLogic = createApiLogic(
['create_api_index_api_logic'],
createApiIndex
);
export type CreateApiIndexApiLogicActions = Actions<
CreateApiIndexApiLogicArgs,
CreateApiIndexApiLogicResponse
>;

View file

@ -0,0 +1,31 @@
/*
* 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 { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices';
import { Meta } from '../../../../../common/types/pagination';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
export interface FetchAllIndicesResponse {
indices: ElasticsearchIndexWithIngestion[];
meta: Meta;
}
export const fetchAllIndices = async (): Promise<FetchAllIndicesResponse> => {
const { http } = HttpLogic.values;
const route = '/internal/enterprise_search/indices';
const response = await http.get<FetchAllIndicesResponse>(route);
return response;
};
export const FetchAllIndicesAPILogic = createApiLogic(
['content', 'fetch_all_indices_api_logic'],
fetchAllIndices
);
export type FetchAllIndicesApiActions = Actions<{}, FetchAllIndicesResponse>;

View file

@ -0,0 +1,143 @@
/*
* 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 React, { useEffect, useState } from 'react';
import { useActions, useValues } from 'kea';
import {
EuiButton,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiLink,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { Connector } from '@kbn/search-connectors';
import { Status } from '../../../../../common/types/api';
import { FetchAllIndicesAPILogic } from '../../api/index/fetch_all_indices_api_logic';
import { AttachIndexLogic } from './attach_index_logic';
export interface AttachIndexBoxProps {
connector: Connector;
}
export const AttachIndexBox: React.FC<AttachIndexBoxProps> = ({ connector }) => {
const { createIndex, attachIndex, setConnector } = useActions(AttachIndexLogic);
const { isLoading: isSaveLoading } = useValues(AttachIndexLogic);
const [selectedIndex, setSelectedIndex] = useState<{ label: string; shouldCreate?: boolean }>();
const [selectedLanguage] = useState<string>();
const { makeRequest } = useActions(FetchAllIndicesAPILogic);
const { data, status } = useValues(FetchAllIndicesAPILogic);
const isLoading = [Status.IDLE, Status.LOADING].includes(status);
const onSave = () => {
if (selectedIndex?.shouldCreate) {
createIndex({ indexName: selectedIndex.label, language: selectedLanguage ?? null });
} else if (selectedIndex) {
attachIndex({ connectorId: connector.id, indexName: selectedIndex.label });
}
};
const options: Array<EuiComboBoxOptionOption<{ label: string }>> = isLoading
? []
: data?.indices.map((index) => {
return {
label: index.name,
};
}) ?? [];
useEffect(() => {
setConnector(connector);
makeRequest({});
}, [connector.id]);
return (
<EuiPanel hasShadow={false} hasBorder>
<EuiTitle size="s">
<h4>
{i18n.translate('xpack.enterpriseSearch.attachIndexBox.h4.attachAnIndexLabel', {
defaultMessage: 'Attach an index',
})}
</h4>
</EuiTitle>
<EuiSpacer />
<EuiText size="s">
<FormattedMessage
id="xpack.enterpriseSearch.attachIndexBox.thisIndexWillHoldTextLabel"
defaultMessage="This index will hold your data source content, and is optimized with default field mappings
for relevant search experiences. Give your index a unique name and optionally set a default
language analyzer for the index."
/>
</EuiText>
<EuiSpacer />
<EuiLink>
{i18n.translate('xpack.enterpriseSearch.attachIndexBox.learnMoreAboutIndicesLinkLabel', {
defaultMessage: 'Learn more about indices',
})}
</EuiLink>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate(
'xpack.enterpriseSearch.attachIndexBox.euiFormRow.associatedIndexLabel',
{ defaultMessage: 'Associated index' }
)}
helpText={i18n.translate(
'xpack.enterpriseSearch.attachIndexBox.euiFormRow.associatedIndexHelpTextLabel',
{ defaultMessage: 'You can use an existing index or create a new one' }
)}
>
<EuiComboBox
placeholder={i18n.translate(
'xpack.enterpriseSearch.attachIndexBox.euiFormRow.indexSelector.placeholder',
{ defaultMessage: 'Select or create an index' }
)}
customOptionText={i18n.translate(
'xpack.enterpriseSearch.attachIndexBox.euiFormRow.indexSelector.customOption',
{
defaultMessage: 'Create {searchValue} new index',
values: { searchValue: '{searchValue}' },
}
)}
isLoading={isLoading}
options={options}
onChange={(selection) => setSelectedIndex(selection[0] || undefined)}
selectedOptions={selectedIndex ? [selectedIndex] : undefined}
onCreateOption={(value) => {
setSelectedIndex({ label: value.trim(), shouldCreate: true });
}}
singleSelection={{ asPlainText: true }}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => onSave()} disabled={!selectedIndex} isLoading={isSaveLoading}>
{i18n.translate('xpack.enterpriseSearch.attachIndexBox.saveConfigurationButtonLabel', {
defaultMessage: 'Save Configuration',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -0,0 +1,94 @@
/*
* 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 { kea, MakeLogicType } from 'kea';
import { Connector } from '@kbn/search-connectors';
import { Status } from '../../../../../common/types/api';
import {
AttachIndexApiLogic,
AttachIndexApiLogicActions,
} from '../../api/connector/attach_index_api_logic';
import {
CreateApiIndexApiLogic,
CreateApiIndexApiLogicActions,
} from '../../api/index/create_api_index_api_logic';
export interface AttachIndexActions {
attachIndex: AttachIndexApiLogicActions['makeRequest'];
attachIndexApiError: AttachIndexApiLogicActions['apiError'];
attachIndexApiSuccess: AttachIndexApiLogicActions['apiSuccess'];
createIndex: CreateApiIndexApiLogicActions['makeRequest'];
createIndexApiError: CreateApiIndexApiLogicActions['apiError'];
createIndexApiSuccess: CreateApiIndexApiLogicActions['apiSuccess'];
setConnector(connector: Connector): Connector;
}
export interface AttachIndexValues {
attachApiStatus: Status;
connector: Connector | null;
createIndexApiStatus: Status;
isLoading: boolean;
}
export const AttachIndexLogic = kea<MakeLogicType<AttachIndexValues, AttachIndexActions>>({
actions: { setConnector: (connector) => connector },
connect: {
actions: [
AttachIndexApiLogic,
[
'makeRequest as attachIndex',
'apiSuccess as attachIndexApiSuccess',
'apiError as attachIndexApiError',
],
CreateApiIndexApiLogic,
[
'makeRequest as createIndex',
'apiSuccess as createIndexApiSuccess',
'apiError as createIndexApiError',
],
],
values: [
AttachIndexApiLogic,
['status as attachApiStatus'],
CreateApiIndexApiLogic,
['status as createIndexApiStatus'],
],
},
listeners: ({ actions, values }) => ({
attachIndexApiSuccess: () => {
if (values.connector) {
// TODO this is hacky
location.reload();
}
},
createIndexApiSuccess: async ({ indexName }, breakpoint) => {
if (values.connector) {
await breakpoint(500);
actions.attachIndex({ connectorId: values.connector?.id, indexName });
}
},
}),
path: ['enterprise_search', 'content', 'attach_index_logic'],
reducers: {
connector: [
null,
{
setConnector: (_, connector) => connector,
},
],
},
selectors: ({ selectors }) => ({
isLoading: [
() => [selectors.attachApiStatus, selectors.createIndexApiStatus],
(attachStatus, createStatus) =>
attachStatus === Status.LOADING || createStatus === Status.LOADING,
],
}),
});

View file

@ -0,0 +1,428 @@
/*
* 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 React from 'react';
import { useActions, useValues } from 'kea';
import {
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiPanel,
EuiSpacer,
EuiSteps,
EuiCodeBlock,
EuiCallOut,
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ConnectorConfigurationComponent, ConnectorStatus } from '@kbn/search-connectors';
import { Status } from '../../../../../common/types/api';
import { BetaConnectorCallout } from '../../../shared/beta/beta_connector_callout';
import { useCloudDetails } from '../../../shared/cloud_details/cloud_details';
import { docLinks } from '../../../shared/doc_links';
import { generateEncodedPath } from '../../../shared/encode_path_params';
import { HttpLogic } from '../../../shared/http';
import { LicensingLogic } from '../../../shared/licensing';
import { EuiButtonTo, EuiLinkTo } from '../../../shared/react_router_helpers';
import { GenerateConnectorApiKeyApiLogic } from '../../api/connector/generate_connector_api_key_api_logic';
import { ConnectorConfigurationApiLogic } from '../../api/connector/update_connector_configuration_api_logic';
import { CONNECTOR_DETAIL_TAB_PATH } from '../../routes';
import { SyncsContextMenu } from '../search_index/components/header_actions/syncs_context_menu';
import { ApiKeyConfig } from '../search_index/connector/api_key_configuration';
import {
BETA_CONNECTORS,
CONNECTORS,
getConnectorTemplate,
} from '../search_index/connector/constants';
import { IndexNameLogic } from '../search_index/index_name_logic';
import { IndexViewLogic } from '../search_index/index_view_logic';
import { AttachIndexBox } from './attach_index_box';
import { ConnectorDetailTabId } from './connector_detail';
import { ConnectorViewLogic } from './connector_view_logic';
import { NativeConnectorConfiguration } from './native_connector_configuration';
export const ConnectorConfiguration: React.FC = () => {
const { data: apiKeyData } = useValues(GenerateConnectorApiKeyApiLogic);
const { index, recheckIndexLoading, connector } = useValues(ConnectorViewLogic);
const { indexName } = useValues(IndexNameLogic);
const { recheckIndex } = useActions(IndexViewLogic);
const cloudContext = useCloudDetails();
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { status } = useValues(ConnectorConfigurationApiLogic);
const { makeRequest } = useActions(ConnectorConfigurationApiLogic);
const { http } = useValues(HttpLogic);
if (!connector) {
return <></>;
}
// TODO make it work without index if possible
if (connector.is_native && connector.service_type) {
return <NativeConnectorConfiguration />;
}
const hasApiKey = !!(connector.api_key_id ?? apiKeyData);
const docsUrl = CONNECTORS.find(
({ serviceType }) => serviceType === connector.service_type
)?.docsUrl;
const isBeta =
!connector.service_type ||
Boolean(BETA_CONNECTORS.find(({ serviceType }) => serviceType === connector.service_type));
return (
<>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={2}>
<EuiPanel hasShadow={false} hasBorder>
{!connector.index_name && (
<>
<AttachIndexBox connector={connector} />
<EuiSpacer />
</>
)}
<EuiSteps
steps={[
{
children: (
<ApiKeyConfig
indexName={indexName}
hasApiKey={!!connector.api_key_id}
isNative={false}
secretId={null}
/>
),
status: hasApiKey ? 'complete' : 'incomplete',
title: i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.generateApiKey.title',
{
defaultMessage: 'Generate an API key',
}
),
titleSize: 'xs',
},
{
children: (
<>
<EuiText size="s">
<FormattedMessage
id="xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.description.secondParagraph"
defaultMessage="The connectors repository contains several {link}. Use our framework to accelerate developing connectors for custom data sources."
values={{
link: (
<EuiLink
href="https://github.com/elastic/connectors-python/tree/main/connectors"
target="_blank"
external
>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.clientExamplesLink',
{ defaultMessage: 'connector client examples' }
)}
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer />
<EuiText size="s">
<FormattedMessage
id="xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.description.thirdParagraph"
defaultMessage="In this step, you will need to clone or fork the repository, and copy the generated API key and connector ID to the associated {link}. The connector ID will identify this connector to Search. The service type will determine which type of data source the connector is configured for."
values={{
link: (
<EuiLink
href="https://github.com/elastic/connectors-python/blob/main/config.yml"
target="_blank"
external
>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.configurationFileLink',
{ defaultMessage: 'configuration file' }
)}
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer />
<EuiCodeBlock fontSize="m" paddingSize="m" color="dark" isCopyable>
{getConnectorTemplate({
apiKeyData,
connectorData: {
id: connector.id,
service_type: connector.service_type,
},
host: cloudContext.elasticsearchUrl,
})}
</EuiCodeBlock>
<EuiSpacer />
<EuiText size="s">
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.connectorDeployedText',
{
defaultMessage:
'Once configured, deploy the connector on your infrastructure.',
}
)}
</EuiText>
</>
),
status:
!connector.status || connector.status === ConnectorStatus.CREATED
? 'incomplete'
: 'complete',
title: i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.deployConnector.title',
{
defaultMessage: 'Deploy connector',
}
),
titleSize: 'xs',
},
{
children: (
<ConnectorConfigurationComponent
connector={connector}
hasPlatinumLicense={hasPlatinumLicense}
isLoading={status === Status.LOADING}
saveConfig={(
configuration // TODO update endpoints
) =>
index &&
makeRequest({
configuration,
connectorId: connector.id,
indexName: index.name,
})
}
subscriptionLink={docLinks.licenseManagement}
stackManagementLink={http.basePath.prepend(
'/app/management/stack/license_management'
)}
>
{!connector.status || connector.status === ConnectorStatus.CREATED ? (
<EuiCallOut
title={i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.waitingForConnectorTitle',
{
defaultMessage: 'Waiting for your connector',
}
)}
iconType="iInCircle"
>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.waitingForConnectorText',
{
defaultMessage:
'Your connector has not connected to Search. Troubleshoot your configuration and refresh the page.',
}
)}
<EuiSpacer size="s" />
<EuiButton
data-telemetry-id="entSearchContent-connector-configuration-recheckNow"
iconType="refresh"
onClick={() => recheckIndex()}
isLoading={recheckIndexLoading}
>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.waitingForConnector.button.label',
{
defaultMessage: 'Recheck now',
}
)}
</EuiButton>
</EuiCallOut>
) : (
<EuiCallOut
iconType="check"
color="success"
title={i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.connectorPackage.connectorConnected',
{
defaultMessage:
'Your connector {name} has connected to Search successfully.',
values: { name: connector.name },
}
)}
/>
)}
</ConnectorConfigurationComponent>
),
status:
connector.status === ConnectorStatus.CONNECTED ? 'complete' : 'incomplete',
title: i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.enhance.title',
{
defaultMessage: 'Enhance your connector client',
}
),
titleSize: 'xs',
},
{
children: (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiText size="s">
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.scheduleSync.description',
{
defaultMessage:
'Finalize your connector by triggering a one-time sync, or setting a recurring sync to keep your data source in sync over time',
}
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonTo
data-test-subj="entSearchContent-connector-configuration-setScheduleAndSync"
data-telemetry-id="entSearchContent-connector-configuration-setScheduleAndSync"
to={`${generateEncodedPath(CONNECTOR_DETAIL_TAB_PATH, {
connectorId: connector.id,
tabId: ConnectorDetailTabId.SCHEDULING,
})}`}
>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.schedule.button.label',
{
defaultMessage: 'Set schedule and sync',
}
)}
</EuiButtonTo>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SyncsContextMenu />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
),
status: connector.scheduling.full.enabled ? 'complete' : 'incomplete',
title: i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.schedule.title',
{
defaultMessage: 'Sync your data',
}
),
titleSize: 'xs',
},
]}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiPanel hasBorder hasShadow={false}>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiText>
<h4>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.support.title',
{
defaultMessage: 'Support and documentation',
}
)}
</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s">
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.support.description',
{
defaultMessage:
'You need to deploy this connector on your own infrastructure.',
}
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiLink href={docLinks.connectors} target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.support.viewDocumentation.label',
{
defaultMessage: 'View documentation',
}
)}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem>
<EuiLinkTo to={'/app/management/security/api_keys'} shouldNotCreateHref>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.support.manageKeys.label',
{
defaultMessage: 'Manage API keys',
}
)}
</EuiLinkTo>
</EuiFlexItem>
<EuiFlexItem>
<EuiLink
href="https://github.com/elastic/connectors-python/blob/main/README.md"
target="_blank"
>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.support.readme.label',
{
defaultMessage: 'Connector readme',
}
)}
</EuiLink>
</EuiFlexItem>
{docsUrl && (
<EuiFlexItem>
<EuiLink href={docsUrl} target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.support.dockerDeploy.label',
{
defaultMessage: 'Deploy with Docker',
}
)}
</EuiLink>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiLink
href="https://github.com/elastic/connectors-python/blob/main/docs/CONFIG.md#run-the-connector-service-for-a-custom-connector"
target="_blank"
>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.support.deploy.label',
{
defaultMessage: 'Deploy without Docker',
}
)}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
{isBeta ? (
<EuiFlexItem>
<BetaConnectorCallout />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -19,13 +19,13 @@ import { baseBreadcrumbs } from '../connectors/connectors';
import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
import { getHeaderActions } from '../search_index/components/header_actions/header_actions';
import { ConnectorConfiguration } from '../search_index/connector/connector_configuration';
import { ConnectorSchedulingComponent } from '../search_index/connector/connector_scheduling';
import { ConnectorSyncRules } from '../search_index/connector/sync_rules/connector_rules';
import { SearchIndexDocuments } from '../search_index/documents';
import { SearchIndexIndexMappings } from '../search_index/index_mappings';
import { SearchIndexPipelines } from '../search_index/pipelines/pipelines';
import { ConnectorConfiguration } from './connector_configuration';
import { ConnectorViewLogic } from './connector_view_logic';
import { ConnectorDetailOverview } from './overview';

View file

@ -30,7 +30,6 @@ export interface ConnectorViewActions {
setIndexName: IndexNameActions['setIndexName'];
}
// TODO UPDATE
export interface ConnectorViewValues {
connector: Connector | undefined;
connectorData: typeof FetchConnectorByIdApiLogic.values.data;
@ -112,27 +111,21 @@ export const ConnectorViewLogic = kea<MakeLogicType<ConnectorViewValues, Connect
return connectorData?.connector;
},
],
connectorError: [
() => [selectors.connector],
(connector: Connector | undefined) => connector?.error,
],
connectorId: [() => [selectors.connector], (connector) => connector?.id],
error: [
() => [selectors.connector],
(connector: Connector | undefined) => connector?.error || connector?.last_sync_error || null,
],
indexName: [
() => [selectors.connector],
(connector: Connector | undefined) => {
return connector?.index_name || undefined;
},
],
isLoading: [
() => [selectors.fetchConnectorApiStatus, selectors.fetchIndexApiStatus],
(fetchConnectorApiStatus: Status, fetchIndexApiStatus: Status) =>
[Status.IDLE && Status.LOADING].includes(fetchConnectorApiStatus) ||
[Status.IDLE && Status.LOADING].includes(fetchIndexApiStatus),
],
connectorId: [() => [selectors.connector], (connector) => connector?.id],
connectorError: [
() => [selectors.connector],
(connector: Connector | undefined) => connector?.error,
],
error: [
() => [selectors.connector],
(connector: Connector | undefined) => connector?.error || connector?.last_sync_error || null,
],
hasAdvancedFilteringFeature: [
() => [selectors.connector],
(connector?: Connector) =>
@ -168,6 +161,16 @@ export const ConnectorViewLogic = kea<MakeLogicType<ConnectorViewValues, Connect
(connector: Connector | undefined) =>
connector?.configuration.extract_full_html?.value ?? undefined,
],
isLoading: [
() => [selectors.fetchConnectorApiStatus, selectors.fetchIndexApiStatus, selectors.index],
(
fetchConnectorApiStatus: Status,
fetchIndexApiStatus: Status,
index: ConnectorViewValues['index']
) =>
[Status.IDLE && Status.LOADING].includes(fetchConnectorApiStatus) ||
(index && [Status.IDLE && Status.LOADING].includes(fetchIndexApiStatus)),
],
pipelineData: [
() => [selectors.connector],
(connector: Connector | undefined) => connector?.pipeline ?? undefined,

View file

@ -0,0 +1,308 @@
/*
* 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 React from 'react';
import { useValues } from 'kea';
import {
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiPanel,
EuiSpacer,
EuiSteps,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { BetaConnectorCallout } from '../../../shared/beta/beta_connector_callout';
import { docLinks } from '../../../shared/doc_links';
import { generateEncodedPath } from '../../../shared/encode_path_params';
import { HttpLogic } from '../../../shared/http';
import { CONNECTOR_ICONS } from '../../../shared/icons/connector_icons';
import { KibanaLogic } from '../../../shared/kibana';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { CONNECTOR_DETAIL_TAB_PATH } from '../../routes';
import { hasConfiguredConfiguration } from '../../utils/has_configured_configuration';
import { SyncsContextMenu } from '../search_index/components/header_actions/syncs_context_menu';
import { BETA_CONNECTORS, NATIVE_CONNECTORS } from '../search_index/connector/constants';
import { ConvertConnector } from '../search_index/connector/native_connector_configuration/convert_connector';
import { NativeConnectorConfigurationConfig } from '../search_index/connector/native_connector_configuration/native_connector_configuration_config';
import { ResearchConfiguration } from '../search_index/connector/native_connector_configuration/research_configuration';
import { AttachIndexBox } from './attach_index_box';
import { ConnectorDetailTabId } from './connector_detail';
import { ConnectorViewLogic } from './connector_view_logic';
export const NativeConnectorConfiguration: React.FC = () => {
const { connector } = useValues(ConnectorViewLogic);
const { config } = useValues(KibanaLogic);
const { errorConnectingMessage } = useValues(HttpLogic);
if (!connector) {
return <></>;
}
const nativeConnector = NATIVE_CONNECTORS.find(
(connectorDefinition) => connectorDefinition.serviceType === connector.service_type
) || {
docsUrl: '',
externalAuthDocsUrl: '',
externalDocsUrl: '',
icon: CONNECTOR_ICONS.custom,
iconPath: 'custom.svg',
isBeta: true,
isNative: true,
keywords: [],
name: connector.name,
serviceType: connector.service_type ?? '',
};
const hasDescription = !!connector.description;
const hasConfigured = hasConfiguredConfiguration(connector.configuration);
const hasConfiguredAdvanced =
connector.last_synced ||
connector.scheduling.full.enabled ||
connector.scheduling.incremental.enabled;
const hasResearched = hasDescription || hasConfigured || hasConfiguredAdvanced;
const icon = nativeConnector.icon;
// TODO service_type === "" is considered unknown/custom connector multipleplaces replace all of them with a better solution
const isBeta =
!connector.service_type ||
Boolean(BETA_CONNECTORS.find(({ serviceType }) => serviceType === connector.service_type));
return (
<>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={2}>
<EuiPanel hasShadow={false} hasBorder>
<EuiFlexGroup gutterSize="m" direction="row" alignItems="center">
{icon && (
<EuiFlexItem grow={false}>
<EuiIcon size="xl" type={icon} />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>{nativeConnector?.name ?? connector.name}</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{config.host && config.canDeployEntSearch && errorConnectingMessage && (
<>
<EuiCallOut
color="warning"
size="m"
title={i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.entSearchWarning.title',
{
defaultMessage: 'No running Enterprise Search instance detected',
}
)}
iconType="warning"
>
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.entSearchWarning.text',
{
defaultMessage:
'Native connectors require a running Enterprise Search instance to sync content from source.',
}
)}
</p>
</EuiCallOut>
<EuiSpacer />
</>
)}
{!connector.index_name && (
<>
<AttachIndexBox connector={connector} />
<EuiSpacer />
</>
)}
<EuiSteps
steps={[
{
children: <ResearchConfiguration nativeConnector={nativeConnector} />,
status: hasResearched ? 'complete' : 'incomplete',
title: i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.steps.researchConfigurationTitle',
{
defaultMessage: 'Research configuration requirements',
}
),
titleSize: 'xs',
},
{
children: (
<NativeConnectorConfigurationConfig
connector={connector}
nativeConnector={nativeConnector}
status={connector.status}
/>
),
status: hasConfigured ? 'complete' : 'incomplete',
title: i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.steps.configurationTitle',
{
defaultMessage: 'Configuration',
}
),
titleSize: 'xs',
},
{
children: (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiText size="s">
<FormattedMessage
id="xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnectorAdvancedConfiguration.description"
defaultMessage="Finalize your connector by triggering a one time sync, or setting a recurring sync schedule."
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonTo
to={`${generateEncodedPath(CONNECTOR_DETAIL_TAB_PATH, {
connectorId: connector.id,
tabId: ConnectorDetailTabId.SCHEDULING,
})}`}
>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnectorAdvancedConfiguration.schedulingButtonLabel',
{
defaultMessage: 'Set schedule and sync',
}
)}
</EuiButtonTo>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SyncsContextMenu />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
),
status: hasConfiguredAdvanced ? 'complete' : 'incomplete',
title: i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.steps.advancedConfigurationTitle',
{
defaultMessage: 'Sync your data',
}
),
titleSize: 'xs',
},
]}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiPanel hasBorder hasShadow={false}>
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type="clock" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.schedulingReminder.title',
{
defaultMessage: 'Configurable sync schedule',
}
)}
</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText size="s">
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.schedulingReminder.description',
{
defaultMessage:
'Remember to set a sync schedule in the Scheduling tab to continually refresh your searchable data.',
}
)}
</EuiText>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel hasBorder hasShadow={false}>
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type="globe" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.securityReminder.title',
{
defaultMessage: 'Document level security',
}
)}
</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText size="s">
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.securityReminder.description',
{
defaultMessage:
'Restrict and personalize the read access users have to the index documents at query time.',
}
)}
<EuiSpacer size="s" />
<EuiLink href={docLinks.documentLevelSecurity} target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.securityReminder.securityLinkLabel',
{
defaultMessage: 'Document level security',
}
)}
</EuiLink>
</EuiText>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel hasBorder hasShadow={false}>
<ConvertConnector />
</EuiPanel>
</EuiFlexItem>
{isBeta ? (
<EuiFlexItem grow={false}>
<EuiPanel hasBorder hasShadow={false}>
<BetaConnectorCallout />
</EuiPanel>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -15,6 +15,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE } from '../../../../../common/constants';
import { docLinks } from '../../../shared/doc_links';
import { KibanaLogic } from '../../../shared/kibana';
import { isConnectorIndex } from '../../utils/indices';
@ -100,10 +102,13 @@ export const ConnectorDetailOverview: React.FC = () => {
<EuiSpacer />
</>
)}
{isConnectorIndex(indexData) && connector && (
<ConnectorStats connector={connector} indexData={indexData} />
{connector && connector.service_type !== ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE && (
<ConnectorStats
connector={connector}
indexData={isConnectorIndex(indexData) ? indexData : undefined}
/>
)}
{isConnectorIndex(indexData) && (
{connector && connector.service_type !== ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE && (
<>
<EuiSpacer />
<SyncJobs />

View file

@ -54,12 +54,12 @@ describe('AddConnectorLogic', () => {
describe('apiSuccess', () => {
it('navigates to correct spot and flashes success toast', async () => {
jest.useFakeTimers({ legacyFakeTimers: true });
AddConnectorApiLogic.actions.apiSuccess({ indexName: 'success' } as any);
AddConnectorApiLogic.actions.apiSuccess({ id: 'success123' } as any);
await nextTick();
jest.advanceTimersByTime(1001);
await nextTick();
expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(
'/search_indices/success/configuration'
'/connectors/success123/configuration'
);
});
});

View file

@ -18,12 +18,12 @@ import {
AddConnectorApiLogicArgs,
AddConnectorApiLogicResponse,
} from '../../../api/connector/add_connector_api_logic';
import { SEARCH_INDEX_TAB_PATH } from '../../../routes';
import { CONNECTOR_DETAIL_TAB_PATH } from '../../../routes';
import { SearchIndexTabId } from '../../search_index/search_index';
type AddConnectorActions = Pick<
Actions<AddConnectorApiLogicArgs, AddConnectorApiLogicResponse>,
'apiError' | 'apiSuccess' | 'makeRequest'
'apiError' | 'apiSuccess' | 'makeRequest' | 'apiReset'
> & {
setIsModalVisible: (isModalVisible: boolean) => { isModalVisible: boolean };
};
@ -37,15 +37,13 @@ export const AddConnectorLogic = kea<MakeLogicType<AddConnectorValues, AddConnec
setIsModalVisible: (isModalVisible: boolean) => ({ isModalVisible }),
},
connect: {
actions: [AddConnectorApiLogic, ['apiError', 'apiSuccess']],
actions: [AddConnectorApiLogic, ['apiError', 'apiSuccess', 'makeRequest', 'apiReset']],
},
listeners: {
apiSuccess: async ({ indexName }, breakpoint) => {
// Give Elasticsearch the chance to propagate the index so we don't end up in an error state after navigating
await breakpoint(1000);
apiSuccess: async ({ id }) => {
KibanaLogic.values.navigateToUrl(
generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
generateEncodedPath(CONNECTOR_DETAIL_TAB_PATH, {
connectorId: id,
tabId: SearchIndexTabId.CONFIGURATION,
})
);

View file

@ -1,33 +0,0 @@
/*
* 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 { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
import React from 'react';
import { shallow } from 'enzyme';
import { Status } from '../../../../../../common/types/api';
import { NewSearchIndexTemplate } from '../new_search_index_template';
import { MethodConnector } from './method_connector';
describe('MethodConnector', () => {
beforeEach(() => {
jest.clearAllMocks();
setMockValues({ status: Status.IDLE });
setMockActions({ makeRequest: jest.fn() });
});
it('renders connector ingestion method tab', () => {
const wrapper = shallow(<MethodConnector serviceType="mongodb" />);
const template = wrapper.find(NewSearchIndexTemplate);
expect(template.prop('type')).toEqual('connector');
});
});

View file

@ -9,9 +9,7 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
import { EuiConfirmModal, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Status } from '../../../../../../common/types/api';
import { docLinks } from '../../../../shared/doc_links';
@ -25,12 +23,11 @@ import { AddConnectorApiLogic } from '../../../api/connector/add_connector_api_l
import { FetchCloudHealthApiLogic } from '../../../api/stats/fetch_cloud_health_api_logic';
import { BETA_CONNECTORS, NATIVE_CONNECTORS } from '../../search_index/connector/constants';
import { NewSearchIndexLogic } from '../new_search_index_logic';
import { NewSearchIndexTemplate } from '../new_search_index_template';
import { errorToText } from '../utils/error_to_text';
import { AddConnectorLogic } from './add_connector_logic';
import { NewConnectorTemplate } from './new_connector_template';
interface MethodConnectorProps {
isNative?: boolean;
@ -41,11 +38,8 @@ export const MethodConnector: React.FC<MethodConnectorProps> = ({
serviceType,
isNative: isNativeProp = true,
}) => {
const { apiReset, makeRequest } = useActions(AddConnectorApiLogic);
const { apiReset, makeRequest } = useActions(AddConnectorLogic);
const { error, status } = useValues(AddConnectorApiLogic);
const { isModalVisible } = useValues(AddConnectorLogic);
const { setIsModalVisible } = useActions(AddConnectorLogic);
const { fullIndexName, language } = useValues(NewSearchIndexLogic);
const { isCloud } = useValues(KibanaLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
@ -76,7 +70,7 @@ export const MethodConnector: React.FC<MethodConnectorProps> = ({
</EuiFlexItem>
)}
<EuiFlexItem>
<NewSearchIndexTemplate
<NewConnectorTemplate
docsUrl={docLinks.connectors}
disabled={isGated}
error={errorToText(error)}
@ -84,59 +78,17 @@ export const MethodConnector: React.FC<MethodConnectorProps> = ({
onNameChange={() => {
apiReset();
}}
onSubmit={(name, lang) =>
makeRequest({ indexName: name, isNative, language: lang, serviceType })
onSubmit={(name) =>
makeRequest({
isNative,
language: null,
name,
serviceType,
})
}
buttonLoading={status === Status.LOADING}
isBeta={isBeta}
/>
{isModalVisible && (
<EuiConfirmModal
title={i18n.translate(
'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.title',
{
defaultMessage: 'Replace existing connector',
}
)}
onCancel={() => {
setIsModalVisible(false);
}}
onConfirm={() => {
makeRequest({
deleteExistingConnector: true,
indexName: fullIndexName,
isNative,
language,
serviceType,
});
}}
cancelButtonText={i18n.translate(
'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.cancelButton.label',
{
defaultMessage: 'Cancel',
}
)}
confirmButtonText={i18n.translate(
'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.confirmButton.label',
{
defaultMessage: 'Replace configuration',
}
)}
defaultFocusedButton="confirm"
>
{i18n.translate(
'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description',
{
defaultMessage:
'A deleted index named {indexName} was originally tied to an existing connector configuration. Would you like to replace the existing connector configuration with a new one?',
values: {
indexName: fullIndexName,
},
}
)}
</EuiConfirmModal>
)}
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,93 @@
/*
* 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 { kea, MakeLogicType } from 'kea';
import { Actions } from '../../../../shared/api_logic/create_api_logic';
import {
AddConnectorApiLogic,
AddConnectorApiLogicArgs,
AddConnectorApiLogicResponse,
} from '../../../api/connector/add_connector_api_logic';
import {
IndexExistsApiLogic,
IndexExistsApiParams,
IndexExistsApiResponse,
} from '../../../api/index/index_exists_api_logic';
import { isValidIndexName } from '../../../utils/validate_index_name';
import { UNIVERSAL_LANGUAGE_VALUE } from '../constants';
import { LanguageForOptimization } from '../types';
import { getLanguageForOptimization } from '../utils';
export interface NewConnectorValues {
data: IndexExistsApiResponse;
fullIndexName: string;
fullIndexNameExists: boolean;
fullIndexNameIsValid: boolean;
language: LanguageForOptimization;
languageSelectValue: string;
rawName: string;
}
type NewConnectorActions = Pick<
Actions<IndexExistsApiParams, IndexExistsApiResponse>,
'makeRequest'
> & {
connectorCreated: Actions<AddConnectorApiLogicArgs, AddConnectorApiLogicResponse>['apiSuccess'];
setLanguageSelectValue(language: string): { language: string };
setRawName(rawName: string): { rawName: string };
};
export const NewConnectorLogic = kea<MakeLogicType<NewConnectorValues, NewConnectorActions>>({
actions: {
setLanguageSelectValue: (language) => ({ language }),
setRawName: (rawName) => ({ rawName }),
},
connect: {
actions: [
AddConnectorApiLogic,
['apiSuccess as connectorCreated'],
IndexExistsApiLogic,
['makeRequest'],
],
values: [IndexExistsApiLogic, ['data']],
},
path: ['enterprise_search', 'content', 'new_search_index'],
reducers: {
languageSelectValue: [
UNIVERSAL_LANGUAGE_VALUE,
{
setLanguageSelectValue: (_, { language }) => language ?? null,
},
],
rawName: [
'',
{
setRawName: (_, { rawName }) => rawName,
},
],
},
selectors: ({ selectors }) => ({
fullIndexName: [() => [selectors.rawName], (name: string) => name],
fullIndexNameExists: [
() => [selectors.data, selectors.fullIndexName],
(data: IndexExistsApiResponse | undefined, fullIndexName: string) =>
data?.exists === true && data.indexName === fullIndexName,
],
fullIndexNameIsValid: [
() => [selectors.fullIndexName],
(fullIndexName) => isValidIndexName(fullIndexName),
],
language: [
() => [selectors.languageSelectValue],
(languageSelectValue) => getLanguageForOptimization(languageSelectValue),
],
}),
});

View file

@ -0,0 +1,213 @@
/*
* 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 React, { ChangeEvent } from 'react';
import { useValues, useActions } from 'kea';
import {
EuiButton,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { INGESTION_METHOD_IDS } from '../../../../../../common/constants';
import { BetaConnectorCallout } from '../../../../shared/beta/beta_connector_callout';
import { BACK_BUTTON_LABEL } from '../../../../shared/constants';
import { docLinks } from '../../../../shared/doc_links';
import { NewConnectorLogic } from './new_connector_logic';
export interface Props {
buttonLoading?: boolean;
disabled?: boolean;
docsUrl?: string;
error?: string | React.ReactNode;
isBeta?: boolean;
onNameChange?(name: string): void;
onSubmit(name: string): void;
type: string;
}
export const NewConnectorTemplate: React.FC<Props> = ({
buttonLoading,
disabled,
error,
onNameChange,
onSubmit,
type,
isBeta,
}) => {
const { fullIndexName, fullIndexNameExists, fullIndexNameIsValid, rawName } =
useValues(NewConnectorLogic);
const { setRawName } = useActions(NewConnectorLogic);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setRawName(e.target.value);
if (onNameChange) {
onNameChange(fullIndexName);
}
};
const formInvalid = !!error || fullIndexNameExists || !fullIndexNameIsValid;
const formError = () => {
if (fullIndexNameExists) {
return i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error',
{
defaultMessage: 'A connector with the name {connectorName} already exists',
values: {
connectorName: fullIndexName,
},
}
);
}
if (!fullIndexNameIsValid) {
return i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error',
{
defaultMessage: '{connectorName} is an invalid connector name',
values: {
connectorName: fullIndexName,
},
}
);
}
return error;
};
return (
<>
<EuiForm
component="form"
id="enterprise-search-create-connector"
onSubmit={(event) => {
event.preventDefault();
onSubmit(fullIndexName);
}}
>
<EuiFlexGroup direction="column">
{isBeta ? (
<EuiFlexItem>
<BetaConnectorCallout />
</EuiFlexItem>
) : null}
<EuiFlexItem>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.formTitle"
defaultMessage="Create a connector"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow>
<EuiFlexGroup>
<EuiFlexItem grow>
<EuiFormRow
isDisabled={disabled || buttonLoading}
label={i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputLabel',
{
defaultMessage: 'Connector name',
}
)}
isInvalid={formInvalid}
error={formError()}
fullWidth
>
<EuiFieldText
data-telemetry-id={`entSearchContent-${type}-newConnector-editName`}
placeholder={i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputPlaceholder',
{
defaultMessage: 'Set a name for your connector',
}
)}
fullWidth
disabled={disabled}
isInvalid={false}
value={rawName}
onChange={handleNameChange}
autoFocus
/>
</EuiFormRow>
<EuiText size="xs" color="subdued">
{i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputHelpText.lineTwo',
{
defaultMessage:
'Names should be lowercase and cannot contain spaces or special characters.',
}
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup direction="column" gutterSize="xs">
{type === INGESTION_METHOD_IDS.CONNECTOR && (
<EuiFlexItem grow={false}>
<EuiLink target="_blank" href={docLinks.connectors}>
{i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText',
{
defaultMessage: 'Learn more about connectors',
}
)}
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup direction="row" alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButton
data-telemetry-id={`entSearchContent-${type}-newConnector-goBack`}
isDisabled={buttonLoading}
onClick={() => history.back()}
>
{BACK_BUTTON_LABEL}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj={`entSearchContent-${type}-newConnector-createConnector`}
data-telemetry-id={`entSearchContent-${type}-newConnector-createConnector`}
fill
isDisabled={!rawName || buttonLoading || formInvalid || disabled}
isLoading={buttonLoading}
type="submit"
>
{i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.createIndex.buttonText',
{
defaultMessage: 'Create connector',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
</>
);
};

View file

@ -22,9 +22,10 @@ jest.mock('../../../shared/kibana/kibana_logic', () => ({
const DEFAULT_VALUES: NewSearchIndexValues = {
data: undefined as any,
fullIndexName: 'search-',
fullIndexName: '',
fullIndexNameExists: false,
fullIndexNameIsValid: true,
hasPrefix: false,
language: null,
languageSelectValue: UNIVERSAL_LANGUAGE_VALUE,
rawName: '',
@ -71,7 +72,7 @@ describe('NewSearchIndexLogic', () => {
NewSearchIndexLogic.actions.setRawName('rawname');
expect(NewSearchIndexLogic.values).toEqual({
...DEFAULT_VALUES,
fullIndexName: 'search-rawname',
fullIndexName: 'rawname',
fullIndexNameIsValid: true,
rawName: 'rawname',
});
@ -81,7 +82,7 @@ describe('NewSearchIndexLogic', () => {
NewSearchIndexLogic.actions.setRawName('invalid/name');
expect(NewSearchIndexLogic.values).toEqual({
...DEFAULT_VALUES,
fullIndexName: 'search-invalid/name',
fullIndexName: 'invalid/name',
fullIndexNameIsValid: false,
rawName: 'invalid/name',
});
@ -94,7 +95,7 @@ describe('NewSearchIndexLogic', () => {
jest.advanceTimersByTime(150);
await nextTick();
expect(NewSearchIndexLogic.actions.makeRequest).toHaveBeenCalledWith({
indexName: 'search-indexname',
indexName: 'indexname',
});
jest.useRealTimers();
});
@ -102,11 +103,11 @@ describe('NewSearchIndexLogic', () => {
describe('apiSuccess', () => {
it('sets correct values for existing index', () => {
NewSearchIndexLogic.actions.setRawName('indexname');
IndexExistsApiLogic.actions.apiSuccess({ exists: true, indexName: 'search-indexname' });
IndexExistsApiLogic.actions.apiSuccess({ exists: true, indexName: 'indexname' });
expect(NewSearchIndexLogic.values).toEqual({
...DEFAULT_VALUES,
data: { exists: true, indexName: 'search-indexname' },
fullIndexName: 'search-indexname',
data: { exists: true, indexName: 'indexname' },
fullIndexName: 'indexname',
fullIndexNameExists: true,
rawName: 'indexname',
});

View file

@ -43,6 +43,7 @@ export interface NewSearchIndexValues {
fullIndexName: string;
fullIndexNameExists: boolean;
fullIndexNameIsValid: boolean;
hasPrefix: boolean;
language: LanguageForOptimization;
languageSelectValue: string;
rawName: string;
@ -61,12 +62,14 @@ type NewSearchIndexActions = Pick<
AddConnectorApiLogicResponse
>['apiSuccess'];
crawlerIndexCreated: Actions<CreateCrawlerIndexArgs, CreateCrawlerIndexResponse>['apiSuccess'];
setHasPrefix(hasPrefix: boolean): { hasPrefix: boolean };
setLanguageSelectValue(language: string): { language: string };
setRawName(rawName: string): { rawName: string };
};
export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSearchIndexActions>>({
actions: {
setHasPrefix: (hasPrefix) => ({ hasPrefix }),
setLanguageSelectValue: (language) => ({ language }),
setRawName: (rawName) => ({ rawName }),
},
@ -103,6 +106,12 @@ export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSe
}),
path: ['enterprise_search', 'content', 'new_search_index'],
reducers: {
hasPrefix: [
false,
{
setHasPrefix: (_, { hasPrefix }) => hasPrefix,
},
],
languageSelectValue: [
UNIVERSAL_LANGUAGE_VALUE,
{
@ -117,7 +126,10 @@ export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSe
],
},
selectors: ({ selectors }) => ({
fullIndexName: [() => [selectors.rawName], (name: string) => `search-${name}`],
fullIndexName: [
() => [selectors.rawName, selectors.hasPrefix],
(name: string, hasPrefix: boolean) => (hasPrefix ? `search-${name}` : name),
],
fullIndexNameExists: [
() => [selectors.data, selectors.fullIndexName],
(data: IndexExistsApiResponse | undefined, fullIndexName: string) =>

View file

@ -31,7 +31,11 @@ describe('NewSearchIndexTemplate', () => {
name: 'my-name',
rawName: 'MY$_RAW_$NAME',
});
setMockActions({ makeRequest: jest.fn(), setLanguageSelectValue: jest.fn() });
setMockActions({
makeRequest: jest.fn(),
setHasPrefix: jest.fn(),
setLanguageSelectValue: jest.fn(),
});
});
it('renders', () => {

View file

@ -61,11 +61,13 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({
fullIndexName,
fullIndexNameExists,
fullIndexNameIsValid,
hasPrefix,
language,
rawName,
languageSelectValue,
} = useValues(NewSearchIndexLogic);
const { setRawName, setLanguageSelectValue } = useActions(NewSearchIndexLogic);
const { setRawName, setLanguageSelectValue, setHasPrefix } = useActions(NewSearchIndexLogic);
setHasPrefix(type === INGESTION_METHOD_IDS.CRAWLER);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setRawName(e.target.value);
@ -195,7 +197,7 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({
value={rawName}
onChange={handleNameChange}
autoFocus
prepend="search-"
prepend={hasPrefix ? 'search-' : undefined}
/>
</EuiFormRow>
<EuiText size="xs" color="subdued">

View file

@ -125,7 +125,11 @@ export const ApiKeyConfig: React.FC<{
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton onClick={clickGenerateApiKey} isLoading={status === Status.LOADING}>
<EuiButton
onClick={clickGenerateApiKey}
isLoading={status === Status.LOADING}
isDisabled={indexName.length === 0}
>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.button.label',
{

View file

@ -30,7 +30,7 @@ import { HttpLogic } from '../../../shared/http/http_logic';
import { KibanaLogic } from '../../../shared/kibana';
import { EuiButtonTo, EuiLinkTo } from '../../../shared/react_router_helpers';
import { handlePageChange } from '../../../shared/table_pagination';
import { NEW_INDEX_PATH } from '../../routes';
import { NEW_API_PATH } from '../../routes';
import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
import { CannotConnect } from '../search_index/components/cannot_connect';
@ -101,7 +101,7 @@ export const SearchIndices: React.FC = () => {
rightSideItems: isLoading
? []
: [
<EuiLinkTo data-test-subj="create-new-index-button" to={NEW_INDEX_PATH}>
<EuiLinkTo data-test-subj="create-new-index-button" to={NEW_API_PATH}>
<EuiButton
iconType="plusInCircle"
color="primary"

View file

@ -27,7 +27,7 @@ import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../../../common/constants';
import welcomeGraphicDark from '../../../assets/images/welcome_dark.svg';
import welcomeGraphicLight from '../../../assets/images/welcome_light.svg';
import { NEW_INDEX_PATH } from '../../enterprise_search_content/routes';
import { NEW_API_PATH } from '../../enterprise_search_content/routes';
import { docLinks } from '../doc_links';
import './add_content_empty_prompt.scss';
@ -60,7 +60,7 @@ export const AddContentEmptyPrompt: React.FC = () => {
<EuiFlexItem grow={false}>
<div>
<EuiLinkTo
to={generatePath(ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL + NEW_INDEX_PATH)}
to={generatePath(ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL + NEW_API_PATH)}
shouldNotCreateHref
>
<EuiButton color="primary" fill iconType="plusInCircle">

View file

@ -79,6 +79,7 @@ describe('addConnector lib function', () => {
indexName: 'index_name',
isNative: false,
language: 'fr',
name: 'index_name',
})
).resolves.toEqual(expect.objectContaining({ id: 'fakeId', index_name: 'index_name' }));
expect(createConnector).toHaveBeenCalledWith(mockClient.asCurrentUser, {
@ -123,6 +124,7 @@ describe('addConnector lib function', () => {
indexName: 'index_name',
isNative: true,
language: 'ja',
name: 'index_name',
})
).resolves.toEqual(expect.objectContaining({ id: 'fakeId', index_name: 'index_name' }));
expect(createConnector).toHaveBeenCalledWith(mockClient.asCurrentUser, {
@ -163,6 +165,7 @@ describe('addConnector lib function', () => {
indexName: 'index_name',
isNative: true,
language: 'en',
name: '',
})
).rejects.toEqual(new Error(ErrorCode.INDEX_ALREADY_EXISTS));
expect(mockClient.asCurrentUser.indices.create).not.toHaveBeenCalled();
@ -180,6 +183,7 @@ describe('addConnector lib function', () => {
indexName: 'index_name',
isNative: false,
language: 'en',
name: '',
})
).rejects.toEqual(new Error(ErrorCode.CONNECTOR_DOCUMENT_ALREADY_EXISTS));
expect(mockClient.asCurrentUser.indices.create).not.toHaveBeenCalled();
@ -201,6 +205,7 @@ describe('addConnector lib function', () => {
indexName: 'index_name',
isNative: false,
language: 'en',
name: '',
})
).rejects.toEqual(new Error(ErrorCode.CRAWLER_ALREADY_EXISTS));
expect(mockClient.asCurrentUser.indices.create).not.toHaveBeenCalled();
@ -219,6 +224,7 @@ describe('addConnector lib function', () => {
indexName: 'index_name',
isNative: true,
language: 'en',
name: '',
})
).rejects.toEqual(new Error(ErrorCode.INDEX_ALREADY_EXISTS));
expect(mockClient.asCurrentUser.indices.create).not.toHaveBeenCalled();
@ -246,6 +252,7 @@ describe('addConnector lib function', () => {
indexName: 'index_name',
isNative: true,
language: null,
name: '',
})
).resolves.toEqual(expect.objectContaining({ id: 'fakeId', index_name: 'index_name' }));
expect(deleteConnectorById).toHaveBeenCalledWith(mockClient.asCurrentUser, 'connectorId');
@ -254,7 +261,7 @@ describe('addConnector lib function', () => {
indexName: 'index_name',
isNative: true,
language: null,
name: 'index_name',
name: '',
pipeline: {
extract_binary_content: true,
name: 'ent-search-generic-ingestion',

View file

@ -32,6 +32,7 @@ export const addConnector = async (
indexName: string | null;
isNative: boolean;
language: string | null;
name: string | null;
serviceType?: string | null;
}
): Promise<Connector> => {
@ -84,7 +85,7 @@ export const addConnector = async (
const connector = await createConnector(client.asCurrentUser, {
...input,
name: input.indexName || '',
name: input.name || '',
...nativeFields,
pipeline: await getDefaultPipeline(client),
});

View file

@ -53,9 +53,10 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
validate: {
body: schema.object({
delete_existing_connector: schema.maybe(schema.boolean()),
index_name: schema.string(),
index_name: schema.maybe(schema.string()),
is_native: schema.boolean(),
language: schema.nullable(schema.string()),
name: schema.maybe(schema.string()),
service_type: schema.maybe(schema.string()),
}),
},
@ -65,9 +66,10 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
try {
const body = await addConnector(client, {
deleteExistingConnector: request.body.delete_existing_connector,
indexName: request.body.index_name,
indexName: request.body.index_name ?? null,
isNative: request.body.is_native,
language: request.body.language,
name: request.body.name ?? null,
serviceType: request.body.service_type,
});
return response.ok({ body });
@ -640,4 +642,40 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
return response.ok({ body: connectorResponse });
})
);
router.put(
{
path: '/internal/enterprise_search/connectors/{connectorId}/index_name/{indexName}',
validate: {
params: schema.object({
connectorId: schema.string(),
indexName: schema.string(),
}),
},
},
elasticsearchErrorHandler(log, async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const { connectorId, indexName } = request.params;
try {
await client.asCurrentUser.transport.request({
body: {
index_name: indexName,
},
method: 'PUT',
path: `/_connector/${connectorId}/_index_name`,
});
return response.ok();
} catch (error) {
if (isIndexNotFoundException(error)) {
return createError({
errorCode: ErrorCode.INDEX_NOT_FOUND,
message: `Could not find index ${indexName}`,
response,
statusCode: 404,
});
}
throw error;
}
})
);
}

View file

@ -47,6 +47,7 @@ export function registerCrawlerRoutes(routeDependencies: RouteDependencies) {
indexName: request.body.index_name,
isNative: true,
language: request.body.language,
name: null,
serviceType: ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE,
};
const { client } = (await context.core).elasticsearch;

View file

@ -12696,7 +12696,6 @@
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.formDescription": "Cet index contiendra le contenu de la source de données et il est optimisé avec les mappings de champ par défaut pour les expériences de recherche correspondantes. Donnez un nom unique à votre index et définissez éventuellement un {language_analyzer} par défaut pour lindex.",
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.isInvalid.error": "{indexName} n'est pas un nom d'index valide",
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "Votre index sera nommé : {indexName}",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "Un index supprimé appelé {indexName} était, à l'origine, lié à une configuration de connecteur. Voulez-vous remplacer cette configuration de connecteur par la nouvelle ?",
"xpack.enterpriseSearch.content.searchIndex.cannotConnect.body": "Le robot d'indexation Elastic requiert Enterprise Search. {link}",
"xpack.enterpriseSearch.content.searchIndex.mappings.description": "Vos documents sont constitués d'un ensemble de champs. Les mappings d'index donnent à chaque champ un type (tel que {keyword}, {number} ou {date}) et des champs secondaires supplémentaires. Ces mappings d'index déterminent les fonctions disponibles dans votre réglage de pertinence et votre expérience de recherche.",
"xpack.enterpriseSearch.content.searchIndex.nativeCloudCallout.content": "Convertissez-les en {link} afin quils soient autogérés sur votre propre infrastructure. Les connecteurs natifs sont disponibles uniquement dans votre déploiement Elastic Cloud.",
@ -14308,9 +14307,6 @@
"xpack.enterpriseSearch.content.newIndex.selectConnector.subscriptionButtonLabel": "Plans d'abonnement",
"xpack.enterpriseSearch.content.newIndex.selectConnector.upgradeContent": "Pour utiliser ce connecteur, vous devez mettre à jour votre licence vers Platinum ou commencer un essai gratuit de 30 jours.",
"xpack.enterpriseSearch.content.newIndex.selectConnector.upgradeTitle": "Mettre à niveau vers Elastic Platinum",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.cancelButton.label": "Annuler",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.confirmButton.label": "Remplacer la configuration",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.title": "Remplacer un connecteur existant",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.connectorAlreadyExists": "Un connecteur existe déjà pour cet index",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.genericError": "Nous n'avons pas pu créer votre index",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.indexAlreadyExists": "L'index existe déjà.",

View file

@ -12709,7 +12709,6 @@
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.formDescription": "このインデックスには、データソースコンテンツが格納されます。また、デフォルトフィールドマッピングで最適化され、関連する検索エクスペリエンスを実現します。一意のインデックス名を指定し、任意でインデックスのデフォルト{language_analyzer}を設定します。",
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.isInvalid.error": "{indexName}は無効なインデックス名です",
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "インデックスは次の名前になります:{indexName}",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "削除されたインデックス{indexName}は、既存のコネクター構成に関連付けられていました。既存のコネクター構成を新しいコネクター構成で置き換えますか?",
"xpack.enterpriseSearch.content.searchIndex.cannotConnect.body": "Elastic Webクローラーにはエンタープライズ サーチが必要です。{link}",
"xpack.enterpriseSearch.content.searchIndex.mappings.description": "ドキュメントには、複数のフィールドのセットがあります。インデックスマッピングでは、各フィールドに型({keyword}、{number}、{date})と、追加のサブフィールドが指定されます。これらのインデックスマッピングでは、関連性の調整と検索エクスペリエンスで使用可能な機能が決まります。",
"xpack.enterpriseSearch.content.searchIndex.nativeCloudCallout.content": "独自のインフラでセルフマネージドされる{link}に変換します。ネイティブコネクターはElastic Cloudデプロイでのみ使用できます。",
@ -14321,9 +14320,6 @@
"xpack.enterpriseSearch.content.newIndex.selectConnector.subscriptionButtonLabel": "サブスクリプションオプション",
"xpack.enterpriseSearch.content.newIndex.selectConnector.upgradeContent": "このコネクターを使用するには、ライセンスをPlatinumに更新するか、30日間の無料トライアルを開始する必要があります。",
"xpack.enterpriseSearch.content.newIndex.selectConnector.upgradeTitle": "Elastic Platinum へのアップグレード",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.cancelButton.label": "キャンセル",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.confirmButton.label": "構成を置換",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.title": "既存のコネクターを置換",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.connectorAlreadyExists": "このインデックスのコネクターはすでに存在します",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.genericError": "インデックスを作成できませんでした",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.indexAlreadyExists": "このインデックスはすでに存在します",

View file

@ -12803,7 +12803,6 @@
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.formDescription": "此索引将存放您的数据源内容,并通过默认字段映射进行优化,以提供相关搜索体验。提供唯一的索引名称,并为索引设置默认的 {language_analyzer}(可选)。",
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.isInvalid.error": "{indexName} 为无效索引名称",
"xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "您的索引将命名为:{indexName}",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "名为 {indexName} 的已删除索引最初绑定到现有连接器配置。是否要将现有连接器配置替换成新的?",
"xpack.enterpriseSearch.content.searchIndex.cannotConnect.body": "Elastic 网络爬虫需要 Enterprise Search。{link}",
"xpack.enterpriseSearch.content.searchIndex.mappings.description": "您的文档由一组字段构成。索引映射为每个字段提供类型(例如,{keyword}、{number}或{date})和其他子字段。这些索引映射确定您的相关性调整和搜索体验中可用的功能。",
"xpack.enterpriseSearch.content.searchIndex.nativeCloudCallout.content": "将其转换为将在您自己的基础设施上进行自我管理的 {link}。本机连接器只可用于您的 Elastic Cloud 部署。",
@ -14415,9 +14414,6 @@
"xpack.enterpriseSearch.content.newIndex.selectConnector.subscriptionButtonLabel": "订阅计划",
"xpack.enterpriseSearch.content.newIndex.selectConnector.upgradeContent": "要使用此连接器,必须将您的许可证更新到白金级,或开始 30 天免费试用。",
"xpack.enterpriseSearch.content.newIndex.selectConnector.upgradeTitle": "升级到 Elastic 白金级",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.cancelButton.label": "取消",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.confirmButton.label": "替换配置",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.title": "替换现有连接器",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.connectorAlreadyExists": "此索引的连接器已存在",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.genericError": "无法创建您的索引",
"xpack.enterpriseSearch.content.newIndex.steps.buildConnector.error.indexAlreadyExists": "此索引已存在",