[Enterprise Search] Convert IndexingStatus to use logic for fetching (#84710)

* Add IndexingStatusLogic

* Replace IndexingStatusFetcher with logic

* Refactor out unnecessary conditional

onComplete is not optional so these if blocks can be consolidated

* Misc styling - destructuring and typing

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Misc styling - imports

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Remove div

* Refactor test

* Replace method with string for statusPath

In ent-search, we use Rails helpers to generate paths. These were in the form of routes.whateverPath(). We passed these method to the IndexingStatus component to generate the app-specific rotues in the shared component.

In Kibana, we will not have these generators and should instead pass the path strings directly

Co-authored-by: Constance <constancecchen@users.noreply.github.com>
This commit is contained in:
Scotty Bollinger 2020-12-02 13:34:07 -06:00 committed by GitHub
parent 88359b742a
commit b6913a3d2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 249 additions and 101 deletions

View file

@ -4,6 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import '../../__mocks__/kea.mock';
import '../../__mocks__/shallow_useeffect.mock';
import { setMockActions, setMockValues } from '../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
@ -11,41 +16,49 @@ 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 fetchIndexingStatus = jest.fn();
const props = {
percentageComplete: 50,
numDocumentsWithErrors: 1,
activeReindexJobId: 12,
viewLinkPath: '/path',
statusPath: '/other_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
);
beforeEach(() => {
setMockActions({ fetchIndexingStatus });
});
expect(shallow(fetcher).find(EuiPanel)).toHaveLength(1);
expect(shallow(fetcher).find(IndexingStatusContent)).toHaveLength(1);
it('renders', () => {
setMockValues({
percentageComplete: 50,
numDocumentsWithErrors: 0,
});
const wrapper = shallow(<IndexingStatus {...props} />);
expect(wrapper.find(EuiPanel)).toHaveLength(1);
expect(wrapper.find(IndexingStatusContent)).toHaveLength(1);
expect(fetchIndexingStatus).toHaveBeenCalled();
});
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);
setMockValues({
percentageComplete: 100,
numDocumentsWithErrors: 1,
});
const wrapper = shallow(<IndexingStatus {...props} />);
expect(wrapper.find(IndexingStatusErrors)).toHaveLength(1);
});
});

View file

@ -4,41 +4,52 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
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 { IndexingStatusLogic } from './indexing_status_logic';
import { IIndexingStatus } from '../types';
export interface IIndexingStatusProps extends IIndexingStatus {
export interface IIndexingStatusProps {
viewLinkPath: string;
itemId: string;
statusPath: 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>
);
export const IndexingStatus: React.FC<IIndexingStatusProps> = ({
viewLinkPath,
statusPath,
onComplete,
}) => {
const { percentageComplete, numDocumentsWithErrors } = useValues(IndexingStatusLogic);
const { fetchIndexingStatus } = useActions(IndexingStatusLogic);
useEffect(() => {
fetchIndexingStatus({ statusPath, onComplete });
}, []);
return (
<>
{percentageComplete < 100 && (
<EuiPanel paddingSize="l" hasShadow={true}>
<IndexingStatusContent percentageComplete={percentageComplete} />
</EuiPanel>
)}
{percentageComplete === 100 && numDocumentsWithErrors > 0 && (
<>
<EuiSpacer />
<IndexingStatusErrors viewLinkPath={viewLinkPath} />
</>
)}
</>
);
};

View file

@ -1,64 +0,0 @@
/*
* 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);
};

View file

@ -0,0 +1,110 @@
/*
* 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 { resetContext } from 'kea';
jest.mock('../http', () => ({
HttpLogic: {
values: { http: { get: jest.fn() } },
},
}));
import { HttpLogic } from '../http';
jest.mock('../flash_messages', () => ({
flashAPIErrors: jest.fn(),
}));
import { flashAPIErrors } from '../flash_messages';
import { IndexingStatusLogic } from './indexing_status_logic';
describe('IndexingStatusLogic', () => {
let unmount: any;
const mockStatusResponse = {
percentageComplete: 50,
numDocumentsWithErrors: 3,
activeReindexJobId: 1,
};
beforeEach(() => {
jest.clearAllMocks();
resetContext({});
unmount = IndexingStatusLogic.mount();
});
it('has expected default values', () => {
expect(IndexingStatusLogic.values).toEqual({
percentageComplete: 100,
numDocumentsWithErrors: 0,
});
});
describe('setIndexingStatus', () => {
it('sets reducers', () => {
IndexingStatusLogic.actions.setIndexingStatus(mockStatusResponse);
expect(IndexingStatusLogic.values.percentageComplete).toEqual(
mockStatusResponse.percentageComplete
);
expect(IndexingStatusLogic.values.numDocumentsWithErrors).toEqual(
mockStatusResponse.numDocumentsWithErrors
);
});
});
describe('fetchIndexingStatus', () => {
jest.useFakeTimers();
const statusPath = '/api/workplace_search/path/123';
const onComplete = jest.fn();
const TIMEOUT = 3000;
it('calls API and sets values', async () => {
const setIndexingStatusSpy = jest.spyOn(IndexingStatusLogic.actions, 'setIndexingStatus');
const promise = Promise.resolve(mockStatusResponse);
(HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete });
jest.advanceTimersByTime(TIMEOUT);
expect(HttpLogic.values.http.get).toHaveBeenCalledWith(statusPath);
await promise;
expect(setIndexingStatusSpy).toHaveBeenCalledWith(mockStatusResponse);
});
it('handles error', async () => {
const promise = Promise.reject('An error occured');
(HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete });
jest.advanceTimersByTime(TIMEOUT);
try {
await promise;
} catch {
// Do nothing
}
expect(flashAPIErrors).toHaveBeenCalledWith('An error occured');
});
it('handles indexing complete state', async () => {
const promise = Promise.resolve({ ...mockStatusResponse, percentageComplete: 100 });
(HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise);
IndexingStatusLogic.actions.fetchIndexingStatus({ statusPath, onComplete });
jest.advanceTimersByTime(TIMEOUT);
await promise;
expect(clearInterval).toHaveBeenCalled();
expect(onComplete).toHaveBeenCalledWith(mockStatusResponse.numDocumentsWithErrors);
});
it('handles unmounting', async () => {
unmount();
expect(clearInterval).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,78 @@
/*
* 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 { kea, MakeLogicType } from 'kea';
import { HttpLogic } from '../http';
import { IIndexingStatus } from '../types';
import { flashAPIErrors } from '../flash_messages';
interface IndexingStatusProps {
statusPath: string;
onComplete(numDocumentsWithErrors: number): void;
}
interface IndexingStatusActions {
fetchIndexingStatus(props: IndexingStatusProps): IndexingStatusProps;
setIndexingStatus({
percentageComplete,
numDocumentsWithErrors,
}: IIndexingStatus): IIndexingStatus;
}
interface IndexingStatusValues {
percentageComplete: number;
numDocumentsWithErrors: number;
}
let pollingInterval: number;
export const IndexingStatusLogic = kea<MakeLogicType<IndexingStatusValues, IndexingStatusActions>>({
actions: {
fetchIndexingStatus: ({ statusPath, onComplete }) => ({ statusPath, onComplete }),
setIndexingStatus: ({ numDocumentsWithErrors, percentageComplete }) => ({
numDocumentsWithErrors,
percentageComplete,
}),
},
reducers: {
percentageComplete: [
100,
{
setIndexingStatus: (_, { percentageComplete }) => percentageComplete,
},
],
numDocumentsWithErrors: [
0,
{
setIndexingStatus: (_, { numDocumentsWithErrors }) => numDocumentsWithErrors,
},
],
},
listeners: ({ actions }) => ({
fetchIndexingStatus: ({ statusPath, onComplete }: IndexingStatusProps) => {
const { http } = HttpLogic.values;
pollingInterval = window.setInterval(async () => {
try {
const response: IIndexingStatus = await http.get(statusPath);
if (response.percentageComplete >= 100) {
clearInterval(pollingInterval);
onComplete(response.numDocumentsWithErrors);
}
actions.setIndexingStatus(response);
} catch (e) {
flashAPIErrors(e);
}
}, 3000);
},
}),
events: () => ({
beforeUnmount() {
clearInterval(pollingInterval);
},
}),
});