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;
+}