diff --git a/x-pack/plugins/enterprise_search/common/types/connectors.ts b/x-pack/plugins/enterprise_search/common/types/connectors.ts
new file mode 100644
index 0000000000000..9af170e4dbdcd
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/common/types/connectors.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export interface DeleteConnectorResponse {
+ acknowledge: boolean;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/delete_connector_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/delete_connector_api_logic.ts
new file mode 100644
index 0000000000000..a427b634c3b6e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/delete_connector_api_logic.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { DeleteConnectorResponse } from '../../../../../common/types/connectors';
+
+import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
+import { HttpLogic } from '../../../shared/http';
+
+export interface DeleteConnectorApiLogicArgs {
+ connectorId: string;
+ shouldDeleteIndex: boolean;
+}
+
+export interface DeleteConnectorApiLogicResponse {
+ acknowledged: boolean;
+}
+
+export const deleteConnector = async ({
+ connectorId,
+ shouldDeleteIndex = false,
+}: DeleteConnectorApiLogicArgs) => {
+ return await HttpLogic.values.http.delete(
+ `/internal/enterprise_search/connectors/${connectorId}`,
+ {
+ query: {
+ shouldDeleteIndex,
+ },
+ }
+ );
+};
+
+export const DeleteConnectorApiLogic = createApiLogic(
+ ['delete_connector_api_logic'],
+ deleteConnector
+);
+
+export type DeleteConnectorApiLogicActions = Actions<
+ DeleteConnectorApiLogicArgs,
+ DeleteConnectorResponse
+>;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx
index 0d797329093c8..3519fc9e16086 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors.tsx
@@ -42,6 +42,7 @@ import { ConnectorStats } from './connector_stats';
import { ConnectorsLogic } from './connectors_logic';
import { ConnectorsTable } from './connectors_table';
import { CrawlerEmptyState } from './crawler_empty_state';
+import { DeleteConnectorModal } from './delete_connector_modal';
export const baseBreadcrumbs = [
i18n.translate('xpack.enterpriseSearch.content.connectors.breadcrumb', {
@@ -53,7 +54,8 @@ export interface ConnectorsProps {
isCrawler: boolean;
}
export const Connectors: React.FC = ({ isCrawler }) => {
- const { fetchConnectors, onPaginate, setIsFirstRequest } = useActions(ConnectorsLogic);
+ const { fetchConnectors, onPaginate, setIsFirstRequest, openDeleteModal } =
+ useActions(ConnectorsLogic);
const { data, isLoading, searchParams, isEmpty, connectors } = useValues(ConnectorsLogic);
const { errorConnectingMessage } = useValues(HttpLogic);
const [searchQuery, setSearchValue] = useState('');
@@ -69,151 +71,158 @@ export const Connectors: React.FC = ({ isCrawler }) => {
return !isLoading && isEmpty && !isCrawler ? (
) : (
- {
- KibanaLogic.values.navigateToUrl(NEW_INDEX_SELECT_CONNECTOR_PATH);
- }}
- >
-
- ,
- {
- KibanaLogic.values.navigateToUrl(NEW_INDEX_SELECT_CONNECTOR_NATIVE_PATH);
- }}
- >
- {i18n.translate('xpack.enterpriseSearch.connectors.newNativeConnectorButtonLabel', {
- defaultMessage: 'New Native Connector',
- })}
- ,
- {
- KibanaLogic.values.navigateToUrl(NEW_INDEX_SELECT_CONNECTOR_CLIENTS_PATH);
- }}
- >
- {i18n.translate(
- 'xpack.enterpriseSearch.connectors.newConnectorsClientButtonLabel',
- { defaultMessage: 'New Connector Client' }
- )}
- ,
- ]
- : [
- {
- KibanaLogic.values.navigateToUrl(
- generateEncodedPath(NEW_INDEX_METHOD_PATH, {
- type: INGESTION_METHOD_IDS.CRAWLER,
- })
- );
- }}
- >
- {i18n.translate('xpack.enterpriseSearch.connectors.newCrawlerButtonLabel', {
- defaultMessage: 'New web crawler',
- })}
- ,
- ],
- }}
- >
- {Boolean(errorConnectingMessage) && (
- <>
-
-
- >
- )}
-
-
-
-
- {isEmpty && isCrawler ? (
-
- ) : (
- <>
-
-
-
- {!isCrawler ? (
-
- ) : (
-
+ <>
+
+ {
+ KibanaLogic.values.navigateToUrl(NEW_INDEX_SELECT_CONNECTOR_PATH);
+ }}
+ >
+
+ ,
+ {
+ KibanaLogic.values.navigateToUrl(NEW_INDEX_SELECT_CONNECTOR_NATIVE_PATH);
+ }}
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.connectors.newNativeConnectorButtonLabel',
+ {
+ defaultMessage: 'New Native Connector',
+ }
)}
-
-
-
-
- setSearchValue(event.queryText)}
- />
-
-
+ ,
+ {
+ KibanaLogic.values.navigateToUrl(NEW_INDEX_SELECT_CONNECTOR_CLIENTS_PATH);
+ }}
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.connectors.newConnectorsClientButtonLabel',
+ { defaultMessage: 'New Connector Client' }
+ )}
+ ,
+ ]
+ : [
+ {
+ KibanaLogic.values.navigateToUrl(
+ generateEncodedPath(NEW_INDEX_METHOD_PATH, {
+ type: INGESTION_METHOD_IDS.CRAWLER,
+ })
+ );
+ }}
+ >
+ {i18n.translate('xpack.enterpriseSearch.connectors.newCrawlerButtonLabel', {
+ defaultMessage: 'New web crawler',
+ })}
+ ,
+ ],
+ }}
+ >
+ {Boolean(errorConnectingMessage) && (
+ <>
+
+
>
)}
-
-
+
+
+
+
+ {isEmpty && isCrawler ? (
+
+ ) : (
+ <>
+
+
+
+ {!isCrawler ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ setSearchValue(event.queryText)}
+ />
+
+
+ >
+ )}
+
+
+ >
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_logic.ts
index d4b6925ddade3..332b20d84928e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_logic.ts
@@ -12,6 +12,10 @@ import { Connector } from '@kbn/search-connectors/types';
import { Status } from '../../../../../common/types/api';
import { Meta } from '../../../../../common/types/pagination';
+import {
+ DeleteConnectorApiLogic,
+ DeleteConnectorApiLogicActions,
+} from '../../api/connector/delete_connector_api_logic';
import {
FetchConnectorsApiLogic,
FetchConnectorsApiLogicActions,
@@ -21,6 +25,10 @@ export type ConnectorViewItem = Connector & { docsCount?: number };
export interface ConnectorsActions {
apiError: FetchConnectorsApiLogicActions['apiError'];
apiSuccess: FetchConnectorsApiLogicActions['apiSuccess'];
+ closeDeleteModal(): void;
+ deleteConnector: DeleteConnectorApiLogicActions['makeRequest'];
+ deleteError: DeleteConnectorApiLogicActions['apiError'];
+ deleteSuccess: DeleteConnectorApiLogicActions['apiSuccess'];
fetchConnectors({
fetchCrawlersOnly,
from,
@@ -39,11 +47,26 @@ export interface ConnectorsActions {
};
makeRequest: FetchConnectorsApiLogicActions['makeRequest'];
onPaginate(newPageIndex: number): { newPageIndex: number };
+ openDeleteModal(
+ connectorName: string,
+ connectorId: string,
+ indexName: string | null
+ ): {
+ connectorId: string;
+ connectorName: string;
+ indexName: string | null;
+ };
setIsFirstRequest(): void;
}
export interface ConnectorsValues {
connectors: ConnectorViewItem[];
data: typeof FetchConnectorsApiLogic.values.data;
+ deleteModalConnectorId: string;
+ deleteModalConnectorName: string;
+ deleteModalIndexName: string | null;
+ deleteStatus: typeof DeleteConnectorApiLogic.values.status;
+ isDeleteLoading: boolean;
+ isDeleteModalVisible: boolean;
isEmpty: boolean;
isFetchConnectorsDetailsLoading: boolean;
isFirstRequest: boolean;
@@ -60,6 +83,7 @@ export interface ConnectorsValues {
export const ConnectorsLogic = kea>({
actions: {
+ closeDeleteModal: true,
fetchConnectors: ({ fetchCrawlersOnly, from, size, searchQuery }) => ({
fetchCrawlersOnly,
from,
@@ -67,13 +91,32 @@ export const ConnectorsLogic = kea ({ newPageIndex }),
+ openDeleteModal: (connectorName, connectorId, indexName) => ({
+ connectorId,
+ connectorName,
+ indexName,
+ }),
setIsFirstRequest: true,
},
connect: {
- actions: [FetchConnectorsApiLogic, ['makeRequest', 'apiSuccess', 'apiError']],
- values: [FetchConnectorsApiLogic, ['data', 'status']],
+ actions: [
+ DeleteConnectorApiLogic,
+ ['apiError as deleteError', 'apiSuccess as deleteSuccess', 'makeRequest as deleteConnector'],
+ FetchConnectorsApiLogic,
+ ['makeRequest', 'apiSuccess', 'apiError'],
+ ],
+ values: [
+ DeleteConnectorApiLogic,
+ ['status as deleteStatus'],
+ FetchConnectorsApiLogic,
+ ['data', 'status'],
+ ],
},
- listeners: ({ actions }) => ({
+ listeners: ({ actions, values }) => ({
+ deleteSuccess: () => {
+ actions.closeDeleteModal();
+ actions.makeRequest(values.searchParams);
+ },
fetchConnectors: async (input, breakpoint) => {
await breakpoint(150);
actions.makeRequest(input);
@@ -81,6 +124,34 @@ export const ConnectorsLogic = kea ({
+ deleteModalConnectorId: [
+ '',
+ {
+ closeDeleteModal: () => '',
+ openDeleteModal: (_, { connectorId }) => connectorId,
+ },
+ ],
+ deleteModalConnectorName: [
+ '',
+ {
+ closeDeleteModal: () => '',
+ openDeleteModal: (_, { connectorName }) => connectorName,
+ },
+ ],
+ deleteModalIndexName: [
+ null,
+ {
+ closeDeleteModal: () => null,
+ openDeleteModal: (_, { indexName }) => indexName,
+ },
+ ],
+ isDeleteModalVisible: [
+ false,
+ {
+ closeDeleteModal: () => false,
+ openDeleteModal: () => true,
+ },
+ ],
isFirstRequest: [
true,
{
@@ -128,6 +199,10 @@ export const ConnectorsLogic = kea [selectors.deleteStatus],
+ (deleteStatus) => Status.LOADING === deleteStatus,
+ ],
isEmpty: [
() => [selectors.data],
(data) =>
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx
index 3f58137b5dbd9..2c675c362b00e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/connectors_table.tsx
@@ -41,6 +41,7 @@ interface ConnectorsTableProps {
items: ConnectorViewItem[];
meta?: Meta;
onChange: (criteria: CriteriaWithPagination) => void;
+ onDelete: (connectorName: string, connectorId: string, indexName: string | null) => void;
}
export const ConnectorsTable: React.FC = ({
items,
@@ -53,6 +54,7 @@ export const ConnectorsTable: React.FC = ({
},
onChange,
isLoading,
+ onDelete,
}) => {
const { navigateToUrl } = useValues(KibanaLogic);
const columns: Array> = [
@@ -122,6 +124,23 @@ export const ConnectorsTable: React.FC = ({
},
{
actions: [
+ {
+ description: 'Delete this connector',
+ icon: 'trash',
+ isPrimary: false,
+ name: (connector) =>
+ i18n.translate(
+ 'xpack.enterpriseSearch.content.connectors.connectorTable.column.actions.deleteIndex',
+ {
+ defaultMessage: 'Delete connector {connectorName}',
+ values: { connectorName: connector.name },
+ }
+ ),
+ onClick: (connector) => {
+ onDelete(connector.name, connector.id, connector.index_name);
+ },
+ type: 'icon',
+ },
{
description: i18n.translate(
'xpack.enterpriseSearch.content.connectors.connectorTable.columns.actions.viewIndex',
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/delete_connector_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/delete_connector_modal.tsx
new file mode 100644
index 0000000000000..1a4236f59a798
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/delete_connector_modal.tsx
@@ -0,0 +1,158 @@
+/*
+ * 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, { useState, useEffect } from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import {
+ EuiCheckbox,
+ EuiConfirmModal,
+ EuiFieldText,
+ EuiForm,
+ EuiFormRow,
+ EuiSpacer,
+ EuiText,
+ EuiTextColor,
+} from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import { ConnectorsLogic } from './connectors_logic';
+
+export const DeleteConnectorModal: React.FC = () => {
+ const { closeDeleteModal, deleteConnector } = useActions(ConnectorsLogic);
+ const {
+ deleteModalConnectorId: connectorId,
+ deleteModalConnectorName: connectorName,
+ deleteModalIndexName,
+ isDeleteLoading,
+ isDeleteModalVisible,
+ } = useValues(ConnectorsLogic);
+
+ const [inputConnectorName, setInputConnectorName] = useState('');
+ const [shouldDeleteIndex, setShouldDeleteIndex] = useState(false);
+
+ useEffect(() => {
+ setShouldDeleteIndex(false);
+ setInputConnectorName('');
+ }, [isDeleteModalVisible]);
+
+ return isDeleteModalVisible ? (
+ {
+ closeDeleteModal();
+ }}
+ onConfirm={() => {
+ deleteConnector({
+ connectorId,
+ shouldDeleteIndex,
+ });
+ }}
+ cancelButtonText={
+ isDeleteLoading
+ ? i18n.translate(
+ 'xpack.enterpriseSearch.content.connectors.deleteModal.closeButton.title',
+ {
+ defaultMessage: 'Close',
+ }
+ )
+ : i18n.translate(
+ 'xpack.enterpriseSearch.content.connectors.deleteModal.cancelButton.title',
+ {
+ defaultMessage: 'Cancel',
+ }
+ )
+ }
+ confirmButtonText={i18n.translate(
+ 'xpack.enterpriseSearch.content.connectors.deleteModal.confirmButton.title',
+ {
+ defaultMessage: 'Delete index',
+ }
+ )}
+ defaultFocusedButton="confirm"
+ buttonColor="danger"
+ confirmButtonDisabled={inputConnectorName.trim() !== connectorName}
+ isLoading={isDeleteLoading}
+ >
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.connectors.deleteModal.delete.description',
+ {
+ defaultMessage: 'You are about to delete this connector:',
+ }
+ )}
+
+
+
+
+
+
+
+ {connectorName}
+
+ ),
+ }}
+ />
+
+
+ {deleteModalIndexName && (
+ <>
+ setShouldDeleteIndex(!shouldDeleteIndex)}
+ />
+
+ >
+ )}
+
+
+ setInputConnectorName(e.target.value)}
+ value={inputConnectorName}
+ />
+
+
+
+ ) : (
+ <>>
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
index cc071d41ccd47..e1b5b399dc3a0 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
@@ -8,6 +8,8 @@
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import {
+ deleteConnectorById,
+ fetchConnectorById,
fetchConnectors,
fetchSyncJobsByConnectorId,
putUpdateNative,
@@ -22,11 +24,14 @@ import {
import { ConnectorStatus, FilteringRule, SyncJobType } from '@kbn/search-connectors';
import { cancelSyncs } from '@kbn/search-connectors/lib/cancel_syncs';
+import { isResourceNotFoundException } from '@kbn/search-connectors/utils/identify_exceptions';
import { ErrorCode } from '../../../common/types/error_codes';
import { addConnector } from '../../lib/connectors/add_connector';
import { startSync } from '../../lib/connectors/start_sync';
+import { deleteAccessControlIndex } from '../../lib/indices/delete_access_control_index';
import { fetchIndexCounts } from '../../lib/indices/fetch_index_counts';
+import { deleteIndexPipelines } from '../../lib/pipelines/delete_pipelines';
import { getDefaultPipeline } from '../../lib/pipelines/get_default_pipeline';
import { updateDefaultPipeline } from '../../lib/pipelines/update_default_pipeline';
import { updateConnectorPipeline } from '../../lib/pipelines/update_pipeline';
@@ -37,6 +42,7 @@ import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handl
import {
isAccessControlDisabledException,
isExpensiveQueriesNotAllowedException,
+ isIndexNotFoundException,
} from '../../utils/identify_exceptions';
export function registerConnectorRoutes({ router, log }: RouteDependencies) {
@@ -539,4 +545,66 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
});
})
);
+
+ router.delete(
+ {
+ path: '/internal/enterprise_search/connectors/{connectorId}',
+ validate: {
+ params: schema.object({
+ connectorId: schema.string(),
+ }),
+ query: schema.object({
+ shouldDeleteIndex: schema.maybe(schema.boolean()),
+ }),
+ },
+ },
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+ const { connectorId } = request.params;
+ const { shouldDeleteIndex } = request.query;
+
+ let connectorResponse;
+ let indexNameToDelete;
+ try {
+ if (shouldDeleteIndex) {
+ const connector = await fetchConnectorById(client.asCurrentUser, connectorId);
+ indexNameToDelete = connector?.value.index_name;
+ }
+ connectorResponse = await deleteConnectorById(client.asCurrentUser, connectorId);
+ if (indexNameToDelete) {
+ await deleteIndexPipelines(client, indexNameToDelete);
+ await deleteAccessControlIndex(client, indexNameToDelete);
+ await client.asCurrentUser.indices.delete({ index: indexNameToDelete });
+ }
+ } catch (error) {
+ if (isResourceNotFoundException(error)) {
+ return createError({
+ errorCode: ErrorCode.RESOURCE_NOT_FOUND,
+ message: i18n.translate(
+ 'xpack.enterpriseSearch.server.routes.connectors.resource_not_found_error',
+ {
+ defaultMessage: 'Connector with id {connectorId} is not found.',
+ values: { connectorId },
+ }
+ ),
+ response,
+ statusCode: 404,
+ });
+ }
+
+ if (isIndexNotFoundException(error)) {
+ return createError({
+ errorCode: ErrorCode.INDEX_NOT_FOUND,
+ message: 'Could not find index',
+ response,
+ statusCode: 404,
+ });
+ }
+
+ throw error;
+ }
+
+ return response.ok({ body: connectorResponse });
+ })
+ );
}