[Enterprise Search] Create reusable Group and Engine assignment selectors (#101790)

* Create AssignmentSelectors

These components will be used in both the Role Mapping and User flyouts to create and edit role mappings and users, respectively

* Implement AssignmentSelectors in components
This commit is contained in:
Scotty Bollinger 2021-06-10 12:36:11 -05:00 committed by GitHub
parent a9a834a105
commit d7d67df5eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 393 additions and 199 deletions

View file

@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import '../../../__mocks__/react_router';
import '../../../__mocks__/shallow_useeffect.mock';
import { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__';
import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic';
import { engines } from '../../__mocks__/engines.mock';
import React from 'react';
import { waitFor } from '@testing-library/dom';
import { shallow } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui';
import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
import { EngineAssignmentSelector } from './engine_assignment_selector';
describe('EngineAssignmentSelector', () => {
const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role;
const actions = {
initializeRoleMappings: jest.fn(),
initializeRoleMapping: jest.fn(),
handleSaveMapping: jest.fn(),
handleEngineSelectionChange: jest.fn(),
handleAccessAllEnginesChange: jest.fn(),
handleAttributeValueChange: jest.fn(),
handleAttributeSelectorChange: jest.fn(),
handleDeleteMapping: jest.fn(),
handleRoleChange: jest.fn(),
handleAuthProviderChange: jest.fn(),
resetState: jest.fn(),
};
const mockValues = {
attributes: [],
elasticsearchRoles: [],
hasAdvancedRoles: true,
dataLoading: false,
roleType: 'admin',
roleMappings: [asRoleMapping],
attributeValue: '',
attributeName: 'username',
availableEngines: engines,
selectedEngines: new Set(),
accessAllEngines: false,
availableAuthProviders: [],
multipleAuthProvidersConfig: true,
selectedAuthProviders: [],
myRole: {
availableRoleTypes: mockRole.ability.availableRoleTypes,
},
roleMappingErrors: [],
};
beforeEach(() => {
setMockActions(actions);
setMockValues(mockValues);
});
it('renders', () => {
setMockValues({ ...mockValues, roleMapping: asRoleMapping });
const wrapper = shallow(<EngineAssignmentSelector />);
expect(wrapper.find(EuiRadioGroup)).toHaveLength(1);
expect(wrapper.find(EuiComboBox)).toHaveLength(1);
});
it('sets initial selected state when accessAllEngines is true', () => {
setMockValues({ ...mockValues, accessAllEngines: true });
const wrapper = shallow(<EngineAssignmentSelector />);
expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all');
});
it('handles all/specific engines radio change', () => {
const wrapper = shallow(<EngineAssignmentSelector />);
const radio = wrapper.find(EuiRadioGroup);
radio.simulate('change', { target: { checked: false } });
expect(actions.handleAccessAllEnginesChange).toHaveBeenCalledWith(false);
});
it('handles engine checkbox click', async () => {
const wrapper = shallow(<EngineAssignmentSelector />);
await waitFor(() =>
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: engines[0].name, value: engines[0].name }])
);
wrapper.update();
expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith([engines[0].name]);
});
});

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useActions, useValues } from 'kea';
import { EuiComboBox, EuiFormRow, EuiHorizontalRule, EuiRadioGroup } from '@elastic/eui';
import { RoleOptionLabel } from '../../../shared/role_mapping';
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
import {
ENGINE_REQUIRED_ERROR,
ALL_ENGINES_LABEL,
ALL_ENGINES_DESCRIPTION,
SPECIFIC_ENGINES_LABEL,
SPECIFIC_ENGINES_DESCRIPTION,
ENGINE_ASSIGNMENT_LABEL,
} from './constants';
import { RoleMappingsLogic } from './role_mappings_logic';
export const EngineAssignmentSelector: React.FC = () => {
const { handleAccessAllEnginesChange, handleEngineSelectionChange } = useActions(
RoleMappingsLogic
);
const {
accessAllEngines,
availableEngines,
roleType,
selectedEngines,
selectedOptions,
} = useValues(RoleMappingsLogic);
const hasEngineAssignment = selectedEngines.size > 0 || accessAllEngines;
const engineOptions = [
{
id: 'all',
label: <RoleOptionLabel label={ALL_ENGINES_LABEL} description={ALL_ENGINES_DESCRIPTION} />,
},
{
id: 'specific',
label: (
<RoleOptionLabel
label={SPECIFIC_ENGINES_LABEL}
description={SPECIFIC_ENGINES_DESCRIPTION}
/>
),
},
];
return (
<>
<EuiHorizontalRule />
<EuiFormRow>
<EuiRadioGroup
options={engineOptions}
disabled={!roleHasScopedEngines(roleType)}
idSelected={accessAllEngines ? 'all' : 'specific'}
onChange={(id) => handleAccessAllEnginesChange(id === 'all')}
legend={{
children: <span>{ENGINE_ASSIGNMENT_LABEL}</span>,
}}
/>
</EuiFormRow>
<EuiFormRow isInvalid={!hasEngineAssignment} error={[ENGINE_REQUIRED_ERROR]}>
<EuiComboBox
data-test-subj="enginesSelect"
selectedOptions={selectedOptions}
options={availableEngines.map(({ name }) => ({ label: name, value: name }))}
onChange={(options) => {
handleEngineSelectionChange(options.map(({ value }) => value as string));
}}
fullWidth
isDisabled={accessAllEngines || !roleHasScopedEngines(roleType)}
/>
</EuiFormRow>
</>
);
};

View file

@ -13,16 +13,14 @@ import { engines } from '../../__mocks__/engines.mock';
import React from 'react';
import { waitFor } from '@testing-library/dom';
import { shallow } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui';
import { AttributeSelector, RoleSelector, RoleMappingFlyout } from '../../../shared/role_mapping';
import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
import { STANDARD_ROLE_TYPES } from './constants';
import { EngineAssignmentSelector } from './engine_assignment_selector';
import { RoleMapping } from './role_mapping';
describe('RoleMapping', () => {
@ -73,6 +71,7 @@ describe('RoleMapping', () => {
expect(wrapper.find(AttributeSelector)).toHaveLength(1);
expect(wrapper.find(RoleSelector)).toHaveLength(1);
expect(wrapper.find(EngineAssignmentSelector)).toHaveLength(1);
});
it('only passes standard role options for non-advanced roles', () => {
@ -82,33 +81,6 @@ describe('RoleMapping', () => {
expect(wrapper.find(RoleSelector).prop('roleOptions')).toHaveLength(STANDARD_ROLE_TYPES.length);
});
it('sets initial selected state when accessAllEngines is true', () => {
setMockValues({ ...mockValues, accessAllEngines: true });
const wrapper = shallow(<RoleMapping />);
expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all');
});
it('handles all/specific engines radio change', () => {
const wrapper = shallow(<RoleMapping />);
const radio = wrapper.find(EuiRadioGroup);
radio.simulate('change', { target: { checked: false } });
expect(actions.handleAccessAllEnginesChange).toHaveBeenCalledWith(false);
});
it('handles engine checkbox click', async () => {
const wrapper = shallow(<RoleMapping />);
await waitFor(() =>
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: engines[0].name, value: engines[0].name }])
);
wrapper.update();
expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith([engines[0].name]);
});
it('enables flyout when attribute value is valid', () => {
setMockValues({
...mockValues,

View file

@ -9,47 +9,23 @@ import React from 'react';
import { useActions, useValues } from 'kea';
import {
EuiComboBox,
EuiForm,
EuiFormRow,
EuiHorizontalRule,
EuiRadioGroup,
EuiSpacer,
} from '@elastic/eui';
import { EuiForm, EuiSpacer } from '@elastic/eui';
import {
AttributeSelector,
RoleSelector,
RoleOptionLabel,
RoleMappingFlyout,
} from '../../../shared/role_mapping';
import { AttributeSelector, RoleSelector, RoleMappingFlyout } from '../../../shared/role_mapping';
import { AppLogic } from '../../app_logic';
import { AdvanceRoleType } from '../../types';
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
import {
ADVANCED_ROLE_TYPES,
STANDARD_ROLE_TYPES,
ENGINE_REQUIRED_ERROR,
ALL_ENGINES_LABEL,
ALL_ENGINES_DESCRIPTION,
SPECIFIC_ENGINES_LABEL,
SPECIFIC_ENGINES_DESCRIPTION,
ENGINE_ASSIGNMENT_LABEL,
} from './constants';
import { ADVANCED_ROLE_TYPES, STANDARD_ROLE_TYPES } from './constants';
import { EngineAssignmentSelector } from './engine_assignment_selector';
import { RoleMappingsLogic } from './role_mappings_logic';
export const RoleMapping: React.FC = () => {
const { myRole } = useValues(AppLogic);
const {
handleAccessAllEnginesChange,
handleAttributeSelectorChange,
handleAttributeValueChange,
handleAuthProviderChange,
handleEngineSelectionChange,
handleRoleChange,
handleSaveMapping,
closeRoleMappingFlyout,
@ -61,7 +37,6 @@ export const RoleMapping: React.FC = () => {
attributeValue,
attributes,
availableAuthProviders,
availableEngines,
elasticsearchRoles,
hasAdvancedRoles,
multipleAuthProvidersConfig,
@ -69,7 +44,6 @@ export const RoleMapping: React.FC = () => {
roleType,
selectedEngines,
selectedAuthProviders,
selectedOptions,
roleMappingErrors,
} = useValues(RoleMappingsLogic);
@ -90,22 +64,6 @@ export const RoleMapping: React.FC = () => {
? [...standardRoleOptions, ...advancedRoleOptions]
: standardRoleOptions;
const engineOptions = [
{
id: 'all',
label: <RoleOptionLabel label={ALL_ENGINES_LABEL} description={ALL_ENGINES_DESCRIPTION} />,
},
{
id: 'specific',
label: (
<RoleOptionLabel
label={SPECIFIC_ENGINES_LABEL}
description={SPECIFIC_ENGINES_DESCRIPTION}
/>
),
},
];
return (
<RoleMappingFlyout
disabled={attributeValueInvalid || !hasEngineAssignment}
@ -135,35 +93,7 @@ export const RoleMapping: React.FC = () => {
onChange={handleRoleChange}
label="Role"
/>
{hasAdvancedRoles && (
<>
<EuiHorizontalRule />
<EuiFormRow>
<EuiRadioGroup
options={engineOptions}
disabled={!roleHasScopedEngines(roleType)}
idSelected={accessAllEngines ? 'all' : 'specific'}
onChange={(id) => handleAccessAllEnginesChange(id === 'all')}
legend={{
children: <span>{ENGINE_ASSIGNMENT_LABEL}</span>,
}}
/>
</EuiFormRow>
<EuiFormRow isInvalid={!hasEngineAssignment} error={[ENGINE_REQUIRED_ERROR]}>
<EuiComboBox
data-test-subj="enginesSelect"
selectedOptions={selectedOptions}
options={availableEngines.map(({ name }) => ({ label: name, value: name }))}
onChange={(options) => {
handleEngineSelectionChange(options.map(({ value }) => value as string));
}}
fullWidth
isDisabled={accessAllEngines || !roleHasScopedEngines(roleType)}
/>
</EuiFormRow>
</>
)}
{hasAdvancedRoles && <EngineAssignmentSelector />}
</EuiForm>
</RoleMappingFlyout>
);

View file

@ -0,0 +1,113 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import '../../../__mocks__/react_router';
import '../../../__mocks__/shallow_useeffect.mock';
import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic';
import React from 'react';
import { waitFor } from '@testing-library/dom';
import { shallow } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui';
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
import { GroupAssignmentSelector } from './group_assignment_selector';
describe('GroupAssignmentSelector', () => {
const initializeRoleMappings = jest.fn();
const initializeRoleMapping = jest.fn();
const handleSaveMapping = jest.fn();
const handleGroupSelectionChange = jest.fn();
const handleAllGroupsSelectionChange = jest.fn();
const handleAttributeValueChange = jest.fn();
const handleAttributeSelectorChange = jest.fn();
const handleDeleteMapping = jest.fn();
const handleRoleChange = jest.fn();
const handleAuthProviderChange = jest.fn();
const resetState = jest.fn();
const groups = [
{
name: 'Group 1',
id: 'g1',
},
{
name: 'Group 2',
id: 'g2',
},
];
const mockValues = {
attributes: [],
elasticsearchRoles: [],
dataLoading: false,
roleType: 'admin',
roleMappings: [wsRoleMapping],
attributeValue: '',
attributeName: 'username',
availableGroups: groups,
selectedGroups: new Set(),
includeInAllGroups: false,
availableAuthProviders: [],
multipleAuthProvidersConfig: true,
selectedAuthProviders: [],
roleMappingErrors: [],
};
beforeEach(() => {
setMockActions({
initializeRoleMappings,
initializeRoleMapping,
handleSaveMapping,
handleGroupSelectionChange,
handleAllGroupsSelectionChange,
handleAttributeValueChange,
handleAttributeSelectorChange,
handleDeleteMapping,
handleRoleChange,
handleAuthProviderChange,
resetState,
});
setMockValues(mockValues);
});
it('renders', () => {
setMockValues({ ...mockValues, GroupAssignmentSelector: wsRoleMapping });
const wrapper = shallow(<GroupAssignmentSelector />);
expect(wrapper.find(EuiRadioGroup)).toHaveLength(1);
expect(wrapper.find(EuiComboBox)).toHaveLength(1);
});
it('sets initial selected state when includeInAllGroups is true', () => {
setMockValues({ ...mockValues, includeInAllGroups: true });
const wrapper = shallow(<GroupAssignmentSelector />);
expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all');
});
it('handles all/specific groups radio change', () => {
const wrapper = shallow(<GroupAssignmentSelector />);
const radio = wrapper.find(EuiRadioGroup);
radio.simulate('change', { target: { checked: false } });
expect(handleAllGroupsSelectionChange).toHaveBeenCalledWith(false);
});
it('handles group checkbox click', async () => {
const wrapper = shallow(<GroupAssignmentSelector />);
await waitFor(() =>
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: groups[0].name, value: groups[0].name }])
);
wrapper.update();
expect(handleGroupSelectionChange).toHaveBeenCalledWith([groups[0].name]);
});
});

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useActions, useValues } from 'kea';
import { EuiComboBox, EuiFormRow, EuiHorizontalRule, EuiRadioGroup } from '@elastic/eui';
import { RoleOptionLabel } from '../../../shared/role_mapping';
import {
GROUP_ASSIGNMENT_INVALID_ERROR,
GROUP_ASSIGNMENT_LABEL,
ALL_GROUPS_LABEL,
ALL_GROUPS_DESCRIPTION,
SPECIFIC_GROUPS_LABEL,
SPECIFIC_GROUPS_DESCRIPTION,
} from './constants';
import { RoleMappingsLogic } from './role_mappings_logic';
export const GroupAssignmentSelector: React.FC = () => {
const { handleAllGroupsSelectionChange, handleGroupSelectionChange } = useActions(
RoleMappingsLogic
);
const { includeInAllGroups, availableGroups, selectedGroups, selectedOptions } = useValues(
RoleMappingsLogic
);
const hasGroupAssignment = selectedGroups.size > 0 || includeInAllGroups;
const groupOptions = [
{
id: 'all',
label: <RoleOptionLabel label={ALL_GROUPS_LABEL} description={ALL_GROUPS_DESCRIPTION} />,
},
{
id: 'specific',
label: (
<RoleOptionLabel label={SPECIFIC_GROUPS_LABEL} description={SPECIFIC_GROUPS_DESCRIPTION} />
),
},
];
return (
<>
<EuiHorizontalRule />
<EuiFormRow>
<EuiRadioGroup
options={groupOptions}
idSelected={includeInAllGroups ? 'all' : 'specific'}
onChange={(id) => handleAllGroupsSelectionChange(id === 'all')}
legend={{
children: <span>{GROUP_ASSIGNMENT_LABEL}</span>,
}}
/>
</EuiFormRow>
<EuiFormRow isInvalid={!hasGroupAssignment} error={[GROUP_ASSIGNMENT_INVALID_ERROR]}>
<EuiComboBox
data-test-subj="groupsSelect"
selectedOptions={selectedOptions}
options={availableGroups.map(({ name, id }) => ({ label: name, value: id }))}
onChange={(options) => {
handleGroupSelectionChange(options.map(({ value }) => value as string));
}}
fullWidth
isDisabled={includeInAllGroups}
/>
</EuiFormRow>
</>
);
};

View file

@ -11,14 +11,12 @@ import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic';
import React from 'react';
import { waitFor } from '@testing-library/dom';
import { shallow } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui';
import { AttributeSelector, RoleSelector, RoleMappingFlyout } from '../../../shared/role_mapping';
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
import { GroupAssignmentSelector } from './group_assignment_selector';
import { RoleMapping } from './role_mapping';
describe('RoleMapping', () => {
@ -83,33 +81,7 @@ describe('RoleMapping', () => {
expect(wrapper.find(AttributeSelector)).toHaveLength(1);
expect(wrapper.find(RoleSelector)).toHaveLength(1);
});
it('sets initial selected state when includeInAllGroups is true', () => {
setMockValues({ ...mockValues, includeInAllGroups: true });
const wrapper = shallow(<RoleMapping />);
expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all');
});
it('handles all/specific groups radio change', () => {
const wrapper = shallow(<RoleMapping />);
const radio = wrapper.find(EuiRadioGroup);
radio.simulate('change', { target: { checked: false } });
expect(handleAllGroupsSelectionChange).toHaveBeenCalledWith(false);
});
it('handles group checkbox click', async () => {
const wrapper = shallow(<RoleMapping />);
await waitFor(() =>
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: groups[0].name, value: groups[0].name }])
);
wrapper.update();
expect(handleGroupSelectionChange).toHaveBeenCalledWith([groups[0].name]);
expect(wrapper.find(GroupAssignmentSelector)).toHaveLength(1);
});
it('enables flyout when attribute value is valid', () => {

View file

@ -9,35 +9,15 @@ import React from 'react';
import { useActions, useValues } from 'kea';
import {
EuiComboBox,
EuiForm,
EuiFormRow,
EuiHorizontalRule,
EuiRadioGroup,
EuiSpacer,
} from '@elastic/eui';
import { EuiForm, EuiSpacer } from '@elastic/eui';
import {
AttributeSelector,
RoleSelector,
RoleOptionLabel,
RoleMappingFlyout,
} from '../../../shared/role_mapping';
import { AttributeSelector, RoleSelector, RoleMappingFlyout } from '../../../shared/role_mapping';
import { Role } from '../../types';
import {
ADMIN_ROLE_TYPE_DESCRIPTION,
USER_ROLE_TYPE_DESCRIPTION,
GROUP_ASSIGNMENT_INVALID_ERROR,
GROUP_ASSIGNMENT_LABEL,
ALL_GROUPS_LABEL,
ALL_GROUPS_DESCRIPTION,
SPECIFIC_GROUPS_LABEL,
SPECIFIC_GROUPS_DESCRIPTION,
} from './constants';
import { ADMIN_ROLE_TYPE_DESCRIPTION, USER_ROLE_TYPE_DESCRIPTION } from './constants';
import { GroupAssignmentSelector } from './group_assignment_selector';
import { RoleMappingsLogic } from './role_mappings_logic';
interface RoleType {
@ -56,24 +36,9 @@ const roleOptions = [
},
] as RoleType[];
const groupOptions = [
{
id: 'all',
label: <RoleOptionLabel label={ALL_GROUPS_LABEL} description={ALL_GROUPS_DESCRIPTION} />,
},
{
id: 'specific',
label: (
<RoleOptionLabel label={SPECIFIC_GROUPS_LABEL} description={SPECIFIC_GROUPS_DESCRIPTION} />
),
},
];
export const RoleMapping: React.FC = () => {
const {
handleSaveMapping,
handleGroupSelectionChange,
handleAllGroupsSelectionChange,
handleAttributeValueChange,
handleAttributeSelectorChange,
handleRoleChange,
@ -87,13 +52,11 @@ export const RoleMapping: React.FC = () => {
roleType,
attributeValue,
attributeName,
availableGroups,
selectedGroups,
includeInAllGroups,
availableAuthProviders,
multipleAuthProvidersConfig,
selectedAuthProviders,
selectedOptions,
roleMapping,
roleMappingErrors,
} = useValues(RoleMappingsLogic);
@ -132,29 +95,7 @@ export const RoleMapping: React.FC = () => {
onChange={handleRoleChange}
label="Role"
/>
<EuiHorizontalRule />
<EuiFormRow>
<EuiRadioGroup
options={groupOptions}
idSelected={includeInAllGroups ? 'all' : 'specific'}
onChange={(id) => handleAllGroupsSelectionChange(id === 'all')}
legend={{
children: <span>{GROUP_ASSIGNMENT_LABEL}</span>,
}}
/>
</EuiFormRow>
<EuiFormRow isInvalid={!hasGroupAssignment} error={[GROUP_ASSIGNMENT_INVALID_ERROR]}>
<EuiComboBox
data-test-subj="groupsSelect"
selectedOptions={selectedOptions}
options={availableGroups.map(({ name, id }) => ({ label: name, value: id }))}
onChange={(options) => {
handleGroupSelectionChange(options.map(({ value }) => value as string));
}}
fullWidth
isDisabled={includeInAllGroups}
/>
</EuiFormRow>
<GroupAssignmentSelector />
</EuiForm>
</RoleMappingFlyout>
);