Migrate security page (#89720)
* Add server routes for Workplace Search Security page * Initial copy/paste of component tree Also update lodash imports and fix default exports * Update paths * Remove conditional and passed in flash messages This is no longer needed with the Kibana syntax. Flash messages are set globally and only render when present. * Replace removed ConfirmModal In Kibana, we use the Eui components directly * Remove legacy AppView and sidenav * Clear flash messages globally * Update server routes * Replace Rails http with kibana http * Add setSourceRestriction action to app_logic It is used in security_logic * Add missing typings * Add route and update nav * Use internal tools for determining license * Remove Prompt as it doesn't work in Kibana There is an error that recommends using AppMountParameters.onAppLeave instead, but it doesn't cover the case where a user navigates within the app. We'll revisit this problem later. * Add i18n Also refactor PrivateSourcesTable to use static i18n strings. Before we were using 'remote' and 'standard' as both enums and parts of copy, i.e. "Enable {sourceType} private sources". But with i18n we can no longer do this. So I made a refactoring to separate these concerns. Now 'remote' and 'standard' are only used as enums. What i18n string to show is defined based on isRemote variable. * Add components unit tests * Add logic unit tests * Remove redundant imports * Use nextTick instead of awaiting for promises * Update logic tests to use new mockHelpers
This commit is contained in:
parent
4e18fd8a51
commit
e866db7de0
|
@ -21,6 +21,7 @@ interface AppValues extends WorkplaceSearchInitialData {
|
|||
interface AppActions {
|
||||
initializeAppData(props: InitialAppData): InitialAppData;
|
||||
setContext(isOrganization: boolean): boolean;
|
||||
setSourceRestriction(canCreatePersonalSources: boolean): boolean;
|
||||
}
|
||||
|
||||
const emptyOrg = {} as Organization;
|
||||
|
@ -34,6 +35,7 @@ export const AppLogic = kea<MakeLogicType<AppValues, AppActions>>({
|
|||
isFederatedAuth,
|
||||
}),
|
||||
setContext: (isOrganization) => isOrganization,
|
||||
setSourceRestriction: (canCreatePersonalSources: boolean) => canCreatePersonalSources,
|
||||
},
|
||||
reducers: {
|
||||
hasInitialized: [
|
||||
|
@ -64,6 +66,10 @@ export const AppLogic = kea<MakeLogicType<AppValues, AppActions>>({
|
|||
emptyAccount,
|
||||
{
|
||||
initializeAppData: (_, { workplaceSearch }) => workplaceSearch?.account || emptyAccount,
|
||||
setSourceRestriction: (state, canCreatePersonalSources) => ({
|
||||
...state,
|
||||
canCreatePersonalSources,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -45,9 +45,7 @@ export const WorkplaceSearchNav: React.FC<Props> = ({
|
|||
<SideNavLink isExternal to={getWorkplaceSearchUrl(`#${ROLE_MAPPINGS_PATH}`)}>
|
||||
{NAV.ROLE_MAPPINGS}
|
||||
</SideNavLink>
|
||||
<SideNavLink isExternal to={getWorkplaceSearchUrl(`#${SECURITY_PATH}`)}>
|
||||
{NAV.SECURITY}
|
||||
</SideNavLink>
|
||||
<SideNavLink to={SECURITY_PATH}>{NAV.SECURITY}</SideNavLink>
|
||||
<SideNavLink subNav={settingsSubNav} to={ORG_SETTINGS_PATH}>
|
||||
{NAV.SETTINGS}
|
||||
</SideNavLink>
|
||||
|
|
|
@ -289,6 +289,87 @@ export const DOCUMENTATION_LINK_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PRIVATE_SOURCES_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.privateSources.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Private sources are connected by users in your organization to create a personalized search experience.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PRIVATE_SOURCES_TOGGLE_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.privateSourcesToggle.description',
|
||||
{
|
||||
defaultMessage: 'Enable private sources for your organization',
|
||||
}
|
||||
);
|
||||
|
||||
export const REMOTE_SOURCES_TOGGLE_TEXT = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesToggle.text',
|
||||
{
|
||||
defaultMessage: 'Enable remote private sources',
|
||||
}
|
||||
);
|
||||
|
||||
export const REMOTE_SOURCES_TABLE_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesTable.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Remote sources synchronize and store a limited amount of data on disk, with a low impact on storage resources.',
|
||||
}
|
||||
);
|
||||
|
||||
export const REMOTE_SOURCES_EMPTY_TABLE_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesEmptyTable.title',
|
||||
{
|
||||
defaultMessage: 'No remote private sources configured yet',
|
||||
}
|
||||
);
|
||||
|
||||
export const STANDARD_SOURCES_TOGGLE_TEXT = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.standardSourcesToggle.text',
|
||||
{
|
||||
defaultMessage: 'Enable standard private sources',
|
||||
}
|
||||
);
|
||||
|
||||
export const STANDARD_SOURCES_TABLE_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.standardSourcesTable.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Standard sources synchronize and store all searchable data on disk, with a directly correlated impact on storage resources.',
|
||||
}
|
||||
);
|
||||
|
||||
export const STANDARD_SOURCES_EMPTY_TABLE_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.standardSourcesEmptyTable.title',
|
||||
{
|
||||
defaultMessage: 'No standard private sources configured yet',
|
||||
}
|
||||
);
|
||||
|
||||
export const SECURITY_UNSAVED_CHANGES_MESSAGE = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.unsavedChanges.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'Your private sources settings have not been saved. Are you sure you want to leave?',
|
||||
}
|
||||
);
|
||||
|
||||
export const PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.privateSourcesUpdateConfirmation.text',
|
||||
{
|
||||
defaultMessage: 'Updates to private source configuration will take effect immediately.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_RESTRICTIONS_SUCCESS_MESSAGE = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.sourceRestrictionsSuccess.message',
|
||||
{
|
||||
defaultMessage: 'Successfully updated source restrictions.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PUBLIC_KEY_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.publicKey.label',
|
||||
{
|
||||
|
@ -382,6 +463,20 @@ export const SAVE_CHANGES_BUTTON = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SAVE_SETTINGS_BUTTON = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.saveSettings.button',
|
||||
{
|
||||
defaultMessage: 'Save settings',
|
||||
}
|
||||
);
|
||||
|
||||
export const KEEP_EDITING_BUTTON = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.keepEditing.button',
|
||||
{
|
||||
defaultMessage: 'Keep editing',
|
||||
}
|
||||
);
|
||||
|
||||
export const NAME_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.name.label', {
|
||||
defaultMessage: 'Name',
|
||||
});
|
||||
|
@ -493,6 +588,10 @@ export const UPDATE_BUTTON = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const RESET_BUTTON = i18n.translate('xpack.enterpriseSearch.workplaceSearch.reset.button', {
|
||||
defaultMessage: 'Reset',
|
||||
});
|
||||
|
||||
export const CONFIGURE_BUTTON = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.configure.button',
|
||||
{
|
||||
|
@ -522,6 +621,10 @@ export const PRIVATE_PLATINUM_LICENSE_CALLOUT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SOURCE = i18n.translate('xpack.enterpriseSearch.workplaceSearch.source.text', {
|
||||
defaultMessage: 'Source',
|
||||
});
|
||||
|
||||
export const PRIVATE_SOURCE = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.privateSource.text',
|
||||
{
|
||||
|
@ -529,6 +632,20 @@ export const PRIVATE_SOURCE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PRIVATE_SOURCES = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.privateSources.text',
|
||||
{
|
||||
defaultMessage: 'Private Sources',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONFIRM_CHANGES_TEXT = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.confirmChanges.text',
|
||||
{
|
||||
defaultMessage: 'Confirm changes',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONNECTORS_HEADER_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.connectors.header.title',
|
||||
{
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
SOURCES_PATH,
|
||||
PERSONAL_SOURCES_PATH,
|
||||
ORG_SETTINGS_PATH,
|
||||
SECURITY_PATH,
|
||||
} from './routes';
|
||||
|
||||
import { SetupGuide } from './views/setup_guide';
|
||||
|
@ -29,6 +30,7 @@ import { ErrorState } from './views/error_state';
|
|||
import { NotFound } from '../shared/not_found';
|
||||
import { Overview } from './views/overview';
|
||||
import { GroupsRouter } from './views/groups';
|
||||
import { Security } from './views/security';
|
||||
import { SourcesRouter } from './views/content_sources';
|
||||
import { SettingsRouter } from './views/settings';
|
||||
|
||||
|
@ -102,6 +104,11 @@ export const WorkplaceSearchConfigured: React.FC<InitialAppData> = (props) => {
|
|||
<GroupsRouter />
|
||||
</Layout>
|
||||
</Route>
|
||||
<Route path={SECURITY_PATH}>
|
||||
<Layout navigation={<WorkplaceSearchNav />} restrictWidth readOnlyMode={readOnlyMode}>
|
||||
<Security />
|
||||
</Layout>
|
||||
</Route>
|
||||
<Route path={ORG_SETTINGS_PATH}>
|
||||
<Layout
|
||||
navigation={<WorkplaceSearchNav settingsSubNav={<SettingsSubNav />} />}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { setMockValues } from '../../../../__mocks__';
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { EuiSwitch } from '@elastic/eui';
|
||||
|
||||
import { PrivateSourcesTable } from './private_sources_table';
|
||||
|
||||
describe('PrivateSourcesTable', () => {
|
||||
beforeEach(() => {
|
||||
setMockValues({ hasPlatinumLicense: true, isEnabled: true });
|
||||
});
|
||||
|
||||
const props = {
|
||||
sourceSection: { isEnabled: true, contentSources: [] },
|
||||
updateSource: jest.fn(),
|
||||
updateEnabled: jest.fn(),
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<PrivateSourcesTable {...props} sourceType="standard" />);
|
||||
|
||||
expect(wrapper.find(EuiSwitch)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles switches clicks', () => {
|
||||
const wrapper = shallow(
|
||||
<PrivateSourcesTable
|
||||
{...props}
|
||||
sourceSection={{
|
||||
isEnabled: false,
|
||||
contentSources: [{ id: 'gmail', isEnabled: true, name: 'Gmail' }],
|
||||
}}
|
||||
sourceType="remote"
|
||||
/>
|
||||
);
|
||||
|
||||
const sectionSwitch = wrapper.find(EuiSwitch).first();
|
||||
const sourceSwitch = wrapper.find(EuiSwitch).last();
|
||||
|
||||
const event = { target: { value: true } };
|
||||
sectionSwitch.prop('onChange')(event as any);
|
||||
sourceSwitch.prop('onChange')(event as any);
|
||||
|
||||
expect(props.updateEnabled).toHaveBeenCalled();
|
||||
expect(props.updateSource).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiTable,
|
||||
EuiTableBody,
|
||||
EuiTableHeader,
|
||||
EuiTableHeaderCell,
|
||||
EuiTableRow,
|
||||
EuiTableRowCell,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { LicensingLogic } from '../../../../shared/licensing';
|
||||
import { SecurityLogic, PrivateSourceSection } from '../security_logic';
|
||||
import {
|
||||
REMOTE_SOURCES_TOGGLE_TEXT,
|
||||
REMOTE_SOURCES_TABLE_DESCRIPTION,
|
||||
REMOTE_SOURCES_EMPTY_TABLE_TITLE,
|
||||
STANDARD_SOURCES_TOGGLE_TEXT,
|
||||
STANDARD_SOURCES_TABLE_DESCRIPTION,
|
||||
STANDARD_SOURCES_EMPTY_TABLE_TITLE,
|
||||
SOURCE,
|
||||
} from '../../../constants';
|
||||
|
||||
interface PrivateSourcesTableProps {
|
||||
sourceType: 'remote' | 'standard';
|
||||
sourceSection: PrivateSourceSection;
|
||||
updateSource(sourceId: string, isEnabled: boolean): void;
|
||||
updateEnabled(isEnabled: boolean): void;
|
||||
}
|
||||
|
||||
const REMOTE_SOURCES_EMPTY_TABLE_DESCRIPTION = (
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesEmptyTable.description"
|
||||
defaultMessage="Once configured, remote private sources are {enabledStrong}, and users can immediately connect the source from their Personal Dashboard."
|
||||
values={{
|
||||
enabledStrong: (
|
||||
<strong>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.remoteSourcesEmptyTable.enabledStrong',
|
||||
{ defaultMessage: 'enabled by default' }
|
||||
)}
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const STANDARD_SOURCES_EMPTY_TABLE_DESCRIPTION = (
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.workplaceSearch.security.standardSourcesEmptyTable.description"
|
||||
defaultMessage="Once configured, standard private sources are {notEnabledStrong}, and must be activated before users are allowed to connect the source from their Personal Dashboard."
|
||||
values={{
|
||||
notEnabledStrong: (
|
||||
<strong>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.security.standardSourcesEmptyTable.notEnabledStrong',
|
||||
{ defaultMessage: 'not enabled by default' }
|
||||
)}
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const PrivateSourcesTable: React.FC<PrivateSourcesTableProps> = ({
|
||||
sourceType,
|
||||
sourceSection: { isEnabled: sectionEnabled, contentSources },
|
||||
updateSource,
|
||||
updateEnabled,
|
||||
}) => {
|
||||
const { hasPlatinumLicense } = useValues(LicensingLogic);
|
||||
const { isEnabled } = useValues(SecurityLogic);
|
||||
|
||||
const isRemote = sourceType === 'remote';
|
||||
const hasSources = contentSources.length > 0;
|
||||
const panelDisabled = !isEnabled || !hasPlatinumLicense;
|
||||
const sectionDisabled = !sectionEnabled;
|
||||
|
||||
const panelClass = classNames('euiPanel--outline euiPanel--noShadow', {
|
||||
'euiPanel--disabled': panelDisabled,
|
||||
});
|
||||
|
||||
const tableClass = classNames({ 'euiTable--disabled': sectionDisabled });
|
||||
|
||||
const emptyState = (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiPanel className="euiPanel--inset euiPanel--noShadow euiPanel--outline">
|
||||
<EuiText textAlign="center" color="subdued" size="s">
|
||||
<strong>
|
||||
{isRemote ? REMOTE_SOURCES_EMPTY_TABLE_TITLE : STANDARD_SOURCES_EMPTY_TABLE_TITLE}
|
||||
</strong>
|
||||
</EuiText>
|
||||
<EuiText textAlign="center" color="subdued" size="s">
|
||||
{isRemote
|
||||
? REMOTE_SOURCES_EMPTY_TABLE_DESCRIPTION
|
||||
: STANDARD_SOURCES_EMPTY_TABLE_DESCRIPTION}
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
|
||||
const sectionHeading = (
|
||||
<EuiFlexGroup alignItems="flexStart" justifyContent="flexStart" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiSwitch
|
||||
checked={sectionEnabled}
|
||||
onChange={(e) => updateEnabled(e.target.checked)}
|
||||
disabled={!isEnabled || !hasPlatinumLicense}
|
||||
showLabel={false}
|
||||
label={`${sourceType} Sources Toggle`}
|
||||
data-test-subj={`${sourceType}EnabledToggle`}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{isRemote ? REMOTE_SOURCES_TOGGLE_TEXT : STANDARD_SOURCES_TOGGLE_TEXT}</h4>
|
||||
</EuiText>
|
||||
<EuiText color="subdued" size="s">
|
||||
{isRemote ? REMOTE_SOURCES_TABLE_DESCRIPTION : STANDARD_SOURCES_TABLE_DESCRIPTION}
|
||||
</EuiText>
|
||||
{!hasSources && emptyState}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const sourcesTable = (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiTable className={tableClass}>
|
||||
<EuiTableHeader>
|
||||
<EuiTableHeaderCell>{SOURCE}</EuiTableHeaderCell>
|
||||
<EuiTableHeaderCell />
|
||||
</EuiTableHeader>
|
||||
<EuiTableBody>
|
||||
{contentSources.map((source, i) => (
|
||||
<EuiTableRow key={i}>
|
||||
<EuiTableRowCell>{source.name}</EuiTableRowCell>
|
||||
<EuiTableRowCell>
|
||||
<EuiSwitch
|
||||
checked={!!source.isEnabled}
|
||||
disabled={sectionDisabled}
|
||||
onChange={(e) => updateSource(source.id, e.target.checked)}
|
||||
showLabel={false}
|
||||
label={`${source.name} Toggle`}
|
||||
data-test-subj={`${sourceType}SourceToggle`}
|
||||
compressed
|
||||
/>
|
||||
</EuiTableRowCell>
|
||||
</EuiTableRow>
|
||||
))}
|
||||
</EuiTableBody>
|
||||
</EuiTable>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel className={panelClass}>
|
||||
{sectionHeading}
|
||||
{hasSources && sourcesTable}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -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 { Security } from './security';
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 { setMockValues, setMockActions } from '../../../__mocks__';
|
||||
import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock';
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { EuiSwitch, EuiConfirmModal } from '@elastic/eui';
|
||||
import { Loading } from '../../../shared/loading';
|
||||
|
||||
import { ViewContentHeader } from '../../components/shared/view_content_header';
|
||||
import { Security } from './security';
|
||||
|
||||
describe('Security', () => {
|
||||
const initializeSourceRestrictions = jest.fn();
|
||||
const updatePrivateSourcesEnabled = jest.fn();
|
||||
const updateRemoteEnabled = jest.fn();
|
||||
const updateRemoteSource = jest.fn();
|
||||
const updateStandardEnabled = jest.fn();
|
||||
const updateStandardSource = jest.fn();
|
||||
const saveSourceRestrictions = jest.fn();
|
||||
const resetState = jest.fn();
|
||||
|
||||
const mockValues = {
|
||||
isEnabled: true,
|
||||
remote: { isEnabled: true, contentSources: [] },
|
||||
standard: { isEnabled: true, contentSources: [] },
|
||||
dataLoading: false,
|
||||
unsavedChanges: false,
|
||||
hasPlatinumLicense: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockValues(mockValues);
|
||||
setMockActions({
|
||||
initializeSourceRestrictions,
|
||||
updatePrivateSourcesEnabled,
|
||||
updateRemoteEnabled,
|
||||
updateRemoteSource,
|
||||
updateStandardEnabled,
|
||||
updateStandardSource,
|
||||
saveSourceRestrictions,
|
||||
resetState,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders on Basic license', () => {
|
||||
setMockValues({ ...mockValues, hasPlatinumLicense: false });
|
||||
const wrapper = shallow(<Security />);
|
||||
|
||||
expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiSwitch).prop('disabled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('renders on Platinum license', () => {
|
||||
const wrapper = shallow(<Security />);
|
||||
|
||||
expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiSwitch).prop('disabled')).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns Loading when loading', () => {
|
||||
setMockValues({ ...mockValues, dataLoading: true });
|
||||
const wrapper = shallow(<Security />);
|
||||
|
||||
expect(wrapper.find(Loading)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles window.onbeforeunload change', () => {
|
||||
setMockValues({ ...mockValues, unsavedChanges: true });
|
||||
shallow(<Security />);
|
||||
|
||||
expect(window.onbeforeunload!({} as any)).toEqual(
|
||||
'Your private sources settings have not been saved. Are you sure you want to leave?'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles window.onbeforeunload unmount', () => {
|
||||
setMockValues({ ...mockValues, unsavedChanges: true });
|
||||
shallow(<Security />);
|
||||
|
||||
unmountHandler();
|
||||
|
||||
expect(window.onbeforeunload).toEqual(null);
|
||||
});
|
||||
|
||||
it('handles switch click', () => {
|
||||
const wrapper = shallow(<Security />);
|
||||
|
||||
const privateSourcesSwitch = wrapper.find(EuiSwitch);
|
||||
const event = { target: { checked: true } };
|
||||
privateSourcesSwitch.prop('onChange')(event as any);
|
||||
|
||||
expect(updatePrivateSourcesEnabled).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles confirmModal submission', () => {
|
||||
setMockValues({ ...mockValues, unsavedChanges: true });
|
||||
const wrapper = shallow(<Security />);
|
||||
|
||||
const header = wrapper.find(ViewContentHeader).dive();
|
||||
header.find('[data-test-subj="SaveSettingsButton"]').prop('onClick')!({} as any);
|
||||
const modal = wrapper.find(EuiConfirmModal);
|
||||
modal.prop('onConfirm')!({} as any);
|
||||
|
||||
expect(saveSourceRestrictions).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiPanel,
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { LicensingLogic } from '../../../shared/licensing';
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
import { LicenseCallout } from '../../components/shared/license_callout';
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import { ViewContentHeader } from '../../components/shared/view_content_header';
|
||||
import { SecurityLogic } from './security_logic';
|
||||
|
||||
import { PrivateSourcesTable } from './components/private_sources_table';
|
||||
|
||||
import {
|
||||
SECURITY_UNSAVED_CHANGES_MESSAGE,
|
||||
RESET_BUTTON,
|
||||
SAVE_SETTINGS_BUTTON,
|
||||
SAVE_CHANGES_BUTTON,
|
||||
KEEP_EDITING_BUTTON,
|
||||
PRIVATE_SOURCES,
|
||||
PRIVATE_SOURCES_DESCRIPTION,
|
||||
PRIVATE_SOURCES_TOGGLE_DESCRIPTION,
|
||||
PRIVATE_PLATINUM_LICENSE_CALLOUT,
|
||||
CONFIRM_CHANGES_TEXT,
|
||||
PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT,
|
||||
} from '../../constants';
|
||||
|
||||
export const Security: React.FC = () => {
|
||||
const [confirmModalVisible, setConfirmModalVisibility] = useState(false);
|
||||
|
||||
const hideConfirmModal = () => setConfirmModalVisibility(false);
|
||||
const showConfirmModal = () => setConfirmModalVisibility(true);
|
||||
|
||||
const { hasPlatinumLicense } = useValues(LicensingLogic);
|
||||
|
||||
const {
|
||||
initializeSourceRestrictions,
|
||||
updatePrivateSourcesEnabled,
|
||||
updateRemoteEnabled,
|
||||
updateRemoteSource,
|
||||
updateStandardEnabled,
|
||||
updateStandardSource,
|
||||
saveSourceRestrictions,
|
||||
resetState,
|
||||
} = useActions(SecurityLogic);
|
||||
|
||||
const { isEnabled, remote, standard, dataLoading, unsavedChanges } = useValues(SecurityLogic);
|
||||
|
||||
useEffect(() => {
|
||||
initializeSourceRestrictions();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.onbeforeunload = unsavedChanges ? () => SECURITY_UNSAVED_CHANGES_MESSAGE : null;
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, [unsavedChanges]);
|
||||
|
||||
if (dataLoading) return <Loading />;
|
||||
|
||||
const panelClass = classNames('euiPanel--noShadow', {
|
||||
'euiPanel--disabled': !hasPlatinumLicense,
|
||||
});
|
||||
|
||||
const savePrivateSources = () => {
|
||||
saveSourceRestrictions();
|
||||
hideConfirmModal();
|
||||
};
|
||||
|
||||
const headerActions = (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexStart" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty disabled={!unsavedChanges} onClick={resetState}>
|
||||
{RESET_BUTTON}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
disabled={!hasPlatinumLicense || !unsavedChanges}
|
||||
onClick={showConfirmModal}
|
||||
fill
|
||||
data-test-subj="SaveSettingsButton"
|
||||
>
|
||||
{SAVE_SETTINGS_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const header = (
|
||||
<>
|
||||
<ViewContentHeader
|
||||
title={PRIVATE_SOURCES}
|
||||
alignItems="flexStart"
|
||||
description={PRIVATE_SOURCES_DESCRIPTION}
|
||||
action={headerActions}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
|
||||
const allSourcesToggle = (
|
||||
<EuiPanel paddingSize="none" className={panelClass}>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexStart" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
checked={isEnabled}
|
||||
onChange={(e) => updatePrivateSourcesEnabled(e.target.checked)}
|
||||
disabled={!hasPlatinumLicense}
|
||||
showLabel={false}
|
||||
label="Private Sources Toggle"
|
||||
data-test-subj="PrivateSourcesToggle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{PRIVATE_SOURCES_TOGGLE_DESCRIPTION}</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
const platinumLicenseCallout = (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<LicenseCallout message={PRIVATE_PLATINUM_LICENSE_CALLOUT} />
|
||||
</>
|
||||
);
|
||||
|
||||
const sourceTables = (
|
||||
<>
|
||||
<EuiSpacer size="xl" />
|
||||
<PrivateSourcesTable
|
||||
sourceType="remote"
|
||||
sourceSection={remote}
|
||||
updateEnabled={updateRemoteEnabled}
|
||||
updateSource={updateRemoteSource}
|
||||
/>
|
||||
<EuiSpacer size="xxl" />
|
||||
<PrivateSourcesTable
|
||||
sourceType="standard"
|
||||
sourceSection={standard}
|
||||
updateEnabled={updateStandardEnabled}
|
||||
updateSource={updateStandardSource}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const confirmModal = (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={CONFIRM_CHANGES_TEXT}
|
||||
onConfirm={savePrivateSources}
|
||||
onCancel={hideConfirmModal}
|
||||
buttonColor="primary"
|
||||
cancelButtonText={KEEP_EDITING_BUTTON}
|
||||
confirmButtonText={SAVE_CHANGES_BUTTON}
|
||||
>
|
||||
{PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlashMessages />
|
||||
{header}
|
||||
{allSourcesToggle}
|
||||
{!hasPlatinumLicense && platinumLicenseCallout}
|
||||
{sourceTables}
|
||||
{confirmModalVisible && confirmModal}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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 { LogicMounter } from '../../../__mocks__/kea.mock';
|
||||
import { mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__';
|
||||
import { SecurityLogic } from './security_logic';
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
|
||||
describe('SecurityLogic', () => {
|
||||
const { http } = mockHttpValues;
|
||||
const { flashAPIErrors } = mockFlashMessageHelpers;
|
||||
const { mount } = new LogicMounter(SecurityLogic);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mount();
|
||||
});
|
||||
|
||||
const defaultValues = {
|
||||
dataLoading: true,
|
||||
cachedServerState: {},
|
||||
isEnabled: false,
|
||||
remote: {},
|
||||
standard: {},
|
||||
unsavedChanges: true,
|
||||
};
|
||||
|
||||
const serverProps = {
|
||||
isEnabled: true,
|
||||
remote: {
|
||||
isEnabled: true,
|
||||
contentSources: [{ id: 'gmail', name: 'Gmail', isEnabled: true }],
|
||||
},
|
||||
standard: {
|
||||
isEnabled: true,
|
||||
contentSources: [{ id: 'one_drive', name: 'OneDrive', isEnabled: true }],
|
||||
},
|
||||
};
|
||||
|
||||
it('has expected default values', () => {
|
||||
expect(SecurityLogic.values).toEqual(defaultValues);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('setServerProps', () => {
|
||||
SecurityLogic.actions.setServerProps(serverProps);
|
||||
|
||||
expect(SecurityLogic.values.isEnabled).toEqual(true);
|
||||
});
|
||||
|
||||
it('setSourceRestrictionsUpdated', () => {
|
||||
SecurityLogic.actions.setSourceRestrictionsUpdated(serverProps);
|
||||
|
||||
expect(SecurityLogic.values.isEnabled).toEqual(true);
|
||||
});
|
||||
|
||||
it('updatePrivateSourcesEnabled', () => {
|
||||
SecurityLogic.actions.updatePrivateSourcesEnabled(false);
|
||||
|
||||
expect(SecurityLogic.values.isEnabled).toEqual(false);
|
||||
});
|
||||
|
||||
it('updateRemoteEnabled', () => {
|
||||
SecurityLogic.actions.updateRemoteEnabled(false);
|
||||
|
||||
expect(SecurityLogic.values.remote.isEnabled).toEqual(false);
|
||||
});
|
||||
|
||||
it('updateStandardEnabled', () => {
|
||||
SecurityLogic.actions.updateStandardEnabled(false);
|
||||
|
||||
expect(SecurityLogic.values.standard.isEnabled).toEqual(false);
|
||||
});
|
||||
|
||||
it('updateRemoteSource', () => {
|
||||
SecurityLogic.actions.setServerProps(serverProps);
|
||||
SecurityLogic.actions.updateRemoteSource('gmail', false);
|
||||
|
||||
expect(SecurityLogic.values.remote.contentSources[0].isEnabled).toEqual(false);
|
||||
});
|
||||
|
||||
it('updateStandardSource', () => {
|
||||
SecurityLogic.actions.setServerProps(serverProps);
|
||||
SecurityLogic.actions.updateStandardSource('one_drive', false);
|
||||
|
||||
expect(SecurityLogic.values.standard.contentSources[0].isEnabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectors', () => {
|
||||
describe('unsavedChanges', () => {
|
||||
it('returns true while loading', () => {
|
||||
expect(SecurityLogic.values.unsavedChanges).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false after loading', () => {
|
||||
SecurityLogic.actions.setServerProps(serverProps);
|
||||
|
||||
expect(SecurityLogic.values.unsavedChanges).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
describe('initializeSourceRestrictions', () => {
|
||||
it('calls API and sets values', async () => {
|
||||
const setServerPropsSpy = jest.spyOn(SecurityLogic.actions, 'setServerProps');
|
||||
http.get.mockReturnValue(Promise.resolve(serverProps));
|
||||
SecurityLogic.actions.initializeSourceRestrictions();
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/api/workplace_search/org/security/source_restrictions'
|
||||
);
|
||||
await nextTick();
|
||||
expect(setServerPropsSpy).toHaveBeenCalledWith(serverProps);
|
||||
});
|
||||
|
||||
it('handles error', async () => {
|
||||
http.get.mockReturnValue(Promise.reject('this is an error'));
|
||||
|
||||
SecurityLogic.actions.initializeSourceRestrictions();
|
||||
try {
|
||||
await nextTick();
|
||||
} catch {
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveSourceRestrictions', () => {
|
||||
it('calls API and sets values', async () => {
|
||||
http.patch.mockReturnValue(Promise.resolve(serverProps));
|
||||
SecurityLogic.actions.setSourceRestrictionsUpdated(serverProps);
|
||||
SecurityLogic.actions.saveSourceRestrictions();
|
||||
|
||||
expect(http.patch).toHaveBeenCalledWith(
|
||||
'/api/workplace_search/org/security/source_restrictions',
|
||||
{
|
||||
body: JSON.stringify(serverProps),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('handles error', async () => {
|
||||
http.patch.mockReturnValue(Promise.reject('this is an error'));
|
||||
|
||||
SecurityLogic.actions.saveSourceRestrictions();
|
||||
try {
|
||||
await nextTick();
|
||||
} catch {
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetState', () => {
|
||||
it('calls API and sets values', async () => {
|
||||
SecurityLogic.actions.setServerProps(serverProps);
|
||||
SecurityLogic.actions.updatePrivateSourcesEnabled(false);
|
||||
SecurityLogic.actions.resetState();
|
||||
|
||||
expect(SecurityLogic.values.isEnabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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 { cloneDeep } from 'lodash';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import {
|
||||
clearFlashMessages,
|
||||
setSuccessMessage,
|
||||
flashAPIErrors,
|
||||
} from '../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
import { AppLogic } from '../../app_logic';
|
||||
|
||||
import { SOURCE_RESTRICTIONS_SUCCESS_MESSAGE } from '../../constants';
|
||||
|
||||
export interface PrivateSource {
|
||||
id: string;
|
||||
name: string;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface PrivateSourceSection {
|
||||
isEnabled: boolean;
|
||||
contentSources: PrivateSource[];
|
||||
}
|
||||
|
||||
export interface SecurityServerProps {
|
||||
isEnabled: boolean;
|
||||
remote: PrivateSourceSection;
|
||||
standard: PrivateSourceSection;
|
||||
}
|
||||
|
||||
interface SecurityValues extends SecurityServerProps {
|
||||
dataLoading: boolean;
|
||||
unsavedChanges: boolean;
|
||||
cachedServerState: SecurityServerProps;
|
||||
}
|
||||
|
||||
interface SecurityActions {
|
||||
setServerProps(serverProps: SecurityServerProps): SecurityServerProps;
|
||||
setSourceRestrictionsUpdated(serverProps: SecurityServerProps): SecurityServerProps;
|
||||
initializeSourceRestrictions(): void;
|
||||
saveSourceRestrictions(): void;
|
||||
updatePrivateSourcesEnabled(isEnabled: boolean): { isEnabled: boolean };
|
||||
updateRemoteEnabled(isEnabled: boolean): { isEnabled: boolean };
|
||||
updateRemoteSource(
|
||||
sourceId: string,
|
||||
isEnabled: boolean
|
||||
): { sourceId: string; isEnabled: boolean };
|
||||
updateStandardEnabled(isEnabled: boolean): { isEnabled: boolean };
|
||||
updateStandardSource(
|
||||
sourceId: string,
|
||||
isEnabled: boolean
|
||||
): { sourceId: string; isEnabled: boolean };
|
||||
resetState(): void;
|
||||
}
|
||||
|
||||
const route = '/api/workplace_search/org/security/source_restrictions';
|
||||
|
||||
export const SecurityLogic = kea<MakeLogicType<SecurityValues, SecurityActions>>({
|
||||
path: ['enterprise_search', 'workplace_search', 'security_logic'],
|
||||
actions: {
|
||||
setServerProps: (serverProps: SecurityServerProps) => serverProps,
|
||||
setSourceRestrictionsUpdated: (serverProps: SecurityServerProps) => serverProps,
|
||||
initializeSourceRestrictions: () => true,
|
||||
saveSourceRestrictions: () => null,
|
||||
updatePrivateSourcesEnabled: (isEnabled: boolean) => ({ isEnabled }),
|
||||
updateRemoteEnabled: (isEnabled: boolean) => ({ isEnabled }),
|
||||
updateRemoteSource: (sourceId: string, isEnabled: boolean) => ({ sourceId, isEnabled }),
|
||||
updateStandardEnabled: (isEnabled: boolean) => ({ isEnabled }),
|
||||
updateStandardSource: (sourceId: string, isEnabled: boolean) => ({ sourceId, isEnabled }),
|
||||
resetState: () => null,
|
||||
},
|
||||
reducers: {
|
||||
dataLoading: [
|
||||
true,
|
||||
{
|
||||
setServerProps: () => false,
|
||||
},
|
||||
],
|
||||
cachedServerState: [
|
||||
{} as SecurityServerProps,
|
||||
{
|
||||
setServerProps: (_, serverProps) => cloneDeep(serverProps),
|
||||
setSourceRestrictionsUpdated: (_, serverProps) => cloneDeep(serverProps),
|
||||
},
|
||||
],
|
||||
isEnabled: [
|
||||
false,
|
||||
{
|
||||
setServerProps: (_, { isEnabled }) => isEnabled,
|
||||
setSourceRestrictionsUpdated: (_, { isEnabled }) => isEnabled,
|
||||
updatePrivateSourcesEnabled: (_, { isEnabled }) => isEnabled,
|
||||
},
|
||||
],
|
||||
remote: [
|
||||
{} as PrivateSourceSection,
|
||||
{
|
||||
setServerProps: (_, { remote }) => remote,
|
||||
setSourceRestrictionsUpdated: (_, { remote }) => remote,
|
||||
updateRemoteEnabled: (state, { isEnabled }) => ({ ...state, isEnabled }),
|
||||
updateRemoteSource: (state, { sourceId, isEnabled }) =>
|
||||
updateSourceEnabled(state, sourceId, isEnabled),
|
||||
},
|
||||
],
|
||||
standard: [
|
||||
{} as PrivateSourceSection,
|
||||
{
|
||||
setServerProps: (_, { standard }) => standard,
|
||||
setSourceRestrictionsUpdated: (_, { standard }) => standard,
|
||||
updateStandardEnabled: (state, { isEnabled }) => ({ ...state, isEnabled }),
|
||||
updateStandardSource: (state, { sourceId, isEnabled }) =>
|
||||
updateSourceEnabled(state, sourceId, isEnabled),
|
||||
},
|
||||
],
|
||||
},
|
||||
selectors: ({ selectors }) => ({
|
||||
unsavedChanges: [
|
||||
() => [
|
||||
selectors.cachedServerState,
|
||||
selectors.isEnabled,
|
||||
selectors.remote,
|
||||
selectors.standard,
|
||||
],
|
||||
(cached, isEnabled, remote, standard) =>
|
||||
cached.isEnabled !== isEnabled ||
|
||||
!isEqual(cached.remote, remote) ||
|
||||
!isEqual(cached.standard, standard),
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions, values }) => ({
|
||||
initializeSourceRestrictions: async () => {
|
||||
const { http } = HttpLogic.values;
|
||||
|
||||
try {
|
||||
const response = await http.get(route);
|
||||
actions.setServerProps(response);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
saveSourceRestrictions: async () => {
|
||||
const { isEnabled, remote, standard } = values;
|
||||
const serverData = { isEnabled, remote, standard };
|
||||
const body = JSON.stringify(serverData);
|
||||
const { http } = HttpLogic.values;
|
||||
|
||||
try {
|
||||
const response = await http.patch(route, { body });
|
||||
actions.setSourceRestrictionsUpdated(response);
|
||||
setSuccessMessage(SOURCE_RESTRICTIONS_SUCCESS_MESSAGE);
|
||||
AppLogic.actions.setSourceRestriction(isEnabled);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
resetState: () => {
|
||||
actions.setServerProps(cloneDeep(values.cachedServerState));
|
||||
clearFlashMessages();
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const updateSourceEnabled = (
|
||||
section: PrivateSourceSection,
|
||||
id: string,
|
||||
isEnabled: boolean
|
||||
): PrivateSourceSection => {
|
||||
const updatedSection = { ...section };
|
||||
const sources = updatedSection.contentSources;
|
||||
const sourceIndex = sources.findIndex((source) => source.id === id);
|
||||
updatedSection.contentSources[sourceIndex] = { ...sources[sourceIndex], isEnabled };
|
||||
|
||||
return updatedSection;
|
||||
};
|
|
@ -10,10 +10,12 @@ import { registerOverviewRoute } from './overview';
|
|||
import { registerGroupsRoutes } from './groups';
|
||||
import { registerSourcesRoutes } from './sources';
|
||||
import { registerSettingsRoutes } from './settings';
|
||||
import { registerSecurityRoutes } from './security';
|
||||
|
||||
export const registerWorkplaceSearchRoutes = (dependencies: RouteDependencies) => {
|
||||
registerOverviewRoute(dependencies);
|
||||
registerGroupsRoutes(dependencies);
|
||||
registerSourcesRoutes(dependencies);
|
||||
registerSettingsRoutes(dependencies);
|
||||
registerSecurityRoutes(dependencies);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
|
||||
|
||||
import { registerSecurityRoute, registerSecuritySourceRestrictionsRoute } from './security';
|
||||
|
||||
describe('security routes', () => {
|
||||
describe('GET /api/workplace_search/org/security', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockRouter = new MockRouter({
|
||||
method: 'get',
|
||||
path: '/api/workplace_search/org/security',
|
||||
});
|
||||
|
||||
registerSecurityRoute({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
mockRouter.callRoute({});
|
||||
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/ws/org/security',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/workplace_search/org/security/source_restrictions', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockRouter = new MockRouter({
|
||||
method: 'get',
|
||||
path: '/api/workplace_search/org/security/source_restrictions',
|
||||
payload: 'body',
|
||||
});
|
||||
|
||||
registerSecuritySourceRestrictionsRoute({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
mockRouter.callRoute({});
|
||||
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/ws/org/security/source_restrictions',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/workplace_search/org/security/source_restrictions', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockRouter = new MockRouter({
|
||||
method: 'patch',
|
||||
path: '/api/workplace_search/org/security/source_restrictions',
|
||||
payload: 'body',
|
||||
});
|
||||
|
||||
registerSecuritySourceRestrictionsRoute({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/ws/org/security/source_restrictions',
|
||||
});
|
||||
});
|
||||
|
||||
describe('validates', () => {
|
||||
it('correctly', () => {
|
||||
const request = {
|
||||
body: {
|
||||
isEnabled: true,
|
||||
remote: {
|
||||
isEnabled: true,
|
||||
contentSources: [{ id: 'gmail', name: 'Gmail', isEnabled: true }],
|
||||
},
|
||||
standard: {
|
||||
isEnabled: false,
|
||||
contentSources: [{ id: 'dropbox', name: 'Dropbox', isEnabled: false }],
|
||||
},
|
||||
},
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { schema } from '@kbn/config-schema';
|
||||
|
||||
import { RouteDependencies } from '../../plugin';
|
||||
|
||||
export function registerSecurityRoute({
|
||||
router,
|
||||
enterpriseSearchRequestHandler,
|
||||
}: RouteDependencies) {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/workplace_search/org/security',
|
||||
validate: false,
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/ws/org/security',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function registerSecuritySourceRestrictionsRoute({
|
||||
router,
|
||||
enterpriseSearchRequestHandler,
|
||||
}: RouteDependencies) {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/workplace_search/org/security/source_restrictions',
|
||||
validate: false,
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/ws/org/security/source_restrictions',
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
{
|
||||
path: '/api/workplace_search/org/security/source_restrictions',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
isEnabled: schema.boolean(),
|
||||
remote: schema.object({
|
||||
isEnabled: schema.boolean(),
|
||||
contentSources: schema.arrayOf(
|
||||
schema.object({
|
||||
isEnabled: schema.boolean(),
|
||||
id: schema.string(),
|
||||
name: schema.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
standard: schema.object({
|
||||
isEnabled: schema.boolean(),
|
||||
contentSources: schema.arrayOf(
|
||||
schema.object({
|
||||
isEnabled: schema.boolean(),
|
||||
id: schema.string(),
|
||||
name: schema.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/ws/org/security/source_restrictions',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const registerSecurityRoutes = (dependencies: RouteDependencies) => {
|
||||
registerSecurityRoute(dependencies);
|
||||
registerSecuritySourceRestrictionsRoute(dependencies);
|
||||
};
|
Loading…
Reference in a new issue