Skip to content

Commit

Permalink
[Enterprise Search] Check in progress syncs for connector (#150362)
Browse files Browse the repository at this point in the history
## Summary

This adds a warning about in-progress syncs, and cancels any that exist
when deleting a connector.
  • Loading branch information
sphilipse authored Feb 7, 2023
1 parent b31b18d commit c25da59
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { i18n } from '@kbn/i18n';

import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';

export interface CancelSyncsApiArgs {
Expand All @@ -25,3 +25,5 @@ export const CancelSyncsApiLogic = createApiLogic(['cancel_syncs_api_logic'], ca
defaultMessage: 'Successfully canceled syncs',
}),
});

export type CancelSyncsActions = Actions<CancelSyncsApiArgs, {}>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,28 @@
*/

import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices';
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';

export interface FetchIndexApiParams {
indexName: string;
}

export type FetchIndexApiResponse = ElasticsearchIndexWithIngestion;
export type FetchIndexApiResponse = ElasticsearchIndexWithIngestion & {
has_in_progress_syncs?: boolean;
};

export const fetchIndex = async ({
indexName,
}: FetchIndexApiParams): Promise<FetchIndexApiResponse> => {
const route = `/internal/enterprise_search/indices/${indexName}`;

return await HttpLogic.values.http.get<ElasticsearchIndexWithIngestion>(route);
return await HttpLogic.values.http.get<FetchIndexApiResponse>(route);
};

export const FetchIndexApiLogic = createApiLogic(['fetch_index_api_logic'], fetchIndex, {
clearFlashMessagesOnMakeRequest: false,
showErrorFlash: false,
});

export type FetchIndexActions = Actions<FetchIndexApiParams, FetchIndexApiResponse>;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
IngestPipelineParams,
SyncStatus,
} from '../../../../../common/types/connectors';
import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices';
import { Actions } from '../../../shared/api_logic/create_api_logic';
import { flashSuccessToast } from '../../../shared/flash_messages';

Expand All @@ -26,6 +25,7 @@ import {
CachedFetchIndexApiLogicActions,
} from '../../api/index/cached_fetch_index_api_logic';

import { FetchIndexApiResponse } from '../../api/index/fetch_index_api_logic';
import { ElasticsearchViewIndex, IngestionMethod, IngestionStatus } from '../../types';
import {
getIngestionMethod,
Expand Down Expand Up @@ -232,11 +232,12 @@ export const IndexViewLogic = kea<MakeLogicType<IndexViewValues, IndexViewAction
],
isConnectorIndex: [
() => [selectors.indexData],
(data: ElasticsearchIndexWithIngestion | undefined) => isConnectorIndex(data),
(data: FetchIndexApiResponse | undefined) => isConnectorIndex(data),
],
isSyncing: [
() => [selectors.syncStatus],
(syncStatus: SyncStatus) => syncStatus === SyncStatus.IN_PROGRESS,
() => [selectors.indexData, selectors.syncStatus],
(indexData: FetchIndexApiResponse | null, syncStatus: SyncStatus) =>
indexData?.has_in_progress_syncs || syncStatus === SyncStatus.IN_PROGRESS,
],
isWaitingForSync: [
() => [selectors.fetchIndexApiData, selectors.localSyncNowValue],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@
* 2.0.
*/

import React from 'react';
import React, { useState } from 'react';

import { useActions, useValues } from 'kea';

import { EuiConfirmModal } from '@elastic/eui';
import {
EuiCallOut,
EuiConfirmModal,
EuiFieldText,
EuiForm,
EuiFormRow,
EuiSpacer,
} from '@elastic/eui';

import { i18n } from '@kbn/i18n';

import { ingestionMethodToText } from '../../utils/indices';
Expand All @@ -20,10 +28,15 @@ export const DeleteIndexModal: React.FC = () => {
const { closeDeleteModal, deleteIndex } = useActions(IndicesLogic);
const {
deleteModalIndexName: indexName,
deleteModalIndexHasInProgressSyncs,
deleteModalIngestionMethod: ingestionMethod,
isDeleteModalVisible,
isDeleteLoading,
isFetchIndexDetailsLoading,
} = useValues(IndicesLogic);

const [inputIndexName, setInputIndexName] = useState('');

return isDeleteModalVisible ? (
<EuiConfirmModal
title={i18n.translate('xpack.enterpriseSearch.content.searchIndices.deleteModal.title', {
Expand Down Expand Up @@ -59,20 +72,70 @@ export const DeleteIndexModal: React.FC = () => {
)}
defaultFocusedButton="confirm"
buttonColor="danger"
isLoading={isDeleteLoading}
confirmButtonDisabled={inputIndexName.trim() !== indexName}
isLoading={isDeleteLoading || isFetchIndexDetailsLoading}
>
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.deleteModal.delete.description',
{
defaultMessage:
'Deleting this index will also delete all of its data and its {ingestionMethod} configuration. Any associated search engines will no longer be able to access any data stored in this index.This can not be undone.',
'Deleting this index will also delete all of its data and its {ingestionMethod} configuration. Any associated search engines will no longer be able to access any data stored in this index.',
values: {
ingestionMethod: ingestionMethodToText(ingestionMethod),
},
}
)}
</p>
{deleteModalIndexHasInProgressSyncs && (
<>
<EuiCallOut
color="warning"
iconType="alert"
title={i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.deleteModal.syncsWarning.title',
{
defaultMessage: 'Syncs in progress',
}
)}
>
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.deleteModal.syncsWarning.description',
{
defaultMessage:
'This index has in-progress syncs. Deleting the index without stopping these syncs may result in dangling sync job records or the index being re-created.',
}
)}
</p>
</EuiCallOut>
<EuiSpacer />
</>
)}
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.deleteModal.syncsWarning.indexNameDescription',
{
defaultMessage: 'This action cannot be undone. Please type {indexName} to confirm.',
values: { indexName },
}
)}
</p>
<EuiForm>
<EuiFormRow
label={i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.deleteModal.indexNameInput.label',
{
defaultMessage: 'Index name',
}
)}
>
<EuiFieldText
onChange={(e) => setInputIndexName(e.target.value)}
value={inputIndexName}
/>
</EuiFormRow>
</EuiForm>
</EuiConfirmModal>
) : (
<></>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ import { IndicesLogic } from './indices_logic';
const DEFAULT_VALUES = {
data: undefined,
deleteModalIndex: null,
deleteModalIndexHasInProgressSyncs: false,
deleteModalIndexName: '',
deleteModalIngestionMethod: IngestionMethod.API,
deleteStatus: Status.IDLE,
hasNoIndices: false,
indexDetails: undefined,
indexDetailsStatus: 0,
indices: [],
isDeleteLoading: false,
isDeleteModalVisible: false,
isFetchIndexDetailsLoading: true,
isFirstRequest: true,
isLoading: true,
meta: DEFAULT_META,
Expand Down Expand Up @@ -80,21 +84,27 @@ describe('IndicesLogic', () => {
});
describe('openDeleteModal', () => {
it('should set deleteIndexName and set isDeleteModalVisible to true', () => {
IndicesLogic.actions.fetchIndexDetails = jest.fn();
IndicesLogic.actions.openDeleteModal(connectorIndex);
expect(IndicesLogic.values).toEqual({
...DEFAULT_VALUES,
deleteModalIndex: connectorIndex,
deleteModalIndexName: 'connector',
deleteModalIngestionMethod: IngestionMethod.CONNECTOR,
isDeleteModalVisible: true,
});
expect(IndicesLogic.actions.fetchIndexDetails).toHaveBeenCalledWith({
indexName: 'connector',
});
});
});
describe('closeDeleteModal', () => {
it('should set deleteIndexName to empty and set isDeleteModalVisible to false', () => {
IndicesLogic.actions.openDeleteModal(connectorIndex);
IndicesLogic.actions.fetchIndexDetails = jest.fn();
IndicesLogic.actions.closeDeleteModal();
expect(IndicesLogic.values).toEqual(DEFAULT_VALUES);
expect(IndicesLogic.values).toEqual({
...DEFAULT_VALUES,
indexDetailsStatus: Status.LOADING,
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@ import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/ind
import { Actions } from '../../../shared/api_logic/create_api_logic';
import { DEFAULT_META } from '../../../shared/constants';
import { updateMetaPageIndex } from '../../../shared/table_pagination';
import {
CancelSyncsActions,
CancelSyncsApiLogic,
} from '../../api/connector/cancel_syncs_api_logic';
import {
DeleteIndexApiLogic,
DeleteIndexApiLogicArgs,
DeleteIndexApiLogicValues,
} from '../../api/index/delete_index_api_logic';
import {
FetchIndexActions,
FetchIndexApiLogic,
FetchIndexApiResponse,
} from '../../api/index/fetch_index_api_logic';
import { FetchIndicesAPILogic } from '../../api/index/fetch_indices_api_logic';
import { ElasticsearchViewIndex, IngestionMethod } from '../../types';
import { getIngestionMethod, indexToViewIndex } from '../../utils/indices';
Expand All @@ -43,10 +52,12 @@ export interface IndicesActions {
returnHiddenIndices: boolean;
searchQuery?: string;
};
cancelSuccess: CancelSyncsActions['apiSuccess'];
closeDeleteModal(): void;
deleteError: Actions<DeleteIndexApiLogicArgs, DeleteIndexApiLogicValues>['apiError'];
deleteIndex: Actions<DeleteIndexApiLogicArgs, DeleteIndexApiLogicValues>['makeRequest'];
deleteSuccess: Actions<DeleteIndexApiLogicArgs, DeleteIndexApiLogicValues>['apiSuccess'];
fetchIndexDetails: FetchIndexActions['makeRequest'];
fetchIndices({
meta,
returnHiddenIndices,
Expand All @@ -63,14 +74,18 @@ export interface IndicesActions {
}
export interface IndicesValues {
data: typeof FetchIndicesAPILogic.values.data;
deleteModalIndex: ElasticsearchViewIndex | null;
deleteModalIndex: FetchIndexApiResponse | null;
deleteModalIndexHasInProgressSyncs: boolean;
deleteModalIndexName: string;
deleteModalIngestionMethod: IngestionMethod;
deleteStatus: typeof DeleteIndexApiLogic.values.status;
hasNoIndices: boolean;
indexDetails: FetchIndexApiResponse | null;
indexDetailsStatus: Status;
indices: ElasticsearchViewIndex[];
isDeleteLoading: boolean;
isDeleteModalVisible: boolean;
isFetchIndexDetailsLoading: boolean;
isFirstRequest: boolean;
isLoading: boolean;
meta: Meta;
Expand All @@ -92,19 +107,28 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
},
connect: {
actions: [
CancelSyncsApiLogic,
['apiSuccess as cancelSuccess'],
FetchIndexApiLogic,
['makeRequest as fetchIndexDetails'],
FetchIndicesAPILogic,
['makeRequest', 'apiSuccess', 'apiError'],
DeleteIndexApiLogic,
['apiError as deleteError', 'apiSuccess as deleteSuccess', 'makeRequest as deleteIndex'],
],
values: [
FetchIndexApiLogic,
['data as indexDetails', 'status as indexDetailsStatus'],
FetchIndicesAPILogic,
['data', 'status'],
DeleteIndexApiLogic,
['status as deleteStatus'],
],
},
listeners: ({ actions, values }) => ({
cancelSuccess: async () => {
actions.fetchIndexDetails({ indexName: values.deleteModalIndexName });
},
deleteSuccess: () => {
actions.closeDeleteModal();
actions.fetchIndices(values.searchParams);
Expand All @@ -113,14 +137,17 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
await breakpoint(150);
actions.makeRequest(input);
},
openDeleteModal: ({ index }) => {
actions.fetchIndexDetails({ indexName: index.name });
},
}),
path: ['enterprise_search', 'content', 'indices_logic'],
reducers: () => ({
deleteModalIndex: [
null,
deleteModalIndexName: [
'',
{
closeDeleteModal: () => null,
openDeleteModal: (_, { index }) => index,
closeDeleteModal: () => '',
openDeleteModal: (_, { index: { name } }) => name,
},
],
isDeleteModalVisible: [
Expand Down Expand Up @@ -154,10 +181,18 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
],
}),
selectors: ({ selectors }) => ({
deleteModalIndexName: [() => [selectors.deleteModalIndex], (index) => index?.name ?? ''],
deleteModalIngestionMethod: [
deleteModalIndex: [
() => [selectors.deleteModalIndexName, selectors.indexDetails],
(indexName: string, indexDetails: FetchIndexApiResponse | null) =>
indexName === indexDetails?.name ? indexDetails : null,
],
deleteModalIndexHasInProgressSyncs: [
() => [selectors.deleteModalIndex],
(index: ElasticsearchViewIndex | null) =>
(index: FetchIndexApiResponse | null) => (index ? index.has_in_progress_syncs : false),
],
deleteModalIngestionMethod: [
() => [selectors.indexDetails],
(index: FetchIndexApiResponse | null) =>
index ? getIngestionMethod(index) : IngestionMethod.API,
],
hasNoIndices: [
Expand All @@ -174,6 +209,11 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
() => [selectors.deleteStatus],
(status: IndicesValues['deleteStatus']) => [Status.LOADING].includes(status),
],
isFetchIndexDetailsLoading: [
() => [selectors.indexDetailsStatus],
(status: IndicesValues['indexDetailsStatus']) =>
[Status.IDLE, Status.LOADING].includes(status),
],
isLoading: [
() => [selectors.status, selectors.isFirstRequest],
(status, isFirstRequest) => [Status.LOADING, Status.IDLE].includes(status) && isFirstRequest,
Expand Down
Loading

0 comments on commit c25da59

Please sign in to comment.