Skip to content

Commit

Permalink
[Enterprise Search] Show error for crawlers without a connector docum…
Browse files Browse the repository at this point in the history
…ent (#150928)

## Summary

This shows an error for broken crawlers, where a connector document has
been deleted. It offers users the choice to recreate the connector or
delete their index.


https://user-images.githubusercontent.com/94373878/221916920-f75299b4-f88d-4cba-89a2-92fe62366050.mov


https://user-images.githubusercontent.com/94373878/221916935-b416a782-92b4-4795-a1af-73e97fa523c8.mov
  • Loading branch information
sphilipse committed Feb 28, 2023
1 parent a87f440 commit 6be7e3d
Show file tree
Hide file tree
Showing 18 changed files with 757 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { recreateCrawlerConnector } from './recreate_crawler_connector_api_logic';

describe('CreateCrawlerIndexApiLogic', () => {
const { http } = mockHttpValues;
beforeEach(() => {
jest.clearAllMocks();
});
describe('createCrawlerIndex', () => {
it('calls correct api', async () => {
const indexName = 'elastic-co-crawler';
http.post.mockReturnValue(Promise.resolve({ connector_id: 'connectorId' }));

const result = recreateCrawlerConnector({ indexName });
await nextTick();

expect(http.post).toHaveBeenCalledWith(
'/internal/enterprise_search/indices/elastic-co-crawler/crawler/connector'
);
await expect(result).resolves.toEqual({ connector_id: 'connectorId' });
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 RecreateCrawlerConnectorArgs {
indexName: string;
}

export interface RecreateCrawlerConnectorResponse {
created: string; // the name of the newly created index
}

export const recreateCrawlerConnector = async ({ indexName }: RecreateCrawlerConnectorArgs) => {
const route = `/internal/enterprise_search/indices/${indexName}/crawler/connector`;

return await HttpLogic.values.http.post<RecreateCrawlerConnectorResponse>(route);
};

export const RecreateCrawlerConnectorApiLogic = createApiLogic(
['recreate_crawler_connector_api_logic'],
recreateCrawlerConnector
);

export type RecreateCrawlerConnectorActions = Actions<
RecreateCrawlerConnectorArgs,
RecreateCrawlerConnectorResponse
>;
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 DeleteIndexApiLogicArgs {
Expand Down Expand Up @@ -36,3 +36,5 @@ export const DeleteIndexApiLogic = createApiLogic(['delete_index_api_logic'], de
},
}),
});

export type DeleteIndexApiActions = Actions<DeleteIndexApiLogicArgs, DeleteIndexApiLogicValues>;
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { SyncsContextMenu } from './syncs_context_menu';
export const getHeaderActions = (indexData?: ElasticsearchIndexWithIngestion) => {
const ingestionMethod = getIngestionMethod(indexData);
return [
...(isCrawlerIndex(indexData) ? [<CrawlerStatusIndicator />] : []),
...(isCrawlerIndex(indexData) && indexData.connector ? [<CrawlerStatusIndicator />] : []),
...(isConnectorIndex(indexData) ? [<SyncsContextMenu />] : []),
<SearchEnginesPopover
indexName={indexData?.name}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 React from 'react';

import { useActions, useValues } from 'kea';

import { EuiButton, EuiPageTemplate } from '@elastic/eui';

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

import { Status } from '../../../../../../common/types/api';

import { RecreateCrawlerConnectorApiLogic } from '../../../api/crawler/recreate_crawler_connector_api_logic';
import { DeleteIndexModal } from '../../search_indices/delete_index_modal';
import { IndicesLogic } from '../../search_indices/indices_logic';
import { IndexViewLogic } from '../index_view_logic';

import { NoConnectorRecordLogic } from './no_connector_record_logic';

export const NoConnectorRecord: React.FC = () => {
const { indexName } = useValues(IndexViewLogic);
const { isDeleteLoading } = useValues(IndicesLogic);
const { openDeleteModal } = useActions(IndicesLogic);
const { makeRequest } = useActions(RecreateCrawlerConnectorApiLogic);
const { status } = useValues(RecreateCrawlerConnectorApiLogic);
NoConnectorRecordLogic.mount();
const buttonsDisabled = status === Status.LOADING || isDeleteLoading;

return (
<>
<DeleteIndexModal />

<EuiPageTemplate.EmptyPrompt
iconType="alert"
color="danger"
title={
<h2>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.noCrawlerConnectorFound.title',
{
defaultMessage: "This index's connector configuration has been removed",
}
)}
</h2>
}
body={
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.noCrawlerConnectorFound.description',
{
defaultMessage:
'We could not find a connector configuration for this crawler index. The record should be recreated, or the index should be deleted.',
}
)}
</p>
}
actions={[
<EuiButton
color="danger"
disabled={buttonsDisabled}
isLoading={status === Status.LOADING}
onClick={() => makeRequest({ indexName })}
>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.noCrawlerConnectorFound.recreateConnectorRecord',
{
defaultMessage: 'Recreate connector record',
}
)}
</EuiButton>,
<EuiButton
color="danger"
disabled={buttonsDisabled}
isLoading={isDeleteLoading}
fill
onClick={() => openDeleteModal(indexName)}
>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.noCrawlerConnectorFound.deleteIndex',
{
defaultMessage: 'Delete index',
}
)}
</EuiButton>,
]}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 { LogicMounter } from '../../../../__mocks__/kea_logic';

import { KibanaLogic } from '../../../../shared/kibana';

import { RecreateCrawlerConnectorApiLogic } from '../../../api/crawler/recreate_crawler_connector_api_logic';
import { DeleteIndexApiLogic } from '../../../api/index/delete_index_api_logic';
import { SEARCH_INDICES_PATH } from '../../../routes';

import { NoConnectorRecordLogic } from './no_connector_record_logic';

describe('NoConnectorRecordLogic', () => {
const { mount: deleteMount } = new LogicMounter(DeleteIndexApiLogic);
const { mount: recreateMount } = new LogicMounter(RecreateCrawlerConnectorApiLogic);
const { mount } = new LogicMounter(NoConnectorRecordLogic);
beforeEach(() => {
deleteMount();
recreateMount();
mount();
});
it('should redirect to search indices on delete', () => {
KibanaLogic.values.navigateToUrl = jest.fn();
DeleteIndexApiLogic.actions.apiSuccess({} as any);
expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(SEARCH_INDICES_PATH);
});
it('should fetch index on recreate', () => {
NoConnectorRecordLogic.actions.fetchIndex = jest.fn();
RecreateCrawlerConnectorApiLogic.actions.apiSuccess({} as any);
expect(NoConnectorRecordLogic.actions.fetchIndex).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { KibanaLogic } from '../../../../shared/kibana';

import {
RecreateCrawlerConnectorActions,
RecreateCrawlerConnectorApiLogic,
} from '../../../api/crawler/recreate_crawler_connector_api_logic';
import {
DeleteIndexApiActions,
DeleteIndexApiLogic,
} from '../../../api/index/delete_index_api_logic';
import { SEARCH_INDICES_PATH } from '../../../routes';
import { IndexViewActions, IndexViewLogic } from '../index_view_logic';

type NoConnectorRecordActions = RecreateCrawlerConnectorActions['apiSuccess'] & {
deleteSuccess: DeleteIndexApiActions['apiSuccess'];
fetchIndex: IndexViewActions['fetchIndex'];
};

export const NoConnectorRecordLogic = kea<MakeLogicType<{}, NoConnectorRecordActions>>({
connect: {
actions: [
RecreateCrawlerConnectorApiLogic,
['apiSuccess'],
IndexViewLogic,
['fetchIndex'],
DeleteIndexApiLogic,
['apiSuccess as deleteSuccess'],
],
},
listeners: ({ actions }) => ({
apiSuccess: () => {
actions.fetchIndex();
},
deleteSuccess: () => {
KibanaLogic.values.navigateToUrl(SEARCH_INDICES_PATH);
},
}),
path: ['enterprise_search', 'content', 'no_connector_record'],
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { AutomaticCrawlScheduler } from './crawler/automatic_crawl_scheduler/aut
import { CrawlCustomSettingsFlyout } from './crawler/crawl_custom_settings_flyout/crawl_custom_settings_flyout';
import { CrawlerConfiguration } from './crawler/crawler_configuration/crawler_configuration';
import { SearchIndexDomainManagement } from './crawler/domain_management/domain_management';
import { NoConnectorRecord } from './crawler/no_connector_record';
import { SearchIndexDocuments } from './documents';
import { SearchIndexIndexMappings } from './index_mappings';
import { IndexNameLogic } from './index_name_logic';
Expand Down Expand Up @@ -224,12 +225,16 @@ export const SearchIndex: React.FC = () => {
rightSideItems: getHeaderActions(index),
}}
>
<>
{indexName === index?.name && (
<EuiTabbedContent tabs={tabs} selectedTab={selectedTab} onTabClick={onTabClick} />
)}
{isCrawlerIndex(index) && <CrawlCustomSettingsFlyout />}
</>
{isCrawlerIndex(index) && !index.connector ? (
<NoConnectorRecord />
) : (
<>
{indexName === index?.name && (
<EuiTabbedContent tabs={tabs} selectedTab={selectedTab} onTabClick={onTabClick} />
)}
{isCrawlerIndex(index) && <CrawlCustomSettingsFlyout />}
</>
)}
</EnterpriseSearchContentPageTemplate>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('IndicesLogic', () => {
describe('openDeleteModal', () => {
it('should set deleteIndexName and set isDeleteModalVisible to true', () => {
IndicesLogic.actions.fetchIndexDetails = jest.fn();
IndicesLogic.actions.openDeleteModal(connectorIndex);
IndicesLogic.actions.openDeleteModal(connectorIndex.name);
expect(IndicesLogic.values).toEqual({
...DEFAULT_VALUES,
deleteModalIndexName: 'connector',
Expand All @@ -98,7 +98,7 @@ describe('IndicesLogic', () => {
});
describe('closeDeleteModal', () => {
it('should set deleteIndexName to empty and set isDeleteModalVisible to false', () => {
IndicesLogic.actions.openDeleteModal(connectorIndex);
IndicesLogic.actions.openDeleteModal(connectorIndex.name);
IndicesLogic.actions.fetchIndexDetails = jest.fn();
IndicesLogic.actions.closeDeleteModal();
expect(IndicesLogic.values).toEqual({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export interface IndicesActions {
}): { meta: Meta; returnHiddenIndices: boolean; searchQuery?: string };
makeRequest: typeof FetchIndicesAPILogic.actions.makeRequest;
onPaginate(newPageIndex: number): { newPageIndex: number };
openDeleteModal(index: ElasticsearchViewIndex): { index: ElasticsearchViewIndex };
openDeleteModal(indexName: string): { indexName: string };
setIsFirstRequest(): void;
}
export interface IndicesValues {
Expand Down Expand Up @@ -102,7 +102,7 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
searchQuery,
}),
onPaginate: (newPageIndex) => ({ newPageIndex }),
openDeleteModal: (index) => ({ index }),
openDeleteModal: (indexName) => ({ indexName }),
setIsFirstRequest: true,
},
connect: {
Expand Down Expand Up @@ -137,8 +137,8 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
await breakpoint(150);
actions.makeRequest(input);
},
openDeleteModal: ({ index }) => {
actions.fetchIndexDetails({ indexName: index.name });
openDeleteModal: ({ indexName }) => {
actions.fetchIndexDetails({ indexName });
},
}),
path: ['enterprise_search', 'content', 'indices_logic'],
Expand All @@ -147,7 +147,7 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
'',
{
closeDeleteModal: () => '',
openDeleteModal: (_, { index: { name } }) => name,
openDeleteModal: (_, { indexName }) => indexName,
},
],
isDeleteModalVisible: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface IndicesTableProps {
isLoading?: boolean;
meta: Meta;
onChange: (criteria: CriteriaWithPagination<ElasticsearchViewIndex>) => void;
onDelete: (index: ElasticsearchViewIndex) => void;
onDelete: (indexName: string) => void;
}

export const IndicesTable: React.FC<IndicesTableProps> = ({
Expand Down Expand Up @@ -175,7 +175,7 @@ export const IndicesTable: React.FC<IndicesTableProps> = ({
},
}
),
onClick: (index) => onDelete(index),
onClick: (index) => onDelete(index.name),
type: 'icon',
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function getIngestionStatus(index?: ElasticsearchIndexWithIngestion): Ing
if (!index || isApiIndex(index)) {
return IngestionStatus.CONNECTED;
}
if (isConnectorIndex(index) || isCrawlerIndex(index)) {
if (isConnectorIndex(index) || (isCrawlerIndex(index) && index.connector)) {
if (
index.connector.last_seen &&
moment(index.connector.last_seen).isBefore(moment().subtract(30, 'minutes'))
Expand Down

0 comments on commit 6be7e3d

Please sign in to comment.