[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:
parent
88359b742a
commit
b6913a3d2e
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
}),
|
||||
});
|
Loading…
Reference in a new issue