mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Workplace Search] Add tests for remaining Sources components (#89026)
* Remove history params We already replace the history.push functionality with KibanaLogic.values.navigateToUrl but the history object was still being passed around. * Add org sources container tests * Add tests for source router * Clean up leftover history imports * Add tests for SourcesRouter * Quick refactor for cleaner existence check Optional chaining FTW * Refactor to simplify setInterval logic This commit does a refactor to move the logic for polling for status to the logic file. In doing this I realized that we were intializing sources in the SourcesView, when we are actually already initializing sources in the components that use this, which are OrganizationSources and PrivateSources, the top-level containers. Because of this, I was able to remove the useEffect entireley, as the flash messages are cleared between page transitions in Kibana and the initialization of the sources ahppens in the containers. * Add tests for SourcesView * Fix type issue
This commit is contained in:
parent
c495093f76
commit
4281a347c6
9 changed files with 337 additions and 34 deletions
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 '../../../__mocks__/shallow_useeffect.mock';
|
||||
|
||||
import { setMockValues, setMockActions } from '../../../__mocks__';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { contentSources } from '../../__mocks__/content_sources.mock';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import { SourcesTable } from '../../components/shared/sources_table';
|
||||
import { ViewContentHeader } from '../../components/shared/view_content_header';
|
||||
|
||||
import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes';
|
||||
|
||||
import { OrganizationSources } from './organization_sources';
|
||||
|
||||
describe('OrganizationSources', () => {
|
||||
const initializeSources = jest.fn();
|
||||
const setSourceSearchability = jest.fn();
|
||||
|
||||
const mockValues = {
|
||||
contentSources,
|
||||
dataLoading: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockActions({
|
||||
initializeSources,
|
||||
setSourceSearchability,
|
||||
});
|
||||
setMockValues({ ...mockValues });
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<OrganizationSources />);
|
||||
|
||||
expect(wrapper.find(SourcesTable)).toHaveLength(1);
|
||||
expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns loading when loading', () => {
|
||||
setMockValues({ ...mockValues, dataLoading: true });
|
||||
const wrapper = shallow(<OrganizationSources />);
|
||||
|
||||
expect(wrapper.find(Loading)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns redirect when no sources', () => {
|
||||
setMockValues({ ...mockValues, contentSources: [] });
|
||||
const wrapper = shallow(<OrganizationSources />);
|
||||
|
||||
expect(wrapper.find(Redirect).prop('to')).toEqual(getSourcesPath(ADD_SOURCE_PATH, true));
|
||||
});
|
||||
});
|
|
@ -27,10 +27,11 @@ const ORG_HEADER_DESCRIPTION =
|
|||
'Organization sources are available to the entire organization and can be assigned to specific user groups.';
|
||||
|
||||
export const OrganizationSources: React.FC = () => {
|
||||
const { initializeSources, setSourceSearchability } = useActions(SourcesLogic);
|
||||
const { initializeSources, setSourceSearchability, resetSourcesState } = useActions(SourcesLogic);
|
||||
|
||||
useEffect(() => {
|
||||
initializeSources();
|
||||
return resetSourcesState;
|
||||
}, []);
|
||||
|
||||
const { dataLoading, contentSources } = useValues(SourcesLogic);
|
||||
|
|
|
@ -39,7 +39,7 @@ export interface SourceActions {
|
|||
): { sourceId: string; source: { name: string } };
|
||||
resetSourceState(): void;
|
||||
removeContentSource(sourceId: string): { sourceId: string };
|
||||
initializeSource(sourceId: string, history: object): { sourceId: string; history: object };
|
||||
initializeSource(sourceId: string): { sourceId: string };
|
||||
getSourceConfigData(serviceType: string): { serviceType: string };
|
||||
setButtonNotLoading(): void;
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
|
|||
setSearchResults: (searchResultsResponse: SearchResultsResponse) => searchResultsResponse,
|
||||
setContentFilterValue: (contentFilterValue: string) => contentFilterValue,
|
||||
setActivePage: (activePage: number) => activePage,
|
||||
initializeSource: (sourceId: string, history: object) => ({ sourceId, history }),
|
||||
initializeSource: (sourceId: string) => ({ sourceId }),
|
||||
initializeFederatedSummary: (sourceId: string) => ({ sourceId }),
|
||||
searchContentSourceDocuments: (sourceId: string) => ({ sourceId }),
|
||||
updateContentSource: (sourceId: string, source: { name: string }) => ({ sourceId, source }),
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 '../../../__mocks__/shallow_useeffect.mock';
|
||||
|
||||
import { setMockValues, setMockActions } from '../../../__mocks__';
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { contentSources } from '../../__mocks__/content_sources.mock';
|
||||
|
||||
import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
|
||||
|
||||
import { NAV } from '../../constants';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
|
||||
import { DisplaySettingsRouter } from './components/display_settings';
|
||||
import { Overview } from './components/overview';
|
||||
import { Schema } from './components/schema';
|
||||
import { SchemaChangeErrors } from './components/schema/schema_change_errors';
|
||||
import { SourceContent } from './components/source_content';
|
||||
import { SourceSettings } from './components/source_settings';
|
||||
|
||||
import { SourceRouter } from './source_router';
|
||||
|
||||
describe('SourceRouter', () => {
|
||||
const initializeSource = jest.fn();
|
||||
const contentSource = contentSources[1];
|
||||
const customSource = contentSources[0];
|
||||
const mockValues = {
|
||||
contentSource,
|
||||
dataLoading: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockActions({
|
||||
initializeSource,
|
||||
});
|
||||
setMockValues({ ...mockValues });
|
||||
(useParams as jest.Mock).mockImplementationOnce(() => ({
|
||||
sourceId: '1',
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns Loading when loading', () => {
|
||||
setMockValues({ ...mockValues, dataLoading: true });
|
||||
const wrapper = shallow(<SourceRouter />);
|
||||
|
||||
expect(wrapper.find(Loading)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders source routes (standard)', () => {
|
||||
const wrapper = shallow(<SourceRouter />);
|
||||
|
||||
expect(wrapper.find(Overview)).toHaveLength(1);
|
||||
expect(wrapper.find(SourceSettings)).toHaveLength(1);
|
||||
expect(wrapper.find(SourceContent)).toHaveLength(1);
|
||||
expect(wrapper.find(Switch)).toHaveLength(1);
|
||||
expect(wrapper.find(Route)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('renders source routes (custom)', () => {
|
||||
setMockValues({ ...mockValues, contentSource: customSource });
|
||||
const wrapper = shallow(<SourceRouter />);
|
||||
|
||||
expect(wrapper.find(DisplaySettingsRouter)).toHaveLength(1);
|
||||
expect(wrapper.find(Schema)).toHaveLength(1);
|
||||
expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1);
|
||||
expect(wrapper.find(Route)).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('handles breadcrumbs while loading (standard)', () => {
|
||||
setMockValues({
|
||||
...mockValues,
|
||||
contentSource: {},
|
||||
});
|
||||
|
||||
const loadingBreadcrumbs = ['Sources', '...'];
|
||||
|
||||
const wrapper = shallow(<SourceRouter />);
|
||||
|
||||
const overviewBreadCrumb = wrapper.find(SetPageChrome).at(0);
|
||||
const contentBreadCrumb = wrapper.find(SetPageChrome).at(1);
|
||||
const settingsBreadCrumb = wrapper.find(SetPageChrome).at(2);
|
||||
|
||||
expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.OVERVIEW]);
|
||||
expect(contentBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.CONTENT]);
|
||||
expect(settingsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SETTINGS]);
|
||||
});
|
||||
|
||||
it('handles breadcrumbs while loading (custom)', () => {
|
||||
setMockValues({
|
||||
...mockValues,
|
||||
contentSource: { serviceType: 'custom' },
|
||||
});
|
||||
|
||||
const loadingBreadcrumbs = ['Sources', '...'];
|
||||
|
||||
const wrapper = shallow(<SourceRouter />);
|
||||
|
||||
const schemaBreadCrumb = wrapper.find(SetPageChrome).at(2);
|
||||
const schemaErrorsBreadCrumb = wrapper.find(SetPageChrome).at(3);
|
||||
const displaySettingsBreadCrumb = wrapper.find(SetPageChrome).at(4);
|
||||
|
||||
expect(schemaBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]);
|
||||
expect(schemaErrorsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]);
|
||||
expect(displaySettingsBreadCrumb.prop('trail')).toEqual([
|
||||
...loadingBreadcrumbs,
|
||||
NAV.DISPLAY_SETTINGS,
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -6,10 +6,9 @@
|
|||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { History } from 'history';
|
||||
import { useActions, useValues } from 'kea';
|
||||
import moment from 'moment';
|
||||
import { Route, Switch, useHistory, useParams } from 'react-router-dom';
|
||||
import { Route, Switch, useParams } from 'react-router-dom';
|
||||
|
||||
import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
|
@ -46,14 +45,13 @@ import { SourceInfoCard } from './components/source_info_card';
|
|||
import { SourceSettings } from './components/source_settings';
|
||||
|
||||
export const SourceRouter: React.FC = () => {
|
||||
const history = useHistory() as History;
|
||||
const { sourceId } = useParams() as { sourceId: string };
|
||||
const { initializeSource } = useActions(SourceLogic);
|
||||
const { contentSource, dataLoading } = useValues(SourceLogic);
|
||||
const { isOrganization } = useValues(AppLogic);
|
||||
|
||||
useEffect(() => {
|
||||
initializeSource(sourceId, history);
|
||||
initializeSource(sourceId);
|
||||
}, []);
|
||||
|
||||
if (dataLoading) return <Loading />;
|
||||
|
|
|
@ -77,6 +77,9 @@ interface ISourcesServerResponse {
|
|||
serviceTypes: Connector[];
|
||||
}
|
||||
|
||||
let pollingInterval: number;
|
||||
const POLLING_INTERVAL = 10000;
|
||||
|
||||
export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>({
|
||||
path: ['enterprise_search', 'workplace_search', 'sources_logic'],
|
||||
actions: {
|
||||
|
@ -169,6 +172,7 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
|
|||
|
||||
try {
|
||||
const response = await HttpLogic.values.http.get(route);
|
||||
actions.pollForSourceStatusChanges();
|
||||
actions.onInitializeSources(response);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
|
@ -181,18 +185,20 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
|
|||
}
|
||||
},
|
||||
// We poll the server and if the status update, we trigger a new fetch of the sources.
|
||||
pollForSourceStatusChanges: async () => {
|
||||
pollForSourceStatusChanges: () => {
|
||||
const { isOrganization } = AppLogic.values;
|
||||
if (!isOrganization) return;
|
||||
const serverStatuses = values.serverStatuses;
|
||||
|
||||
const sourceStatuses = await fetchSourceStatuses(isOrganization);
|
||||
pollingInterval = window.setInterval(async () => {
|
||||
const sourceStatuses = await fetchSourceStatuses(isOrganization);
|
||||
|
||||
sourceStatuses.some((source: ContentSourceStatus) => {
|
||||
if (serverStatuses && serverStatuses[source.id] !== source.status.status) {
|
||||
return actions.initializeSources();
|
||||
}
|
||||
});
|
||||
sourceStatuses.some((source: ContentSourceStatus) => {
|
||||
if (serverStatuses && serverStatuses[source.id] !== source.status.status) {
|
||||
return actions.initializeSources();
|
||||
}
|
||||
});
|
||||
}, POLLING_INTERVAL);
|
||||
},
|
||||
setSourceSearchability: async ({ sourceId, searchable }) => {
|
||||
const { isOrganization } = AppLogic.values;
|
||||
|
@ -235,6 +241,14 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
|
|||
resetFlashMessages: () => {
|
||||
clearFlashMessages();
|
||||
},
|
||||
resetSourcesState: () => {
|
||||
clearInterval(pollingInterval);
|
||||
},
|
||||
}),
|
||||
events: () => ({
|
||||
beforeUnmount() {
|
||||
clearInterval(pollingInterval);
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 '../../../__mocks__/shallow_useeffect.mock';
|
||||
|
||||
import { setMockValues, setMockActions } from '../../../__mocks__';
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
|
||||
import { ADD_SOURCE_PATH, PERSONAL_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes';
|
||||
|
||||
import { SourcesRouter } from './sources_router';
|
||||
|
||||
describe('SourcesRouter', () => {
|
||||
const resetSourcesState = jest.fn();
|
||||
const mockValues = {
|
||||
account: { canCreatePersonalSources: true },
|
||||
isOrganization: true,
|
||||
hasPlatinumLicense: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockActions({
|
||||
resetSourcesState,
|
||||
});
|
||||
setMockValues({ ...mockValues });
|
||||
});
|
||||
|
||||
it('renders sources routes', () => {
|
||||
const TOTAL_ROUTES = 62;
|
||||
const wrapper = shallow(<SourcesRouter />);
|
||||
|
||||
expect(wrapper.find(Switch)).toHaveLength(1);
|
||||
expect(wrapper.find(Route)).toHaveLength(TOTAL_ROUTES);
|
||||
});
|
||||
|
||||
it('redirects when nonplatinum license and accountOnly context', () => {
|
||||
setMockValues({ ...mockValues, hasPlatinumLicense: false });
|
||||
const wrapper = shallow(<SourcesRouter />);
|
||||
|
||||
expect(wrapper.find(Redirect).first().prop('from')).toEqual(ADD_SOURCE_PATH);
|
||||
expect(wrapper.find(Redirect).first().prop('to')).toEqual(SOURCES_PATH);
|
||||
});
|
||||
|
||||
it('redirects when cannot create sources', () => {
|
||||
setMockValues({ ...mockValues, account: { canCreatePersonalSources: false } });
|
||||
const wrapper = shallow(<SourcesRouter />);
|
||||
|
||||
expect(wrapper.find(Redirect).last().prop('from')).toEqual(
|
||||
getSourcesPath(ADD_SOURCE_PATH, false)
|
||||
);
|
||||
expect(wrapper.find(Redirect).last().prop('to')).toEqual(PERSONAL_SOURCES_PATH);
|
||||
});
|
||||
});
|
|
@ -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 '../../../__mocks__/shallow_useeffect.mock';
|
||||
|
||||
import { setMockValues, setMockActions } from '../../../__mocks__';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiModal } from '@elastic/eui';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
|
||||
import { SourcesView } from './sources_view';
|
||||
|
||||
describe('SourcesView', () => {
|
||||
const resetPermissionsModal = jest.fn();
|
||||
const permissionsModal = {
|
||||
addedSourceName: 'mySource',
|
||||
serviceType: 'jira',
|
||||
additionalConfiguration: true,
|
||||
};
|
||||
|
||||
const mockValues = {
|
||||
permissionsModal,
|
||||
dataLoading: false,
|
||||
};
|
||||
|
||||
const children = <p data-test-subj="TestChildren">test</p>;
|
||||
|
||||
beforeEach(() => {
|
||||
setMockActions({
|
||||
resetPermissionsModal,
|
||||
});
|
||||
setMockValues({ ...mockValues });
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<SourcesView>{children}</SourcesView>);
|
||||
|
||||
expect(wrapper.find('PermissionsModal')).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns loading when loading', () => {
|
||||
setMockValues({ ...mockValues, dataLoading: true });
|
||||
const wrapper = shallow(<SourcesView>{children}</SourcesView>);
|
||||
|
||||
expect(wrapper.find(Loading)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('calls function on modal close', () => {
|
||||
const wrapper = shallow(<SourcesView>{children}</SourcesView>);
|
||||
const modal = wrapper.find('PermissionsModal').dive().find(EuiModal);
|
||||
modal.prop('onClose')();
|
||||
|
||||
expect(resetPermissionsModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
|
@ -22,8 +22,6 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { clearFlashMessages } from '../../../shared/flash_messages';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import { SourceIcon } from '../../components/shared/source_icon';
|
||||
|
||||
|
@ -31,29 +29,14 @@ import { EXTERNAL_IDENTITIES_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL } from '../
|
|||
|
||||
import { SourcesLogic } from './sources_logic';
|
||||
|
||||
const POLLING_INTERVAL = 10000;
|
||||
|
||||
interface SourcesViewProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SourcesView: React.FC<SourcesViewProps> = ({ children }) => {
|
||||
const { initializeSources, pollForSourceStatusChanges, resetPermissionsModal } = useActions(
|
||||
SourcesLogic
|
||||
);
|
||||
|
||||
const { resetPermissionsModal } = useActions(SourcesLogic);
|
||||
const { dataLoading, permissionsModal } = useValues(SourcesLogic);
|
||||
|
||||
useEffect(() => {
|
||||
initializeSources();
|
||||
const pollingInterval = window.setInterval(pollForSourceStatusChanges, POLLING_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearFlashMessages();
|
||||
clearInterval(pollingInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (dataLoading) return <Loading />;
|
||||
|
||||
const PermissionsModal = ({
|
||||
|
@ -113,7 +96,7 @@ export const SourcesView: React.FC<SourcesViewProps> = ({ children }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{!!permissionsModal && permissionsModal.additionalConfiguration && (
|
||||
{permissionsModal?.additionalConfiguration && (
|
||||
<PermissionsModal
|
||||
addedSourceName={permissionsModal.addedSourceName}
|
||||
serviceType={permissionsModal.serviceType}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue