diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts new file mode 100644 index 0000000000000..b2b76d5b987b9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const INDEXING_STATUS_PROGRESS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.progress.title', + { + defaultMessage: 'Indexing progress', + } +); + +export const INDEXING_STATUS_HAS_ERRORS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.hasErrors.title', + { + defaultMessage: 'Several documents have field conversion errors.', + } +); + +export const INDEXING_STATUS_HAS_ERRORS_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.hasErrors.button', + { + defaultMessage: 'View errors', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts new file mode 100644 index 0000000000000..4a97f11e8f0ee --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { IndexingStatus } from './indexing_status'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx new file mode 100644 index 0000000000000..097c3bbc8e9ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiPanel } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; +import { IndexingStatusErrors } from './indexing_status_errors'; +import { IndexingStatusFetcher } from './indexing_status_fetcher'; +import { IndexingStatus } from './indexing_status'; + +describe('IndexingStatus', () => { + const getItemDetailPath = jest.fn(); + const getStatusPath = jest.fn(); + const onComplete = jest.fn(); + const setGlobalIndexingStatus = jest.fn(); + + const props = { + percentageComplete: 50, + numDocumentsWithErrors: 1, + activeReindexJobId: 12, + viewLinkPath: '/path', + itemId: '1', + getItemDetailPath, + getStatusPath, + onComplete, + setGlobalIndexingStatus, + }; + + it('renders', () => { + const wrapper = shallow(<IndexingStatus {...props} />); + const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')( + props.percentageComplete, + props.numDocumentsWithErrors + ); + + expect(shallow(fetcher).find(EuiPanel)).toHaveLength(1); + expect(shallow(fetcher).find(IndexingStatusContent)).toHaveLength(1); + }); + + it('renders errors', () => { + const wrapper = shallow(<IndexingStatus {...props} percentageComplete={100} />); + const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')(100, 1); + expect(shallow(fetcher).find(IndexingStatusErrors)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx new file mode 100644 index 0000000000000..beec0babea590 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; +import { IndexingStatusErrors } from './indexing_status_errors'; +import { IndexingStatusFetcher } from './indexing_status_fetcher'; + +import { IIndexingStatus } from '../types'; + +export interface IIndexingStatusProps extends IIndexingStatus { + viewLinkPath: string; + itemId: string; + getItemDetailPath?(itemId: string): string; + getStatusPath(itemId: string, activeReindexJobId: number): string; + onComplete(numDocumentsWithErrors: number): void; + setGlobalIndexingStatus?(activeReindexJob: IIndexingStatus): void; +} + +export const IndexingStatus: React.FC<IIndexingStatusProps> = (props) => ( + <IndexingStatusFetcher {...props}> + {(percentageComplete, numDocumentsWithErrors) => ( + <div> + {percentageComplete < 100 && ( + <EuiPanel paddingSize="l" hasShadow> + <IndexingStatusContent percentageComplete={percentageComplete} /> + </EuiPanel> + )} + {percentageComplete === 100 && numDocumentsWithErrors > 0 && ( + <> + <EuiSpacer /> + <IndexingStatusErrors viewLinkPath={props.viewLinkPath} /> + </> + )} + </div> + )} + </IndexingStatusFetcher> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx new file mode 100644 index 0000000000000..9fe0e890e6943 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiProgress, EuiTitle } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; + +describe('IndexingStatusContent', () => { + it('renders', () => { + const wrapper = shallow(<IndexingStatusContent percentageComplete={50} />); + + expect(wrapper.find(EuiTitle)).toHaveLength(1); + expect(wrapper.find(EuiProgress)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx new file mode 100644 index 0000000000000..a0c67388621a8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiProgress, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { INDEXING_STATUS_PROGRESS_TITLE } from './constants'; + +interface IIndexingStatusContentProps { + percentageComplete: number; +} + +export const IndexingStatusContent: React.FC<IIndexingStatusContentProps> = ({ + percentageComplete, +}) => ( + <div data-test-subj="IndexingStatusProgressMeter"> + <EuiTitle size="s"> + <h3>{INDEXING_STATUS_PROGRESS_TITLE}</h3> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiProgress color="primary" size="m" value={percentageComplete} max={100} /> + </div> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx new file mode 100644 index 0000000000000..fc706aee659a5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiCallOut, EuiButton } from '@elastic/eui'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { IndexingStatusErrors } from './indexing_status_errors'; + +describe('IndexingStatusErrors', () => { + it('renders', () => { + const wrapper = shallow(<IndexingStatusErrors viewLinkPath="/path" />); + + expect(wrapper.find(EuiButton)).toHaveLength(1); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(EuiLinkTo)).toHaveLength(1); + expect(wrapper.find(EuiLinkTo).prop('to')).toEqual('/path'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx new file mode 100644 index 0000000000000..a928400b2338c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiButton, EuiCallOut } from '@elastic/eui'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { INDEXING_STATUS_HAS_ERRORS_TITLE, INDEXING_STATUS_HAS_ERRORS_BUTTON } from './constants'; + +interface IIndexingStatusErrorsProps { + viewLinkPath: string; +} + +export const IndexingStatusErrors: React.FC<IIndexingStatusErrorsProps> = ({ viewLinkPath }) => ( + <EuiCallOut + color="danger" + iconType="cross" + title="There was an error" + data-test-subj="IndexingStatusErrors" + > + <p>{INDEXING_STATUS_HAS_ERRORS_TITLE}</p> + <EuiButton color="danger" fill={true} size="s" data-test-subj="ViewErrorsButton"> + <EuiLinkTo to={viewLinkPath}>{INDEXING_STATUS_HAS_ERRORS_BUTTON}</EuiLinkTo> + </EuiButton> + </EuiCallOut> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx new file mode 100644 index 0000000000000..cb7c82f91ed61 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState, useRef } from 'react'; + +import { HttpLogic } from '../http'; +import { flashAPIErrors } from '../flash_messages'; + +interface IIndexingStatusFetcherProps { + activeReindexJobId: number; + itemId: string; + percentageComplete: number; + numDocumentsWithErrors: number; + onComplete?(numDocumentsWithErrors: number): void; + getStatusPath(itemId: string, activeReindexJobId: number): string; + children(percentageComplete: number, numDocumentsWithErrors: number): JSX.Element; +} + +export const IndexingStatusFetcher: React.FC<IIndexingStatusFetcherProps> = ({ + activeReindexJobId, + children, + getStatusPath, + itemId, + numDocumentsWithErrors, + onComplete, + percentageComplete = 0, +}) => { + const [indexingStatus, setIndexingStatus] = useState({ + numDocumentsWithErrors, + percentageComplete, + }); + const pollingInterval = useRef<number>(); + + useEffect(() => { + pollingInterval.current = window.setInterval(async () => { + try { + const response = await HttpLogic.values.http.get(getStatusPath(itemId, activeReindexJobId)); + if (response.percentageComplete >= 100) { + clearInterval(pollingInterval.current); + } + setIndexingStatus({ + percentageComplete: response.percentageComplete, + numDocumentsWithErrors: response.numDocumentsWithErrors, + }); + if (response.percentageComplete >= 100 && onComplete) { + onComplete(response.numDocumentsWithErrors); + } + } catch (e) { + flashAPIErrors(e); + } + }, 3000); + + return () => { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + }; + }, []); + + return children(indexingStatus.percentageComplete, indexingStatus.numDocumentsWithErrors); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts new file mode 100644 index 0000000000000..3866d1a7199e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface IIndexingStatus { + percentageComplete: number; + numDocumentsWithErrors: number; + activeReindexJobId: number; +}