[Enterprise Search] Enable converting native connector to custom (#155853)

## Summary

This adds the ability to convert a native connector to a customized
connector.

---------

Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com>
This commit is contained in:
Sander Philipse 2023-04-27 19:23:14 +02:00 committed by GitHub
parent fbe3aa36b3
commit e44087a06d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 320 additions and 0 deletions

View file

@ -126,6 +126,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
apiKeys: `${KIBANA_DOCS}api-keys.html`,
behavioralAnalytics: `${ENTERPRISE_SEARCH_DOCS}analytics-overview.html`,
behavioralAnalyticsEvents: `${ENTERPRISE_SEARCH_DOCS}analytics-events.html`,
buildConnector: `{$ENTERPRISE_SEARCH_DOCS}build-connector.html`,
bulkApi: `${ELASTICSEARCH_DOCS}docs-bulk.html`,
configuration: `${ENTERPRISE_SEARCH_DOCS}configuration.html`,
connectors: `${ENTERPRISE_SEARCH_DOCS}connectors.html`,

View file

@ -111,6 +111,7 @@ export interface DocLinks {
readonly apiKeys: string;
readonly behavioralAnalytics: string;
readonly behavioralAnalyticsEvents: string;
readonly buildConnector: string;
readonly bulkApi: string;
readonly configuration: string;
readonly connectors: string;

View file

@ -0,0 +1,32 @@
/*
* 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 { mockHttpValues } from '../../../__mocks__/kea_logic';
import { nextTick } from '@kbn/test-jest-helpers';
import { convertConnector } from './convert_connector_api_logic';
describe('ConvertConnectorApilogic', () => {
const { http } = mockHttpValues;
beforeEach(() => {
jest.clearAllMocks();
});
describe('convertConnector', () => {
it('calls correct api', async () => {
const promise = Promise.resolve('result');
http.put.mockReturnValue(promise);
const result = convertConnector({ connectorId: 'connectorId1' });
await nextTick();
expect(http.put).toHaveBeenCalledWith(
'/internal/enterprise_search/connectors/connectorId1/native',
{ body: JSON.stringify({ is_native: false }) }
);
await expect(result).resolves.toEqual('result');
});
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
export interface ConvertConnectorApiLogicArgs {
connectorId: string;
}
export interface ConvertConnectorApiLogicResponse {
updated: boolean;
}
export const convertConnector = async ({
connectorId,
}: ConvertConnectorApiLogicArgs): Promise<ConvertConnectorApiLogicResponse> => {
const route = `/internal/enterprise_search/connectors/${connectorId}/native`;
return await HttpLogic.values.http.put<{ updated: boolean }>(route, {
body: JSON.stringify({ is_native: false }),
});
};
export const ConvertConnectorApiLogic = createApiLogic(
['convert_connector_api_logic'],
convertConnector
);

View file

@ -0,0 +1,115 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiTitle,
EuiSpacer,
EuiText,
EuiLink,
EuiButton,
EuiConfirmModal,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants';
import { docLinks } from '../../../../../shared/doc_links';
import { ConvertConnectorLogic } from './convert_connector_logic';
export const ConvertConnector: React.FC = () => {
const { convertConnector, hideModal, showModal } = useActions(ConvertConnectorLogic);
const { isLoading, isModalVisible } = useValues(ConvertConnectorLogic);
return (
<>
{isModalVisible && (
<EuiConfirmModal
onCancel={() => hideModal()}
onConfirm={() => convertConnector()}
title={i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.convertInfexConfirm.title',
{ defaultMessage: 'Sure you want to convert your connector?' }
)}
buttonColor="danger"
cancelButtonText={CANCEL_BUTTON_LABEL}
confirmButtonText={i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.convertIndexConfirm.text',
{
defaultMessage: 'Yes',
}
)}
isLoading={isLoading}
defaultFocusedButton="confirm"
maxWidth
>
<EuiText>
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.convertIndexConfirm.description',
{
defaultMessage:
"Once you convert a native connector to a self-managed connector client this can't be undone.",
}
)}
</p>
</EuiText>
</EuiConfirmModal>
)}
<EuiFlexGroup direction="row" alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiIcon type="wrench" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xxs">
<h4>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.convertConnector.title',
{
defaultMessage: 'Customize your connector',
}
)}
</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText size="s">
<FormattedMessage
id="xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.convertConnector.description"
defaultMessage="Want to customize this native connector? Convert it to a {link}, to be self-managed on your own infrastructure."
values={{
link: (
<EuiLink href={docLinks.buildConnector} target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.convertConnector.linkTitle',
{ defaultMessage: 'connector client' }
)}
</EuiLink>
),
}}
/>
<EuiSpacer size="s" />
<EuiButton onClick={() => showModal()}>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.convertConnector.buttonTitle',
{ defaultMessage: 'Convert connector' }
)}
</EuiButton>
</EuiText>
</>
);
};

View file

@ -0,0 +1,82 @@
/*
* 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.
*/
/*
* 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 { Status } from '../../../../../../../common/types/api';
import { Actions } from '../../../../../shared/api_logic/create_api_logic';
import {
ConvertConnectorApiLogic,
ConvertConnectorApiLogicArgs,
ConvertConnectorApiLogicResponse,
} from '../../../../api/connector/convert_connector_api_logic';
import { IndexViewActions, IndexViewLogic } from '../../index_view_logic';
interface ConvertConnectorValues {
connectorId: typeof IndexViewLogic.values.connectorId;
isLoading: boolean;
isModalVisible: boolean;
status: Status;
}
type ConvertConnectorActions = Pick<
Actions<ConvertConnectorApiLogicArgs, ConvertConnectorApiLogicResponse>,
'apiError' | 'apiSuccess' | 'makeRequest'
> & {
convertConnector(): void;
fetchIndex(): IndexViewActions['fetchIndex'];
hideModal(): void;
showModal(): void;
};
export const ConvertConnectorLogic = kea<
MakeLogicType<ConvertConnectorValues, ConvertConnectorActions>
>({
actions: {
convertConnector: () => true,
deleteDomain: () => true,
hideModal: () => true,
showModal: () => true,
},
connect: {
actions: [
ConvertConnectorApiLogic,
['apiError', 'apiSuccess', 'makeRequest'],
IndexViewLogic,
['fetchIndex'],
],
values: [ConvertConnectorApiLogic, ['status'], IndexViewLogic, ['connectorId']],
},
listeners: ({ actions, values }) => ({
convertConnector: () => {
if (values.connectorId) {
actions.makeRequest({ connectorId: values.connectorId });
}
},
}),
path: ['enterprise_search', 'convert_connector_modal'],
reducers: {
isModalVisible: [
false,
{
apiError: () => false,
apiSuccess: () => false,
hideModal: () => false,
showModal: () => true,
},
],
},
selectors: ({ selectors }) => ({
isLoading: [() => [selectors.status], (status: Status) => status === Status.LOADING],
}),
});

View file

@ -31,6 +31,7 @@ import { IndexViewLogic } from '../../index_view_logic';
import { ConnectorNameAndDescription } from '../connector_name_and_description/connector_name_and_description';
import { NATIVE_CONNECTORS } from '../constants';
import { ConvertConnector } from './convert_connector';
import { NativeConnectorAdvancedConfiguration } from './native_connector_advanced_configuration';
import { NativeConnectorConfigurationConfig } from './native_connector_configuration_config';
import { ResearchConfiguration } from './research_configuration';
@ -203,6 +204,11 @@ export const NativeConnectorConfiguration: React.FC = () => {
</EuiText>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel hasBorder hasShadow={false}>
<ConvertConnector />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -36,6 +36,7 @@ class DocLinks {
public appSearchWebCrawlerReference: string;
public behavioralAnalytics: string;
public behavioralAnalyticsEvents: string;
public buildConnector: string;
public bulkApi: string;
public clientsGoIndex: string;
public clientsGuide: string;
@ -164,6 +165,7 @@ class DocLinks {
this.appSearchWebCrawlerReference = '';
this.behavioralAnalytics = '';
this.behavioralAnalyticsEvents = '';
this.buildConnector = '';
this.bulkApi = '';
this.clientsGoIndex = '';
this.clientsGuide = '';
@ -294,6 +296,7 @@ class DocLinks {
this.appSearchWebCrawlerReference = docLinks.links.appSearch.webCrawlerReference;
this.behavioralAnalytics = docLinks.links.enterpriseSearch.behavioralAnalytics;
this.behavioralAnalyticsEvents = docLinks.links.enterpriseSearch.behavioralAnalyticsEvents;
this.buildConnector = docLinks.links.enterpriseSearch.buildConnector;
this.bulkApi = docLinks.links.enterpriseSearch.bulkApi;
this.clientsGoIndex = docLinks.links.clients.goIndex;
this.clientsGuide = docLinks.links.clients.guide;

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 { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import { CONNECTORS_INDEX } from '../..';
import { Connector } from '../../../common/types/connectors';
export const putUpdateNative = async (
client: IScopedClusterClient,
connectorId: string,
isNative: boolean
) => {
const result = await client.asCurrentUser.update<Connector>({
doc: {
is_native: isNative,
},
id: connectorId,
index: CONNECTORS_INDEX,
});
return result;
};

View file

@ -22,6 +22,7 @@ import { fetchSyncJobsByConnectorId } from '../../lib/connectors/fetch_sync_jobs
import { cancelSyncs } from '../../lib/connectors/post_cancel_syncs';
import { updateFiltering } from '../../lib/connectors/put_update_filtering';
import { updateFilteringDraft } from '../../lib/connectors/put_update_filtering_draft';
import { putUpdateNative } from '../../lib/connectors/put_update_native';
import { startConnectorSync } from '../../lib/connectors/start_sync';
import { updateConnectorConfiguration } from '../../lib/connectors/update_connector_configuration';
import { updateConnectorNameAndDescription } from '../../lib/connectors/update_connector_name_and_description';
@ -383,4 +384,24 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
return result ? response.ok({ body: result }) : response.conflict();
})
);
router.put(
{
path: '/internal/enterprise_search/connectors/{connectorId}/native',
validate: {
body: schema.object({
is_native: schema.boolean(),
}),
params: schema.object({
connectorId: schema.string(),
}),
},
},
elasticsearchErrorHandler(log, async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const connectorId = decodeURIComponent(request.params.connectorId);
const { is_native } = request.body;
const result = await putUpdateNative(client, connectorId, is_native);
return result ? response.ok({ body: result }) : response.conflict();
})
);
}