[Enterprise Search] Convert Role mappings for both apps to use flyouts (#101198)
* Add RoleOptionLabel component * Refactor RoleSelector to use EuiRadioGroup Previously, we used individual radio buttons in a map in the component. However the new designs have a shared label and work best in the EuiRadioGroup component. * Add reducer and actions to logic file for flyout visibility * Remove redirects in favor of refreshing lists With the existing multi-page view, we redirect after creating, editing or deleting a mapping. We now simply refresh the list after the action. Also as a part of this commit, we show a hard-coded error message if a user tries to navigate to a non-existant mapping, instead of redirecting to a 404 page * Add RoleMappingFlyout component * Refactor AttributeSelector No longer uses a panel or has the need for flex groups - Also added a test for 100% coverage * Refactor RoleMappingsTable - Use EuiButtonIcons instead of Link - Manage button now triggers flyout instead of linking to route * Remove AddRoleMappingButton We can just use an EuiButton to trigger the flyout * Convert to use RoleSelector syntax - Passes the entire array to the component instead of mapping. - Uses ‘id’ instead of ‘type’ to match EUI component - For App Search, as per design and PM direction, dropping labels for advanced and standard roles and showing them all in the same list. - Removed unused constant and i18ns * Move constants to shared Will do a lot more of this in a future PR * Remove DeleteMappingCallout - This now an action in the row - Also added tests for correct titles for 100% test coverage * Remove routers and routes - SPA FTW * No longer pass isNew as prop - Determine based on existence of Role Mapping instead * No longer need to initialze role mapping in the component This will become a flyout and the intialization will be triggered when the button in the table is clicked. * Remove flash messages This will be handled globally in the main component. * Wrap components with flyout Also add to main RoleMappings views * Add form row validation for App Search * Remove unnecessary layout components - Don’t need the panel, headings, spacer, and Flex components - Also removed constants and i18n from unused headings * Wire up handleDeleteMapping to take ID param The method now passes the ID directly from the table row action item * Add EuiPortal wrapper for flyout Without this, the flyout was was under the overlay. Hide whitespace changes on this commit * Add spacer to better match design * Update constants for new copy from design * Replace all engines/groups radio and group/engine selectors - The designs call for a radio group and a combo box, instead of separate radios and a list of checkboxes - Also added a spacer to each layout * Remove util that is no longer needed - This was used for generating routes that are no longer there - Also removed unused test file from a component deleted in an earlier PR - Fix test since spacer was added * Add missing i18n constant * Add back missing scoped engine check * Rename roleId -> roleMappingId * Use shared constant for “Cancel” Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
137778d124
commit
fc511f9ec1
|
@ -9,15 +9,6 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { AdvanceRoleType } from '../../types';
|
||||
|
||||
export const SAVE_ROLE_MAPPING = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.roleMapping.saveRoleMappingButtonLabel',
|
||||
{ defaultMessage: 'Save role mapping' }
|
||||
);
|
||||
export const UPDATE_ROLE_MAPPING = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.roleMapping.updateRoleMappingButtonLabel',
|
||||
{ defaultMessage: 'Update role mapping' }
|
||||
);
|
||||
|
||||
export const EMPTY_ROLE_MAPPINGS_BODY = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.roleMapping.emptyRoleMappingsBody',
|
||||
{
|
||||
|
@ -126,74 +117,71 @@ export const ADMIN_ROLE_TYPE_DESCRIPTION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ADVANCED_ROLE_SELECTORS_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.advancedRoleSelectorsTitle',
|
||||
export const ENGINE_REQUIRED_ERROR = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engineRequiredError',
|
||||
{
|
||||
defaultMessage: 'Full or limited engine access',
|
||||
defaultMessage: 'At least one assigned engine is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROLE_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.roleTitle', {
|
||||
defaultMessage: 'Role',
|
||||
});
|
||||
|
||||
export const FULL_ENGINE_ACCESS_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.fullEngineAccessTitle',
|
||||
export const ALL_ENGINES_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.allEnginesLabel',
|
||||
{
|
||||
defaultMessage: 'Full engine access',
|
||||
defaultMessage: 'Assign to all engines',
|
||||
}
|
||||
);
|
||||
|
||||
export const FULL_ENGINE_ACCESS_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.fullEngineAccessDescription',
|
||||
export const ALL_ENGINES_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.allEnginesDescription',
|
||||
{
|
||||
defaultMessage: 'Access to all current and future engines.',
|
||||
defaultMessage:
|
||||
'Assigning to all engines includes all current and future engines as created and administered at a later date.',
|
||||
}
|
||||
);
|
||||
|
||||
export const LIMITED_ENGINE_ACCESS_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.limitedEngineAccessTitle',
|
||||
export const SPECIFIC_ENGINES_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.specificEnginesLabel',
|
||||
{
|
||||
defaultMessage: 'Limited engine access',
|
||||
defaultMessage: 'Assign to specific engines',
|
||||
}
|
||||
);
|
||||
|
||||
export const LIMITED_ENGINE_ACCESS_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.limitedEngineAccessDescription',
|
||||
export const SPECIFIC_ENGINES_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.specificEnginesDescription',
|
||||
{
|
||||
defaultMessage: 'Limit user access to specific engines:',
|
||||
defaultMessage: 'Assign to a select set of engines statically.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENGINE_ACCESS_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engineAccessTitle',
|
||||
export const ENGINE_ASSIGNMENT_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engineAssignmentLabel',
|
||||
{
|
||||
defaultMessage: 'Engine access',
|
||||
defaultMessage: 'Engine assignment',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADVANCED_ROLE_TYPES = [
|
||||
{
|
||||
type: 'dev',
|
||||
id: 'dev',
|
||||
description: DEV_ROLE_TYPE_DESCRIPTION,
|
||||
},
|
||||
{
|
||||
type: 'editor',
|
||||
id: 'editor',
|
||||
description: EDITOR_ROLE_TYPE_DESCRIPTION,
|
||||
},
|
||||
{
|
||||
type: 'analyst',
|
||||
id: 'analyst',
|
||||
description: ANALYST_ROLE_TYPE_DESCRIPTION,
|
||||
},
|
||||
] as AdvanceRoleType[];
|
||||
|
||||
export const STANDARD_ROLE_TYPES = [
|
||||
{
|
||||
type: 'owner',
|
||||
id: 'owner',
|
||||
description: OWNER_ROLE_TYPE_DESCRIPTION,
|
||||
},
|
||||
{
|
||||
type: 'admin',
|
||||
id: 'admin',
|
||||
description: ADMIN_ROLE_TYPE_DESCRIPTION,
|
||||
},
|
||||
] as AdvanceRoleType[];
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { RoleMappingsRouter } from './role_mappings_router';
|
||||
export { RoleMappings } from './role_mappings';
|
||||
|
|
|
@ -12,18 +12,16 @@ import { engines } from '../../__mocks__/engines.mock';
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiCheckbox } from '@elastic/eui';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import {
|
||||
AttributeSelector,
|
||||
DeleteMappingCallout,
|
||||
RoleSelector,
|
||||
} from '../../../shared/role_mapping';
|
||||
import { AttributeSelector, RoleSelector } from '../../../shared/role_mapping';
|
||||
import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
|
||||
|
||||
import { STANDARD_ROLE_TYPES } from './constants';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
|
||||
describe('RoleMapping', () => {
|
||||
|
@ -68,39 +66,44 @@ describe('RoleMapping', () => {
|
|||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<RoleMapping />);
|
||||
|
||||
expect(wrapper.find(AttributeSelector)).toHaveLength(1);
|
||||
expect(wrapper.find(RoleSelector)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('returns Loading when loading', () => {
|
||||
setMockValues({ ...mockValues, dataLoading: true });
|
||||
const wrapper = shallow(<RoleMapping />);
|
||||
|
||||
expect(wrapper.find(Loading)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders DeleteMappingCallout for existing mapping', () => {
|
||||
setMockValues({ ...mockValues, roleMapping: asRoleMapping });
|
||||
const wrapper = shallow(<RoleMapping />);
|
||||
|
||||
expect(wrapper.find(DeleteMappingCallout)).toHaveLength(1);
|
||||
expect(wrapper.find(AttributeSelector)).toHaveLength(1);
|
||||
expect(wrapper.find(RoleSelector)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('hides DeleteMappingCallout for new mapping', () => {
|
||||
const wrapper = shallow(<RoleMapping isNew />);
|
||||
|
||||
expect(wrapper.find(DeleteMappingCallout)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles engine checkbox click', () => {
|
||||
it('only passes standard role options for non-advanced roles', () => {
|
||||
setMockValues({ ...mockValues, hasAdvancedRoles: false });
|
||||
const wrapper = shallow(<RoleMapping />);
|
||||
wrapper
|
||||
.find(EuiCheckbox)
|
||||
.first()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
|
||||
expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith(engines[0].name, true);
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,65 +5,36 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCheckbox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiPageContentBody,
|
||||
EuiPageHeader,
|
||||
EuiPanel,
|
||||
EuiRadio,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { EuiComboBox, EuiFormRow, EuiHorizontalRule, EuiRadioGroup, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import {
|
||||
AttributeSelector,
|
||||
DeleteMappingCallout,
|
||||
RoleSelector,
|
||||
RoleOptionLabel,
|
||||
RoleMappingFlyout,
|
||||
} from '../../../shared/role_mapping';
|
||||
import {
|
||||
ROLE_MAPPINGS_TITLE,
|
||||
ADD_ROLE_MAPPING_TITLE,
|
||||
MANAGE_ROLE_MAPPING_TITLE,
|
||||
} from '../../../shared/role_mapping/constants';
|
||||
import { AppLogic } from '../../app_logic';
|
||||
import { AdvanceRoleType } from '../../types';
|
||||
|
||||
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
|
||||
import { Engine } from '../engine/types';
|
||||
|
||||
import {
|
||||
SAVE_ROLE_MAPPING,
|
||||
UPDATE_ROLE_MAPPING,
|
||||
ADVANCED_ROLE_TYPES,
|
||||
STANDARD_ROLE_TYPES,
|
||||
ADVANCED_ROLE_SELECTORS_TITLE,
|
||||
ROLE_TITLE,
|
||||
FULL_ENGINE_ACCESS_TITLE,
|
||||
FULL_ENGINE_ACCESS_DESCRIPTION,
|
||||
LIMITED_ENGINE_ACCESS_TITLE,
|
||||
LIMITED_ENGINE_ACCESS_DESCRIPTION,
|
||||
ENGINE_ACCESS_TITLE,
|
||||
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';
|
||||
|
||||
interface RoleMappingProps {
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export const RoleMapping: React.FC<RoleMappingProps> = ({ isNew }) => {
|
||||
const { roleId } = useParams() as { roleId: string };
|
||||
export const RoleMapping: React.FC = () => {
|
||||
const { myRole } = useValues(AppLogic);
|
||||
|
||||
const {
|
||||
|
@ -71,12 +42,10 @@ export const RoleMapping: React.FC<RoleMappingProps> = ({ isNew }) => {
|
|||
handleAttributeSelectorChange,
|
||||
handleAttributeValueChange,
|
||||
handleAuthProviderChange,
|
||||
handleDeleteMapping,
|
||||
handleEngineSelectionChange,
|
||||
handleRoleChange,
|
||||
handleSaveMapping,
|
||||
initializeRoleMapping,
|
||||
resetState,
|
||||
closeRoleMappingFlyout,
|
||||
} = useActions(RoleMappingsLogic);
|
||||
|
||||
const {
|
||||
|
@ -86,7 +55,6 @@ export const RoleMapping: React.FC<RoleMappingProps> = ({ isNew }) => {
|
|||
attributes,
|
||||
availableAuthProviders,
|
||||
availableEngines,
|
||||
dataLoading,
|
||||
elasticsearchRoles,
|
||||
hasAdvancedRoles,
|
||||
multipleAuthProvidersConfig,
|
||||
|
@ -94,154 +62,97 @@ export const RoleMapping: React.FC<RoleMappingProps> = ({ isNew }) => {
|
|||
roleType,
|
||||
selectedEngines,
|
||||
selectedAuthProviders,
|
||||
selectedOptions,
|
||||
} = useValues(RoleMappingsLogic);
|
||||
|
||||
useEffect(() => {
|
||||
initializeRoleMapping(roleId);
|
||||
return resetState;
|
||||
}, []);
|
||||
const isNew = !roleMapping;
|
||||
const hasEngineAssignment = selectedEngines.size > 0 || accessAllEngines;
|
||||
|
||||
if (dataLoading) return <Loading />;
|
||||
const mapRoleOptions = ({ id, description }: AdvanceRoleType) => ({
|
||||
id,
|
||||
description,
|
||||
disabled: !myRole.availableRoleTypes.includes(id),
|
||||
});
|
||||
|
||||
const SAVE_ROLE_MAPPING_LABEL = isNew ? SAVE_ROLE_MAPPING : UPDATE_ROLE_MAPPING;
|
||||
const TITLE = isNew ? ADD_ROLE_MAPPING_TITLE : MANAGE_ROLE_MAPPING_TITLE;
|
||||
const standardRoleOptions = STANDARD_ROLE_TYPES.map(mapRoleOptions);
|
||||
const advancedRoleOptions = ADVANCED_ROLE_TYPES.map(mapRoleOptions);
|
||||
|
||||
const saveRoleMappingButton = (
|
||||
<EuiButton onClick={handleSaveMapping} fill>
|
||||
{SAVE_ROLE_MAPPING_LABEL}
|
||||
</EuiButton>
|
||||
);
|
||||
const roleOptions = hasAdvancedRoles
|
||||
? [...standardRoleOptions, ...advancedRoleOptions]
|
||||
: standardRoleOptions;
|
||||
|
||||
const engineSelector = (engine: Engine) => (
|
||||
<EuiCheckbox
|
||||
key={engine.name}
|
||||
name={engine.name}
|
||||
id={`engine_option_${engine.name}`}
|
||||
checked={selectedEngines.has(engine.name)}
|
||||
onChange={(e) => {
|
||||
handleEngineSelectionChange(engine.name, e.target.checked);
|
||||
}}
|
||||
label={engine.name}
|
||||
/>
|
||||
);
|
||||
|
||||
const advancedRoleSelectors = (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiTitle size="xs">
|
||||
<h4>{ADVANCED_ROLE_SELECTORS_TITLE}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
{ADVANCED_ROLE_TYPES.map(({ type, description }) => (
|
||||
<RoleSelector
|
||||
key={type}
|
||||
disabled={!myRole.availableRoleTypes.includes(type)}
|
||||
roleType={roleType}
|
||||
roleTypeOption={type}
|
||||
description={description}
|
||||
onChange={handleRoleChange}
|
||||
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 (
|
||||
<>
|
||||
<SetPageChrome trail={[ROLE_MAPPINGS_TITLE, TITLE]} />
|
||||
<EuiPageHeader rightSideItems={[saveRoleMappingButton]} pageTitle={TITLE} />
|
||||
<EuiSpacer />
|
||||
<EuiPageContentBody>
|
||||
<FlashMessages />
|
||||
<AttributeSelector
|
||||
attributeName={attributeName}
|
||||
attributeValue={attributeValue}
|
||||
attributes={attributes}
|
||||
availableAuthProviders={availableAuthProviders}
|
||||
elasticsearchRoles={elasticsearchRoles}
|
||||
selectedAuthProviders={selectedAuthProviders}
|
||||
disabled={!!roleMapping}
|
||||
handleAttributeSelectorChange={handleAttributeSelectorChange}
|
||||
handleAttributeValueChange={handleAttributeValueChange}
|
||||
handleAuthProviderChange={handleAuthProviderChange}
|
||||
multipleAuthProvidersConfig={multipleAuthProvidersConfig}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup alignItems="stretch">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} color="subdued" paddingSize="l">
|
||||
<EuiTitle size="s">
|
||||
<h3>{ROLE_TITLE}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiTitle size="xs">
|
||||
<h4>{FULL_ENGINE_ACCESS_TITLE}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
{STANDARD_ROLE_TYPES.map(({ type, description }) => (
|
||||
<RoleSelector
|
||||
key={type}
|
||||
roleType={roleType}
|
||||
onChange={handleRoleChange}
|
||||
roleTypeOption={type}
|
||||
description={description}
|
||||
/>
|
||||
))}
|
||||
{hasAdvancedRoles && advancedRoleSelectors}
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{hasAdvancedRoles && (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} color="subdued" paddingSize="l">
|
||||
<EuiTitle size="s">
|
||||
<h3>{ENGINE_ACCESS_TITLE}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiRadio
|
||||
id="accessAllEngines"
|
||||
disabled={!roleHasScopedEngines(roleType)}
|
||||
checked={accessAllEngines}
|
||||
onChange={handleAccessAllEnginesChange}
|
||||
label={
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h4>{FULL_ENGINE_ACCESS_TITLE}</h4>
|
||||
</EuiTitle>
|
||||
<p>{FULL_ENGINE_ACCESS_DESCRIPTION}</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow>
|
||||
<>
|
||||
<EuiRadio
|
||||
id="selectEngines"
|
||||
disabled={!roleHasScopedEngines(roleType)}
|
||||
checked={!accessAllEngines}
|
||||
onChange={handleAccessAllEnginesChange}
|
||||
label={
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h4>{LIMITED_ENGINE_ACCESS_TITLE}</h4>
|
||||
</EuiTitle>
|
||||
<p>{LIMITED_ENGINE_ACCESS_DESCRIPTION}</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{!accessAllEngines && (
|
||||
<div className="engines-list">
|
||||
{availableEngines.map((engine) => engineSelector(engine))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{roleMapping && <DeleteMappingCallout handleDeleteMapping={handleDeleteMapping} />}
|
||||
</EuiPageContentBody>
|
||||
</>
|
||||
<RoleMappingFlyout
|
||||
disabled={!hasEngineAssignment}
|
||||
isNew={isNew}
|
||||
closeRoleMappingFlyout={closeRoleMappingFlyout}
|
||||
handleSaveMapping={handleSaveMapping}
|
||||
>
|
||||
<AttributeSelector
|
||||
attributeName={attributeName}
|
||||
attributeValue={attributeValue}
|
||||
attributes={attributes}
|
||||
availableAuthProviders={availableAuthProviders}
|
||||
elasticsearchRoles={elasticsearchRoles}
|
||||
selectedAuthProviders={selectedAuthProviders}
|
||||
disabled={!!roleMapping}
|
||||
handleAttributeSelectorChange={handleAttributeSelectorChange}
|
||||
handleAttributeValueChange={handleAttributeValueChange}
|
||||
handleAuthProviderChange={handleAuthProviderChange}
|
||||
multipleAuthProvidersConfig={multipleAuthProvidersConfig}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<RoleSelector
|
||||
roleType={roleType}
|
||||
roleOptions={roleOptions}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</RoleMappingFlyout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,16 +12,19 @@ import React from 'react';
|
|||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import { RoleMappingsTable } from '../../../shared/role_mapping';
|
||||
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
import { RoleMappings } from './role_mappings';
|
||||
|
||||
describe('RoleMappings', () => {
|
||||
const initializeRoleMappings = jest.fn();
|
||||
const initializeRoleMapping = jest.fn();
|
||||
const handleDeleteMapping = jest.fn();
|
||||
const mockValues = {
|
||||
roleMappings: [wsRoleMapping],
|
||||
dataLoading: false,
|
||||
|
@ -31,6 +34,8 @@ describe('RoleMappings', () => {
|
|||
beforeEach(() => {
|
||||
setMockActions({
|
||||
initializeRoleMappings,
|
||||
initializeRoleMapping,
|
||||
handleDeleteMapping,
|
||||
});
|
||||
setMockValues(mockValues);
|
||||
});
|
||||
|
@ -54,4 +59,19 @@ describe('RoleMappings', () => {
|
|||
|
||||
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders RoleMapping flyout', () => {
|
||||
setMockValues({ ...mockValues, roleMappingFlyoutOpen: true });
|
||||
const wrapper = shallow(<RoleMappings />);
|
||||
|
||||
expect(wrapper.find(RoleMapping)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles button click', () => {
|
||||
setMockValues({ ...mockValues, roleMappings: [] });
|
||||
const wrapper = shallow(<RoleMappings />);
|
||||
wrapper.find(EuiEmptyPrompt).dive().find(EuiButton).simulate('click');
|
||||
|
||||
expect(initializeRoleMapping).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, { useEffect } from 'react';
|
|||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
|
@ -20,22 +21,31 @@ import {
|
|||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import { AddRoleMappingButton, RoleMappingsTable } from '../../../shared/role_mapping';
|
||||
import { RoleMappingsTable } from '../../../shared/role_mapping';
|
||||
import {
|
||||
EMPTY_ROLE_MAPPINGS_TITLE,
|
||||
ROLE_MAPPING_ADD_BUTTON,
|
||||
ROLE_MAPPINGS_TITLE,
|
||||
ROLE_MAPPINGS_DESCRIPTION,
|
||||
} from '../../../shared/role_mapping/constants';
|
||||
|
||||
import { ROLE_MAPPING_NEW_PATH } from '../../routes';
|
||||
|
||||
import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING, EMPTY_ROLE_MAPPINGS_BODY } from './constants';
|
||||
import { RoleMapping } from './role_mapping';
|
||||
import { RoleMappingsLogic } from './role_mappings_logic';
|
||||
import { generateRoleMappingPath } from './utils';
|
||||
|
||||
export const RoleMappings: React.FC = () => {
|
||||
const { initializeRoleMappings, resetState } = useActions(RoleMappingsLogic);
|
||||
const { roleMappings, multipleAuthProvidersConfig, dataLoading } = useValues(RoleMappingsLogic);
|
||||
const {
|
||||
initializeRoleMappings,
|
||||
initializeRoleMapping,
|
||||
handleDeleteMapping,
|
||||
resetState,
|
||||
} = useActions(RoleMappingsLogic);
|
||||
const {
|
||||
roleMappings,
|
||||
multipleAuthProvidersConfig,
|
||||
dataLoading,
|
||||
roleMappingFlyoutOpen,
|
||||
} = useValues(RoleMappingsLogic);
|
||||
|
||||
useEffect(() => {
|
||||
initializeRoleMappings();
|
||||
|
@ -44,7 +54,11 @@ export const RoleMappings: React.FC = () => {
|
|||
|
||||
if (dataLoading) return <Loading />;
|
||||
|
||||
const addMappingButton = <AddRoleMappingButton path={ROLE_MAPPING_NEW_PATH} />;
|
||||
const addMappingButton = (
|
||||
<EuiButton fill onClick={() => initializeRoleMapping()}>
|
||||
{ROLE_MAPPING_ADD_BUTTON}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
const roleMappingEmptyState = (
|
||||
<EuiPanel paddingSize="l" color="subdued" hasBorder={false}>
|
||||
|
@ -63,8 +77,9 @@ export const RoleMappings: React.FC = () => {
|
|||
accessItemKey="engines"
|
||||
accessHeader={ROLE_MAPPINGS_ENGINE_ACCESS_HEADING}
|
||||
addMappingButton={addMappingButton}
|
||||
getRoleMappingPath={generateRoleMappingPath}
|
||||
initializeRoleMapping={initializeRoleMapping}
|
||||
shouldShowAuthProvider={multipleAuthProvidersConfig}
|
||||
handleDeleteMapping={handleDeleteMapping}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -72,6 +87,8 @@ export const RoleMappings: React.FC = () => {
|
|||
<>
|
||||
<SetPageChrome trail={[ROLE_MAPPINGS_TITLE]} />
|
||||
<EuiPageHeader pageTitle={ROLE_MAPPINGS_TITLE} description={ROLE_MAPPINGS_DESCRIPTION} />
|
||||
|
||||
{roleMappingFlyoutOpen && <RoleMapping />}
|
||||
<EuiPageContent hasShadow={false} hasBorder={roleMappings.length > 0}>
|
||||
<EuiPageContentBody>
|
||||
<FlashMessages />
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__';
|
||||
import { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__';
|
||||
import { LogicMounter } from '../../../__mocks__/kea.mock';
|
||||
|
||||
import { engines } from '../../__mocks__/engines.mock';
|
||||
|
@ -13,20 +13,25 @@ import { engines } from '../../__mocks__/engines.mock';
|
|||
import { nextTick } from '@kbn/test/jest';
|
||||
|
||||
import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
|
||||
import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
|
||||
import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
|
||||
|
||||
import { RoleMappingsLogic } from './role_mappings_logic';
|
||||
|
||||
describe('RoleMappingsLogic', () => {
|
||||
const { http } = mockHttpValues;
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
|
||||
const {
|
||||
clearFlashMessages,
|
||||
flashAPIErrors,
|
||||
setSuccessMessage,
|
||||
setErrorMessage,
|
||||
} = mockFlashMessageHelpers;
|
||||
const { mount } = new LogicMounter(RoleMappingsLogic);
|
||||
const DEFAULT_VALUES = {
|
||||
attributes: [],
|
||||
availableAuthProviders: [],
|
||||
elasticsearchRoles: [],
|
||||
roleMapping: null,
|
||||
roleMappingFlyoutOpen: false,
|
||||
roleMappings: [],
|
||||
roleType: 'owner',
|
||||
attributeValue: '',
|
||||
|
@ -38,6 +43,7 @@ describe('RoleMappingsLogic', () => {
|
|||
selectedEngines: new Set(),
|
||||
accessAllEngines: true,
|
||||
selectedAuthProviders: [ANY_AUTH_PROVIDER],
|
||||
selectedOptions: [],
|
||||
};
|
||||
|
||||
const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [asRoleMapping] };
|
||||
|
@ -87,6 +93,10 @@ describe('RoleMappingsLogic', () => {
|
|||
attributeValue: 'superuser',
|
||||
elasticsearchRoles: mappingServerProps.elasticsearchRoles,
|
||||
selectedEngines: new Set(engines.map((e) => e.name)),
|
||||
selectedOptions: [
|
||||
{ label: engines[0].name, value: engines[0].name },
|
||||
{ label: engines[1].name, value: engines[1].name },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -134,21 +144,21 @@ describe('RoleMappingsLogic', () => {
|
|||
});
|
||||
|
||||
it('handles adding an engine to selected engines', () => {
|
||||
RoleMappingsLogic.actions.handleEngineSelectionChange(otherEngine.name, true);
|
||||
RoleMappingsLogic.actions.handleEngineSelectionChange([engine.name, otherEngine.name]);
|
||||
|
||||
expect(RoleMappingsLogic.values.selectedEngines).toEqual(
|
||||
new Set([engine.name, otherEngine.name])
|
||||
);
|
||||
});
|
||||
it('handles removing an engine from selected engines', () => {
|
||||
RoleMappingsLogic.actions.handleEngineSelectionChange(otherEngine.name, false);
|
||||
RoleMappingsLogic.actions.handleEngineSelectionChange([engine.name]);
|
||||
|
||||
expect(RoleMappingsLogic.values.selectedEngines).toEqual(new Set([engine.name]));
|
||||
});
|
||||
});
|
||||
|
||||
it('handleAccessAllEnginesChange', () => {
|
||||
RoleMappingsLogic.actions.handleAccessAllEnginesChange();
|
||||
RoleMappingsLogic.actions.handleAccessAllEnginesChange(false);
|
||||
|
||||
expect(RoleMappingsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
|
@ -250,6 +260,25 @@ describe('RoleMappingsLogic', () => {
|
|||
expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('openRoleMappingFlyout', () => {
|
||||
mount(mappingServerProps);
|
||||
RoleMappingsLogic.actions.openRoleMappingFlyout();
|
||||
|
||||
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('closeRoleMappingFlyout', () => {
|
||||
mount({
|
||||
...mappingServerProps,
|
||||
roleMappingFlyoutOpen: true,
|
||||
});
|
||||
RoleMappingsLogic.actions.closeRoleMappingFlyout();
|
||||
|
||||
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(false);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
|
@ -302,12 +331,12 @@ describe('RoleMappingsLogic', () => {
|
|||
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
|
||||
});
|
||||
|
||||
it('redirects when there is a 404 status', async () => {
|
||||
it('shows error when there is a 404 status', async () => {
|
||||
http.get.mockReturnValue(Promise.reject({ status: 404 }));
|
||||
RoleMappingsLogic.actions.initializeRoleMapping();
|
||||
await nextTick();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalled();
|
||||
expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -322,8 +351,12 @@ describe('RoleMappingsLogic', () => {
|
|||
engines: [],
|
||||
};
|
||||
|
||||
it('calls API and navigates when new mapping', async () => {
|
||||
it('calls API and refreshes list when new mapping', async () => {
|
||||
mount(mappingsServerProps);
|
||||
const initializeRoleMappingsSpy = jest.spyOn(
|
||||
RoleMappingsLogic.actions,
|
||||
'initializeRoleMappings'
|
||||
);
|
||||
|
||||
http.post.mockReturnValue(Promise.resolve(mappingServerProps));
|
||||
RoleMappingsLogic.actions.handleSaveMapping();
|
||||
|
@ -333,11 +366,15 @@ describe('RoleMappingsLogic', () => {
|
|||
});
|
||||
await nextTick();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalled();
|
||||
expect(initializeRoleMappingsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls API and navigates when existing mapping', async () => {
|
||||
it('calls API and refreshes list when existing mapping', async () => {
|
||||
mount(mappingServerProps);
|
||||
const initializeRoleMappingsSpy = jest.spyOn(
|
||||
RoleMappingsLogic.actions,
|
||||
'initializeRoleMappings'
|
||||
);
|
||||
|
||||
http.put.mockReturnValue(Promise.resolve(mappingServerProps));
|
||||
RoleMappingsLogic.actions.handleSaveMapping();
|
||||
|
@ -347,7 +384,7 @@ describe('RoleMappingsLogic', () => {
|
|||
});
|
||||
await nextTick();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalled();
|
||||
expect(initializeRoleMappingsSpy).toHaveBeenCalled();
|
||||
expect(setSuccessMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -383,6 +420,7 @@ describe('RoleMappingsLogic', () => {
|
|||
|
||||
describe('handleDeleteMapping', () => {
|
||||
let confirmSpy: any;
|
||||
const roleMappingId = 'r1';
|
||||
|
||||
beforeEach(() => {
|
||||
confirmSpy = jest.spyOn(window, 'confirm');
|
||||
|
@ -393,30 +431,26 @@ describe('RoleMappingsLogic', () => {
|
|||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('returns when no mapping', () => {
|
||||
RoleMappingsLogic.actions.handleDeleteMapping();
|
||||
|
||||
expect(http.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls API and navigates', async () => {
|
||||
it('calls API and refreshes list', async () => {
|
||||
mount(mappingServerProps);
|
||||
http.delete.mockReturnValue(Promise.resolve({}));
|
||||
RoleMappingsLogic.actions.handleDeleteMapping();
|
||||
|
||||
expect(http.delete).toHaveBeenCalledWith(
|
||||
`/api/app_search/role_mappings/${asRoleMapping.id}`
|
||||
const initializeRoleMappingsSpy = jest.spyOn(
|
||||
RoleMappingsLogic.actions,
|
||||
'initializeRoleMappings'
|
||||
);
|
||||
http.delete.mockReturnValue(Promise.resolve({}));
|
||||
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
|
||||
|
||||
expect(http.delete).toHaveBeenCalledWith(`/api/app_search/role_mappings/${roleMappingId}`);
|
||||
await nextTick();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalled();
|
||||
expect(initializeRoleMappingsSpy).toHaveBeenCalled();
|
||||
expect(setSuccessMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles error', async () => {
|
||||
mount(mappingServerProps);
|
||||
http.delete.mockReturnValue(Promise.reject('this is an error'));
|
||||
RoleMappingsLogic.actions.handleDeleteMapping();
|
||||
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
|
||||
await nextTick();
|
||||
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
|
||||
|
@ -425,7 +459,7 @@ describe('RoleMappingsLogic', () => {
|
|||
it('will do nothing if not confirmed', () => {
|
||||
mount(mappingServerProps);
|
||||
jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
|
||||
RoleMappingsLogic.actions.handleDeleteMapping();
|
||||
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
|
||||
|
||||
expect(http.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -7,16 +7,17 @@
|
|||
|
||||
import { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
clearFlashMessages,
|
||||
flashAPIErrors,
|
||||
setSuccessMessage,
|
||||
setErrorMessage,
|
||||
} from '../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
|
||||
import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
|
||||
import { AttributeName } from '../../../shared/types';
|
||||
import { ROLE_MAPPINGS_PATH } from '../../routes';
|
||||
import { ASRoleMapping, RoleTypes } from '../../types';
|
||||
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
|
||||
import { Engine } from '../engine/types';
|
||||
|
@ -49,28 +50,24 @@ const getFirstAttributeValue = (roleMapping: ASRoleMapping) =>
|
|||
Object.entries(roleMapping.rules)[0][1] as AttributeName;
|
||||
|
||||
interface RoleMappingsActions {
|
||||
handleAccessAllEnginesChange(): void;
|
||||
handleAccessAllEnginesChange(selected: boolean): { selected: boolean };
|
||||
handleAuthProviderChange(value: string[]): { value: string[] };
|
||||
handleAttributeSelectorChange(
|
||||
value: AttributeName,
|
||||
firstElasticsearchRole: string
|
||||
): { value: AttributeName; firstElasticsearchRole: string };
|
||||
handleAttributeValueChange(value: string): { value: string };
|
||||
handleDeleteMapping(): void;
|
||||
handleEngineSelectionChange(
|
||||
engineName: string,
|
||||
selected: boolean
|
||||
): {
|
||||
engineName: string;
|
||||
selected: boolean;
|
||||
};
|
||||
handleDeleteMapping(roleMappingId: string): { roleMappingId: string };
|
||||
handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] };
|
||||
handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes };
|
||||
handleSaveMapping(): void;
|
||||
initializeRoleMapping(roleId?: string): { roleId?: string };
|
||||
initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
|
||||
initializeRoleMappings(): void;
|
||||
resetState(): void;
|
||||
setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails;
|
||||
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
|
||||
openRoleMappingFlyout(): void;
|
||||
closeRoleMappingFlyout(): void;
|
||||
}
|
||||
|
||||
interface RoleMappingsValues {
|
||||
|
@ -89,6 +86,8 @@ interface RoleMappingsValues {
|
|||
roleType: RoleTypes;
|
||||
selectedAuthProviders: string[];
|
||||
selectedEngines: Set<string>;
|
||||
roleMappingFlyoutOpen: boolean;
|
||||
selectedOptions: EuiComboBoxOptionOption[];
|
||||
}
|
||||
|
||||
export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappingsActions>>({
|
||||
|
@ -98,21 +97,20 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
setRoleMappingData: (data: RoleMappingServerDetails) => data,
|
||||
handleAuthProviderChange: (value: string) => ({ value }),
|
||||
handleRoleChange: (roleType: RoleTypes) => ({ roleType }),
|
||||
handleEngineSelectionChange: (engineName: string, selected: boolean) => ({
|
||||
engineName,
|
||||
selected,
|
||||
}),
|
||||
handleEngineSelectionChange: (engineNames: string[]) => ({ engineNames }),
|
||||
handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({
|
||||
value,
|
||||
firstElasticsearchRole,
|
||||
}),
|
||||
handleAttributeValueChange: (value: string) => ({ value }),
|
||||
handleAccessAllEnginesChange: true,
|
||||
handleAccessAllEnginesChange: (selected: boolean) => ({ selected }),
|
||||
resetState: true,
|
||||
initializeRoleMappings: true,
|
||||
initializeRoleMapping: (roleId) => ({ roleId }),
|
||||
handleDeleteMapping: true,
|
||||
initializeRoleMapping: (roleMappingId) => ({ roleMappingId }),
|
||||
handleDeleteMapping: (roleMappingId: string) => ({ roleMappingId }),
|
||||
handleSaveMapping: true,
|
||||
openRoleMappingFlyout: true,
|
||||
closeRoleMappingFlyout: false,
|
||||
},
|
||||
reducers: {
|
||||
dataLoading: [
|
||||
|
@ -169,6 +167,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
{
|
||||
setRoleMappingData: (_, { roleMapping }) => roleMapping || null,
|
||||
resetState: () => null,
|
||||
closeRoleMappingFlyout: () => null,
|
||||
},
|
||||
],
|
||||
roleType: [
|
||||
|
@ -185,7 +184,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
setRoleMappingData: (_, { roleMapping }) =>
|
||||
roleMapping ? roleMapping.accessAllEngines : true,
|
||||
handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType),
|
||||
handleAccessAllEnginesChange: (accessAllEngines) => !accessAllEngines,
|
||||
handleAccessAllEnginesChange: (_, { selected }) => selected,
|
||||
},
|
||||
],
|
||||
attributeValue: [
|
||||
|
@ -197,6 +196,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
value === 'role' ? firstElasticsearchRole : '',
|
||||
handleAttributeValueChange: (_, { value }) => value,
|
||||
resetState: () => '',
|
||||
closeRoleMappingFlyout: () => '',
|
||||
},
|
||||
],
|
||||
attributeName: [
|
||||
|
@ -206,6 +206,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
roleMapping ? getFirstAttributeName(roleMapping) : 'username',
|
||||
handleAttributeSelectorChange: (_, { value }) => value,
|
||||
resetState: () => 'username',
|
||||
closeRoleMappingFlyout: () => 'username',
|
||||
},
|
||||
],
|
||||
selectedEngines: [
|
||||
|
@ -214,13 +215,9 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
setRoleMappingData: (_, { roleMapping }) =>
|
||||
roleMapping ? new Set(roleMapping.engines.map((engine) => engine.name)) : new Set(),
|
||||
handleAccessAllEnginesChange: () => new Set(),
|
||||
handleEngineSelectionChange: (engines, { engineName, selected }) => {
|
||||
const newSelectedEngineNames = new Set(engines as Set<string>);
|
||||
if (selected) {
|
||||
newSelectedEngineNames.add(engineName);
|
||||
} else {
|
||||
newSelectedEngineNames.delete(engineName);
|
||||
}
|
||||
handleEngineSelectionChange: (_, { engineNames }) => {
|
||||
const newSelectedEngineNames = new Set() as Set<string>;
|
||||
engineNames.forEach((engineName) => newSelectedEngineNames.add(engineName));
|
||||
|
||||
return newSelectedEngineNames;
|
||||
},
|
||||
|
@ -250,7 +247,27 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER],
|
||||
},
|
||||
],
|
||||
roleMappingFlyoutOpen: [
|
||||
false,
|
||||
{
|
||||
openRoleMappingFlyout: () => true,
|
||||
closeRoleMappingFlyout: () => false,
|
||||
initializeRoleMappings: () => false,
|
||||
initializeRoleMapping: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
selectors: ({ selectors }) => ({
|
||||
selectedOptions: [
|
||||
() => [selectors.selectedEngines, selectors.availableEngines],
|
||||
(selectedEngines, availableEngines) => {
|
||||
const selectedNames = Array.from(selectedEngines.values());
|
||||
return availableEngines
|
||||
.filter(({ name }: { name: string }) => selectedNames.includes(name))
|
||||
.map(({ name }: { name: string }) => ({ label: name, value: name }));
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions, values }) => ({
|
||||
initializeRoleMappings: async () => {
|
||||
const { http } = HttpLogic.values;
|
||||
|
@ -263,33 +280,31 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
initializeRoleMapping: async ({ roleId }) => {
|
||||
initializeRoleMapping: async ({ roleMappingId }) => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
const route = roleId
|
||||
? `/api/app_search/role_mappings/${roleId}`
|
||||
const route = roleMappingId
|
||||
? `/api/app_search/role_mappings/${roleMappingId}`
|
||||
: '/api/app_search/role_mappings/new';
|
||||
|
||||
try {
|
||||
const response = await http.get(route);
|
||||
actions.setRoleMappingData(response);
|
||||
} catch (e) {
|
||||
navigateToUrl(ROLE_MAPPINGS_PATH);
|
||||
flashAPIErrors(e);
|
||||
if (e.status === 404) {
|
||||
setErrorMessage(ROLE_MAPPING_NOT_FOUND);
|
||||
} else {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
handleDeleteMapping: async () => {
|
||||
const { roleMapping } = values;
|
||||
if (!roleMapping) return;
|
||||
|
||||
handleDeleteMapping: async ({ roleMappingId }) => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
const route = `/api/app_search/role_mappings/${roleMapping.id}`;
|
||||
const route = `/api/app_search/role_mappings/${roleMappingId}`;
|
||||
|
||||
if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) {
|
||||
try {
|
||||
await http.delete(route);
|
||||
navigateToUrl(ROLE_MAPPINGS_PATH);
|
||||
actions.initializeRoleMappings();
|
||||
setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
|
@ -298,7 +313,6 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
},
|
||||
handleSaveMapping: async () => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
|
||||
const {
|
||||
attributeName,
|
||||
|
@ -330,7 +344,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
|
||||
try {
|
||||
await request;
|
||||
navigateToUrl(ROLE_MAPPINGS_PATH);
|
||||
actions.initializeRoleMappings();
|
||||
setSuccessMessage(SUCCESS_MESSAGE);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
|
@ -339,5 +353,11 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
resetState: () => {
|
||||
clearFlashMessages();
|
||||
},
|
||||
closeRoleMappingFlyout: () => {
|
||||
clearFlashMessages();
|
||||
},
|
||||
openRoleMappingFlyout: () => {
|
||||
clearFlashMessages();
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,26 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
import { RoleMappings } from './role_mappings';
|
||||
import { RoleMappingsRouter } from './role_mappings_router';
|
||||
|
||||
describe('RoleMappingsRouter', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<RoleMappingsRouter />);
|
||||
|
||||
expect(wrapper.find(Switch)).toHaveLength(1);
|
||||
expect(wrapper.find(Route)).toHaveLength(3);
|
||||
expect(wrapper.find(RoleMapping)).toHaveLength(2);
|
||||
expect(wrapper.find(RoleMappings)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -1,29 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { ROLE_MAPPING_NEW_PATH, ROLE_MAPPING_PATH, ROLE_MAPPINGS_PATH } from '../../routes';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
import { RoleMappings } from './role_mappings';
|
||||
|
||||
export const RoleMappingsRouter: React.FC = () => (
|
||||
<Switch>
|
||||
<Route exact path={ROLE_MAPPING_NEW_PATH}>
|
||||
<RoleMapping isNew />
|
||||
</Route>
|
||||
<Route exact path={ROLE_MAPPINGS_PATH}>
|
||||
<RoleMappings />
|
||||
</Route>
|
||||
<Route path={ROLE_MAPPING_PATH}>
|
||||
<RoleMapping />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
|
@ -1,16 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { generateRoleMappingPath } from './utils';
|
||||
|
||||
describe('generateRoleMappingPath', () => {
|
||||
it('generates paths with roleId filled', () => {
|
||||
const roleId = 'role123';
|
||||
|
||||
expect(generateRoleMappingPath(roleId)).toEqual(`/role_mappings/${roleId}`);
|
||||
});
|
||||
});
|
|
@ -1,12 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ROLE_MAPPING_PATH } from '../../routes';
|
||||
import { generateEncodedPath } from '../../utils/encode_path_params';
|
||||
|
||||
export const generateRoleMappingPath = (roleId: string) =>
|
||||
generateEncodedPath(ROLE_MAPPING_PATH, { roleId });
|
|
@ -28,7 +28,7 @@ import { EnginesOverview } from './components/engines';
|
|||
import { ErrorConnecting } from './components/error_connecting';
|
||||
import { Library } from './components/library';
|
||||
import { MetaEngineCreation } from './components/meta_engine_creation';
|
||||
import { RoleMappingsRouter } from './components/role_mappings';
|
||||
import { RoleMappings } from './components/role_mappings';
|
||||
import { SetupGuide } from './components/setup_guide';
|
||||
|
||||
import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './';
|
||||
|
@ -106,13 +106,13 @@ describe('AppSearchConfigured', () => {
|
|||
it('renders RoleMappings when canViewRoleMappings is true', () => {
|
||||
setMockValues({ myRole: { canViewRoleMappings: true } });
|
||||
rerender(wrapper);
|
||||
expect(wrapper.find(RoleMappingsRouter)).toHaveLength(1);
|
||||
expect(wrapper.find(RoleMappings)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not render RoleMappings when user canViewRoleMappings is false', () => {
|
||||
setMockValues({ myRole: { canManageEngines: false } });
|
||||
rerender(wrapper);
|
||||
expect(wrapper.find(RoleMappingsRouter)).toHaveLength(0);
|
||||
expect(wrapper.find(RoleMappings)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ import { ErrorConnecting } from './components/error_connecting';
|
|||
import { KibanaHeaderActions } from './components/layout/kibana_header_actions';
|
||||
import { Library } from './components/library';
|
||||
import { MetaEngineCreation } from './components/meta_engine_creation';
|
||||
import { RoleMappingsRouter } from './components/role_mappings';
|
||||
import { RoleMappings } from './components/role_mappings';
|
||||
import { Settings, SETTINGS_TITLE } from './components/settings';
|
||||
import { SetupGuide } from './components/setup_guide';
|
||||
import {
|
||||
|
@ -112,7 +112,7 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) =
|
|||
</Route>
|
||||
{canViewRoleMappings && (
|
||||
<Route path={ROLE_MAPPINGS_PATH}>
|
||||
<RoleMappingsRouter />
|
||||
<RoleMappings />
|
||||
</Route>
|
||||
)}
|
||||
{canManageEngines && (
|
||||
|
|
|
@ -16,8 +16,6 @@ export const SETTINGS_PATH = '/settings';
|
|||
export const CREDENTIALS_PATH = '/credentials';
|
||||
|
||||
export const ROLE_MAPPINGS_PATH = '/role_mappings';
|
||||
export const ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/:roleId`;
|
||||
export const ROLE_MAPPING_NEW_PATH = `${ROLE_MAPPINGS_PATH}/new`;
|
||||
|
||||
export const ENGINES_PATH = '/engines';
|
||||
export const ENGINE_CREATION_PATH = '/engine_creation';
|
||||
|
|
|
@ -52,6 +52,6 @@ export interface ASRoleMapping extends RoleMapping {
|
|||
}
|
||||
|
||||
export interface AdvanceRoleType {
|
||||
type: RoleTypes;
|
||||
id: RoleTypes;
|
||||
description: string;
|
||||
}
|
||||
|
|
|
@ -1,22 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiButtonTo } from '../react_router_helpers';
|
||||
|
||||
import { AddRoleMappingButton } from './add_role_mapping_button';
|
||||
|
||||
describe('AddRoleMappingButton', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<AddRoleMappingButton path="/foo" />);
|
||||
|
||||
expect(wrapper.find(EuiButtonTo)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -1,22 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiButtonTo } from '../react_router_helpers';
|
||||
|
||||
import { ADD_ROLE_MAPPING_BUTTON } from './constants';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const AddRoleMappingButton: React.FC<Props> = ({ path }) => (
|
||||
<EuiButtonTo to={path} fill>
|
||||
{ADD_ROLE_MAPPING_BUTTON}
|
||||
</EuiButtonTo>
|
||||
);
|
|
@ -114,6 +114,14 @@ describe('AttributeSelector', () => {
|
|||
expect(handleAuthProviderChange).toHaveBeenCalledWith(['kbn_saml']);
|
||||
});
|
||||
|
||||
it('should call the "handleAuthProviderChange" prop with fallback when a value not present', () => {
|
||||
const wrapper = shallow(<AttributeSelector {...baseProps} />);
|
||||
const select = findAuthProvidersSelect(wrapper);
|
||||
select.simulate('change', [{ label: 'kbn_saml' }]);
|
||||
|
||||
expect(handleAuthProviderChange).toHaveBeenCalledWith(['']);
|
||||
});
|
||||
|
||||
it('should call the "handleAttributeSelectorChange" prop when a value is selected', () => {
|
||||
const wrapper = shallow(<AttributeSelector {...baseProps} />);
|
||||
const select = wrapper.find('[data-test-subj="ExternalAttributeSelect"]');
|
||||
|
|
|
@ -11,13 +11,8 @@ import {
|
|||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { AttributeName, AttributeExamples } from '../types';
|
||||
|
@ -27,10 +22,6 @@ import {
|
|||
ANY_AUTH_PROVIDER_OPTION_LABEL,
|
||||
AUTH_ANY_PROVIDER_LABEL,
|
||||
AUTH_INDIVIDUAL_PROVIDER_LABEL,
|
||||
ATTRIBUTE_SELECTOR_TITLE,
|
||||
AUTH_PROVIDER_LABEL,
|
||||
EXTERNAL_ATTRIBUTE_LABEL,
|
||||
ATTRIBUTE_VALUE_LABEL,
|
||||
} from './constants';
|
||||
|
||||
interface Props {
|
||||
|
@ -100,80 +91,65 @@ export const AttributeSelector: React.FC<Props> = ({
|
|||
handleAuthProviderChange = () => null,
|
||||
}) => {
|
||||
return (
|
||||
<EuiPanel data-test-subj="AttributeSelector" hasShadow={false} color="subdued" paddingSize="l">
|
||||
<EuiTitle size="s">
|
||||
<h3>{ATTRIBUTE_SELECTOR_TITLE}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<div data-test-subj="AttributeSelector">
|
||||
{availableAuthProviders && multipleAuthProvidersConfig && (
|
||||
<EuiFlexGroup alignItems="stretch">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label={AUTH_PROVIDER_LABEL} fullWidth>
|
||||
<EuiComboBox
|
||||
data-test-subj="AuthProviderSelect"
|
||||
selectedOptions={getSelectedOptions(selectedAuthProviders, availableAuthProviders)}
|
||||
options={getAuthProviderOptions(availableAuthProviders)}
|
||||
onChange={(options) => {
|
||||
handleAuthProviderChange(options.map((o) => (o as ChildOption).value));
|
||||
}}
|
||||
fullWidth
|
||||
isDisabled={disabled}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
</EuiFlexGroup>
|
||||
<EuiFormRow label="Auth Provider" fullWidth>
|
||||
<EuiComboBox
|
||||
data-test-subj="AuthProviderSelect"
|
||||
selectedOptions={getSelectedOptions(selectedAuthProviders, availableAuthProviders)}
|
||||
options={getAuthProviderOptions(availableAuthProviders)}
|
||||
onChange={(options) => {
|
||||
handleAuthProviderChange(options.map((o) => o.value || ''));
|
||||
}}
|
||||
fullWidth
|
||||
isDisabled={disabled}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFlexGroup alignItems="stretch">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label={EXTERNAL_ATTRIBUTE_LABEL} fullWidth>
|
||||
<EuiSelect
|
||||
name="external-attribute"
|
||||
data-test-subj="ExternalAttributeSelect"
|
||||
value={attributeName}
|
||||
required
|
||||
options={attributes.map((attribute) => ({ value: attribute, text: attribute }))}
|
||||
onChange={(e) => {
|
||||
handleAttributeSelectorChange(e.target.value, elasticsearchRoles[0]);
|
||||
}}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow label={ATTRIBUTE_VALUE_LABEL} fullWidth>
|
||||
{attributeName === 'role' ? (
|
||||
<EuiSelect
|
||||
value={attributeValue}
|
||||
name="elasticsearch-role"
|
||||
data-test-subj="ElasticsearchRoleSelect"
|
||||
required
|
||||
options={elasticsearchRoles.map((elasticsearchRole) => ({
|
||||
value: elasticsearchRole,
|
||||
text: elasticsearchRole,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
handleAttributeValueChange(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<EuiFieldText
|
||||
value={attributeValue}
|
||||
name="attribute-value"
|
||||
placeholder={attributeValueExamples[attributeName]}
|
||||
onChange={(e) => {
|
||||
handleAttributeValueChange(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiFormRow label="External Attribute" fullWidth>
|
||||
<EuiSelect
|
||||
name="external-attribute"
|
||||
data-test-subj="ExternalAttributeSelect"
|
||||
value={attributeName}
|
||||
required
|
||||
options={attributes.map((attribute) => ({ value: attribute, text: attribute }))}
|
||||
onChange={(e) => {
|
||||
handleAttributeSelectorChange(e.target.value, elasticsearchRoles[0]);
|
||||
}}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label="Attribute Value" fullWidth>
|
||||
{attributeName === 'role' ? (
|
||||
<EuiSelect
|
||||
value={attributeValue}
|
||||
name="elasticsearch-role"
|
||||
data-test-subj="ElasticsearchRoleSelect"
|
||||
required
|
||||
options={elasticsearchRoles.map((elasticsearchRole) => ({
|
||||
value: elasticsearchRole,
|
||||
text: elasticsearchRole,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
handleAttributeValueChange(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<EuiFieldText
|
||||
value={attributeValue}
|
||||
name="attribute-value"
|
||||
placeholder={attributeValueExamples[attributeName]}
|
||||
onChange={(e) => {
|
||||
handleAttributeValueChange(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -132,3 +132,62 @@ export const ROLE_MAPPINGS_DESCRIPTION = i18n.translate(
|
|||
'Define role mappings for elasticsearch-native and elasticsearch-saml authentication.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROLE_MAPPING_NOT_FOUND = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.notFoundMessage',
|
||||
{
|
||||
defaultMessage: 'No matching Role mapping found.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROLE_MAPPING_FLYOUT_CREATE_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.flyoutCreateTitle',
|
||||
{
|
||||
defaultMessage: 'Create a role mapping',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROLE_MAPPING_FLYOUT_UPDATE_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.flyoutUpdateTitle',
|
||||
{
|
||||
defaultMessage: 'Update role mapping',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROLE_MAPPING_FLYOUT_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.flyoutDescription',
|
||||
{
|
||||
defaultMessage: 'Assign roles and permissions based on user attributes',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROLE_MAPPING_ADD_BUTTON = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.roleMappingAddButton',
|
||||
{
|
||||
defaultMessage: 'Add mapping',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROLE_MAPPING_FLYOUT_CREATE_BUTTON = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.roleMappingFlyoutCreateButton',
|
||||
{
|
||||
defaultMessage: 'Create mapping',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROLE_MAPPING_FLYOUT_UPDATE_BUTTON = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.roleMappingFlyoutUpdateButton',
|
||||
{
|
||||
defaultMessage: 'Update mapping',
|
||||
}
|
||||
);
|
||||
|
||||
export const SAVE_ROLE_MAPPING = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel',
|
||||
{ defaultMessage: 'Save role mapping' }
|
||||
);
|
||||
|
||||
export const UPDATE_ROLE_MAPPING = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel',
|
||||
{ defaultMessage: 'Update role mapping' }
|
||||
);
|
||||
|
|
|
@ -1,31 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiButton, EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import { DeleteMappingCallout } from './delete_mapping_callout';
|
||||
|
||||
describe('DeleteMappingCallout', () => {
|
||||
const handleDeleteMapping = jest.fn();
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<DeleteMappingCallout handleDeleteMapping={handleDeleteMapping} />);
|
||||
|
||||
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiButton).prop('onClick')).toEqual(handleDeleteMapping);
|
||||
});
|
||||
|
||||
it('handles button click', () => {
|
||||
const wrapper = shallow(<DeleteMappingCallout handleDeleteMapping={handleDeleteMapping} />);
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
|
||||
expect(handleDeleteMapping).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,29 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiButton, EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
DELETE_ROLE_MAPPING_TITLE,
|
||||
DELETE_ROLE_MAPPING_DESCRIPTION,
|
||||
DELETE_ROLE_MAPPING_BUTTON,
|
||||
} from './constants';
|
||||
|
||||
interface Props {
|
||||
handleDeleteMapping(): void;
|
||||
}
|
||||
|
||||
export const DeleteMappingCallout: React.FC<Props> = ({ handleDeleteMapping }) => (
|
||||
<EuiCallOut color="danger" iconType="alert" title={DELETE_ROLE_MAPPING_TITLE}>
|
||||
<p>{DELETE_ROLE_MAPPING_DESCRIPTION}</p>
|
||||
<EuiButton color="danger" fill onClick={handleDeleteMapping}>
|
||||
{DELETE_ROLE_MAPPING_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
);
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { AddRoleMappingButton } from './add_role_mapping_button';
|
||||
export { AttributeSelector } from './attribute_selector';
|
||||
export { DeleteMappingCallout } from './delete_mapping_callout';
|
||||
export { RoleMappingsTable } from './role_mappings_table';
|
||||
export { RoleOptionLabel } from './role_option_label';
|
||||
export { RoleSelector } from './role_selector';
|
||||
export { RoleMappingFlyout } from './role_mapping_flyout';
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiFlyout } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
ROLE_MAPPING_FLYOUT_CREATE_TITLE,
|
||||
ROLE_MAPPING_FLYOUT_UPDATE_TITLE,
|
||||
ROLE_MAPPING_FLYOUT_CREATE_BUTTON,
|
||||
ROLE_MAPPING_FLYOUT_UPDATE_BUTTON,
|
||||
} from './constants';
|
||||
import { RoleMappingFlyout } from './role_mapping_flyout';
|
||||
|
||||
describe('RoleMappingFlyout', () => {
|
||||
const closeRoleMappingFlyout = jest.fn();
|
||||
const handleSaveMapping = jest.fn();
|
||||
|
||||
const props = {
|
||||
isNew: true,
|
||||
disabled: false,
|
||||
closeRoleMappingFlyout,
|
||||
handleSaveMapping,
|
||||
};
|
||||
|
||||
it('renders for new mapping', () => {
|
||||
const wrapper = shallow(
|
||||
<RoleMappingFlyout {...props}>
|
||||
<div />
|
||||
</RoleMappingFlyout>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiFlyout)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="FlyoutTitle"]').prop('children')).toEqual(
|
||||
ROLE_MAPPING_FLYOUT_CREATE_TITLE
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="FlyoutButton"]').prop('children')).toEqual(
|
||||
ROLE_MAPPING_FLYOUT_CREATE_BUTTON
|
||||
);
|
||||
});
|
||||
|
||||
it('renders for existing mapping', () => {
|
||||
const wrapper = shallow(
|
||||
<RoleMappingFlyout {...props} isNew={false}>
|
||||
<div />
|
||||
</RoleMappingFlyout>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiFlyout)).toHaveLength(1);
|
||||
expect(wrapper.find('[data-test-subj="FlyoutTitle"]').prop('children')).toEqual(
|
||||
ROLE_MAPPING_FLYOUT_UPDATE_TITLE
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="FlyoutButton"]').prop('children')).toEqual(
|
||||
ROLE_MAPPING_FLYOUT_UPDATE_BUTTON
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiPortal,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CANCEL_BUTTON_LABEL } from '../../shared/constants/actions';
|
||||
|
||||
import {
|
||||
ROLE_MAPPING_FLYOUT_CREATE_TITLE,
|
||||
ROLE_MAPPING_FLYOUT_UPDATE_TITLE,
|
||||
ROLE_MAPPING_FLYOUT_DESCRIPTION,
|
||||
ROLE_MAPPING_FLYOUT_CREATE_BUTTON,
|
||||
ROLE_MAPPING_FLYOUT_UPDATE_BUTTON,
|
||||
} from './constants';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
isNew: boolean;
|
||||
disabled: boolean;
|
||||
closeRoleMappingFlyout(): void;
|
||||
handleSaveMapping(): void;
|
||||
}
|
||||
|
||||
export const RoleMappingFlyout: React.FC<Props> = ({
|
||||
children,
|
||||
isNew,
|
||||
disabled,
|
||||
closeRoleMappingFlyout,
|
||||
handleSaveMapping,
|
||||
}) => (
|
||||
<EuiPortal>
|
||||
<EuiFlyout
|
||||
ownFocus
|
||||
onClose={closeRoleMappingFlyout}
|
||||
size="s"
|
||||
aria-labelledby="flyoutLargeTitle"
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="flyoutLargeTitle" data-test-subj="FlyoutTitle">
|
||||
{isNew ? ROLE_MAPPING_FLYOUT_CREATE_TITLE : ROLE_MAPPING_FLYOUT_UPDATE_TITLE}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiText size="xs">
|
||||
<p>{ROLE_MAPPING_FLYOUT_DESCRIPTION}</p>
|
||||
</EuiText>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{children}
|
||||
<EuiSpacer />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={closeRoleMappingFlyout}>{CANCEL_BUTTON_LABEL}</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
disabled={disabled}
|
||||
onClick={handleSaveMapping}
|
||||
fill
|
||||
data-test-subj="FlyoutButton"
|
||||
>
|
||||
{isNew ? ROLE_MAPPING_FLYOUT_CREATE_BUTTON : ROLE_MAPPING_FLYOUT_UPDATE_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
|
@ -18,7 +18,8 @@ import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants';
|
|||
import { RoleMappingsTable } from './role_mappings_table';
|
||||
|
||||
describe('RoleMappingsTable', () => {
|
||||
const getRoleMappingPath = jest.fn();
|
||||
const initializeRoleMapping = jest.fn();
|
||||
const handleDeleteMapping = jest.fn();
|
||||
const roleMappings = [
|
||||
{
|
||||
...wsRoleMapping,
|
||||
|
@ -36,7 +37,8 @@ describe('RoleMappingsTable', () => {
|
|||
roleMappings,
|
||||
addMappingButton: <button />,
|
||||
shouldShowAuthProvider: true,
|
||||
getRoleMappingPath,
|
||||
initializeRoleMapping,
|
||||
handleDeleteMapping,
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
|
@ -63,6 +65,20 @@ describe('RoleMappingsTable', () => {
|
|||
expect(wrapper.find(EuiTableRow)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles manage click', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
wrapper.find('[data-test-subj="ManageButton"]').simulate('click');
|
||||
|
||||
expect(initializeRoleMapping).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles delete click', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
wrapper.find('[data-test-subj="DeleteButton"]').simulate('click');
|
||||
|
||||
expect(handleDeleteMapping).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles input change with special chars', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
const input = wrapper.find(EuiFieldSearch);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { Fragment, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFieldSearch,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -25,8 +26,7 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { ASRoleMapping } from '../../app_search/types';
|
||||
import { WSRoleMapping } from '../../workplace_search/types';
|
||||
import { MANAGE_BUTTON_LABEL } from '../constants';
|
||||
import { EuiLinkTo } from '../react_router_helpers';
|
||||
import { MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants';
|
||||
import { RoleRules } from '../types';
|
||||
|
||||
import './role_mappings_table.scss';
|
||||
|
@ -57,7 +57,8 @@ interface Props {
|
|||
addMappingButton: React.ReactNode;
|
||||
accessAllEngines?: boolean;
|
||||
shouldShowAuthProvider?: boolean;
|
||||
getRoleMappingPath(roleId: string): string;
|
||||
initializeRoleMapping(roleMappingId: string): void;
|
||||
handleDeleteMapping(roleMappingId: string): void;
|
||||
}
|
||||
|
||||
const MAX_CELL_WIDTH = 24;
|
||||
|
@ -72,8 +73,9 @@ export const RoleMappingsTable: React.FC<Props> = ({
|
|||
accessHeader,
|
||||
roleMappings,
|
||||
addMappingButton,
|
||||
getRoleMappingPath,
|
||||
shouldShowAuthProvider,
|
||||
initializeRoleMapping,
|
||||
handleDeleteMapping,
|
||||
}) => {
|
||||
const [filterValue, updateValue] = useState('');
|
||||
|
||||
|
@ -96,6 +98,23 @@ export const RoleMappingsTable: React.FC<Props> = ({
|
|||
const getFirstAttributeName = (rules: RoleRules): string => Object.entries(rules)[0][0];
|
||||
const getFirstAttributeValue = (rules: RoleRules): string => Object.entries(rules)[0][1];
|
||||
|
||||
const rowActions = (id: string) => (
|
||||
<>
|
||||
<EuiButtonIcon
|
||||
onClick={() => initializeRoleMapping(id)}
|
||||
iconType="pencil"
|
||||
aria-label={MANAGE_BUTTON_LABEL}
|
||||
data-test-subj="ManageButton"
|
||||
/>{' '}
|
||||
<EuiButtonIcon
|
||||
onClick={() => handleDeleteMapping(id)}
|
||||
iconType="trash"
|
||||
aria-label={DELETE_BUTTON_LABEL}
|
||||
data-test-subj="DeleteButton"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
|
@ -155,7 +174,7 @@ export const RoleMappingsTable: React.FC<Props> = ({
|
|||
</EuiTableRowCell>
|
||||
)}
|
||||
<EuiTableRowCell align="right">
|
||||
{id && <EuiLinkTo to={getRoleMappingPath(id)}>{MANAGE_BUTTON_LABEL}</EuiLinkTo>}
|
||||
{id && rowActions(id)}
|
||||
{toolTip && <EuiIconTip position="left" content={toolTip.content} />}
|
||||
</EuiTableRowCell>
|
||||
</EuiTableRow>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
|
||||
import { EuiText, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { RoleOptionLabel } from './role_option_label';
|
||||
|
||||
describe('RoleOptionLabel', () => {
|
||||
it('renders with capitalized label ', () => {
|
||||
const wrapper = shallow(<RoleOptionLabel label="foO" description="bar" />);
|
||||
|
||||
expect(wrapper.find(EuiText)).toHaveLength(2);
|
||||
expect(wrapper.find(EuiText).first().prop('children')).toBe('Foo');
|
||||
expect(wrapper.find(EuiSpacer)).toHaveLength(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { EuiSpacer, EuiText } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const RoleOptionLabel: React.FC<Props> = ({ label, description }) => (
|
||||
<>
|
||||
<EuiText size="s">{label.charAt(0).toUpperCase() + label.toLowerCase().slice(1)}</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
|
@ -9,46 +9,38 @@ import React from 'react';
|
|||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiRadio } from '@elastic/eui';
|
||||
import { EuiRadioGroup } from '@elastic/eui';
|
||||
|
||||
import { RoleSelector } from './role_selector';
|
||||
|
||||
describe('RoleSelector', () => {
|
||||
const onChange = jest.fn();
|
||||
const roleOptions = [
|
||||
{
|
||||
id: 'user',
|
||||
description: 'User',
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
disabled: false,
|
||||
disabledText: 'Disabled',
|
||||
roleType: 'user',
|
||||
roleTypeOption: 'option',
|
||||
description: 'This a thing',
|
||||
roleOptions,
|
||||
label: 'This a thing',
|
||||
onChange,
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<RoleSelector {...props} />);
|
||||
|
||||
expect(wrapper.find(EuiRadio)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiRadioGroup)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('calls method on change', () => {
|
||||
const wrapper = shallow(<RoleSelector {...props} />);
|
||||
const radio = wrapper.find(EuiRadio);
|
||||
const radio = wrapper.find(EuiRadioGroup);
|
||||
radio.simulate('change', { target: { value: 'bar' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders callout when disabled', () => {
|
||||
const wrapper = shallow(<RoleSelector {...props} disabled />);
|
||||
|
||||
expect(wrapper.find(EuiRadio).prop('checked')).toEqual(false);
|
||||
});
|
||||
|
||||
it('sets checked attribute on radio when option matched type', () => {
|
||||
const wrapper = shallow(<RoleSelector {...props} roleTypeOption="user" />);
|
||||
const radio = wrapper.find(EuiRadio);
|
||||
|
||||
expect(radio.prop('checked')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,45 +7,43 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { startCase } from 'lodash';
|
||||
import { EuiFormRow, EuiRadioGroup } from '@elastic/eui';
|
||||
|
||||
import { EuiFormRow, EuiRadio, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { RoleOptionLabel } from './role_option_label';
|
||||
|
||||
interface RoleOption {
|
||||
id: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
disabledText?: string;
|
||||
roleType?: string;
|
||||
roleTypeOption: string;
|
||||
description: string;
|
||||
onChange(roleTypeOption: string): void;
|
||||
roleOptions: RoleOption[];
|
||||
label: string;
|
||||
onChange(id: string): void;
|
||||
}
|
||||
|
||||
export const RoleSelector: React.FC<Props> = ({
|
||||
disabled,
|
||||
roleType,
|
||||
roleTypeOption,
|
||||
description,
|
||||
onChange,
|
||||
}) => (
|
||||
<EuiFormRow>
|
||||
<EuiRadio
|
||||
disabled={disabled}
|
||||
id={roleTypeOption}
|
||||
checked={roleTypeOption === roleType}
|
||||
onChange={() => {
|
||||
onChange(roleTypeOption);
|
||||
}}
|
||||
label={
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h4 className="usersLayout__users--roletype">{startCase(roleTypeOption)}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="s">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
export const RoleSelector: React.FC<Props> = ({ label, roleType, roleOptions, onChange }) => {
|
||||
const options = roleOptions.map(({ id, description, disabled }) => ({
|
||||
id,
|
||||
label: <RoleOptionLabel label={id} description={description} />,
|
||||
disabled,
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiRadioGroup
|
||||
options={options}
|
||||
idSelected={roleOptions.filter((r) => r.id === roleType)[0].id}
|
||||
onChange={(id) => {
|
||||
onChange(id);
|
||||
}}
|
||||
legend={{
|
||||
children: <span>{label}</span>,
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -37,7 +37,7 @@ import { ErrorState } from './views/error_state';
|
|||
import { GroupsRouter } from './views/groups';
|
||||
import { GroupSubNav } from './views/groups/components/group_sub_nav';
|
||||
import { Overview } from './views/overview';
|
||||
import { RoleMappingsRouter } from './views/role_mappings';
|
||||
import { RoleMappings } from './views/role_mappings';
|
||||
import { Security } from './views/security';
|
||||
import { SettingsRouter } from './views/settings';
|
||||
import { SettingsSubNav } from './views/settings/components/settings_sub_nav';
|
||||
|
@ -123,7 +123,7 @@ export const WorkplaceSearchConfigured: React.FC<InitialAppData> = (props) => {
|
|||
</Route>
|
||||
<Route path={ROLE_MAPPINGS_PATH}>
|
||||
<Layout navigation={<WorkplaceSearchNav />} restrictWidth readOnlyMode={readOnlyMode}>
|
||||
<RoleMappingsRouter />
|
||||
<RoleMappings />
|
||||
</Layout>
|
||||
</Route>
|
||||
<Route path={SECURITY_PATH}>
|
||||
|
|
|
@ -16,12 +16,10 @@ import {
|
|||
getGroupPath,
|
||||
getGroupSourcePrioritizationPath,
|
||||
getReindexJobRoute,
|
||||
getRoleMappingPath,
|
||||
getSourcesPath,
|
||||
GROUPS_PATH,
|
||||
SOURCES_PATH,
|
||||
PERSONAL_SOURCES_PATH,
|
||||
ROLE_MAPPINGS_PATH,
|
||||
SOURCE_DETAILS_PATH,
|
||||
} from './routes';
|
||||
|
||||
|
@ -52,12 +50,6 @@ describe('getGroupPath', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getRoleMappingPath', () => {
|
||||
it('should format path', () => {
|
||||
expect(getRoleMappingPath('123')).toEqual(`${ROLE_MAPPINGS_PATH}/123`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupSourcePrioritizationPath', () => {
|
||||
it('should format path', () => {
|
||||
expect(getGroupSourcePrioritizationPath('123')).toEqual(
|
||||
|
|
|
@ -50,8 +50,6 @@ export const ENT_SEARCH_LICENSE_MANAGEMENT = `${docLinks.enterpriseSearchBase}/l
|
|||
export const PERSONAL_PATH = '/p';
|
||||
|
||||
export const ROLE_MAPPINGS_PATH = '/role_mappings';
|
||||
export const ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/:roleId`;
|
||||
export const ROLE_MAPPING_NEW_PATH = `${ROLE_MAPPINGS_PATH}/new`;
|
||||
|
||||
export const USERS_PATH = '/users';
|
||||
export const SECURITY_PATH = '/security';
|
||||
|
@ -135,4 +133,3 @@ export const getReindexJobRoute = (
|
|||
isOrganization: boolean
|
||||
) =>
|
||||
getSourcesPath(generatePath(REINDEX_JOB_PATH, { sourceId, activeReindexJobId }), isOrganization);
|
||||
export const getRoleMappingPath = (roleId: string) => generatePath(ROLE_MAPPING_PATH, { roleId });
|
||||
|
|
|
@ -59,13 +59,6 @@ export const USER_ROLE_TYPE_DESCRIPTION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const GROUP_ASSIGNMENT_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentTitle',
|
||||
{
|
||||
defaultMessage: 'Group assignment',
|
||||
}
|
||||
);
|
||||
|
||||
export const GROUP_ASSIGNMENT_INVALID_ERROR = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError',
|
||||
{
|
||||
|
@ -73,10 +66,10 @@ export const GROUP_ASSIGNMENT_INVALID_ERROR = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const GROUP_ASSIGNMENT_ALL_GROUPS_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentAllGroupsLabel',
|
||||
export const GROUP_ASSIGNMENT_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentLabel',
|
||||
{
|
||||
defaultMessage: 'Include in all groups, including future groups',
|
||||
defaultMessage: 'Group assignment',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -94,3 +87,32 @@ export const ROLE_MAPPINGS_TABLE_HEADER = i18n.translate(
|
|||
defaultMessage: 'Group Access',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALL_GROUPS_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.allGroupsLabel',
|
||||
{
|
||||
defaultMessage: 'Assign to all groups',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALL_GROUPS_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.allGroupsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Assigning to all groups includes all current and future groups as created and administered at a later date.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SPECIFIC_GROUPS_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.specificGroupsLabel',
|
||||
{
|
||||
defaultMessage: 'Assign to specific groups',
|
||||
}
|
||||
);
|
||||
|
||||
export const SPECIFIC_GROUPS_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.specificGroupsDescription',
|
||||
{
|
||||
defaultMessage: 'Assign to a select set of groups statically.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { RoleMappingsRouter } from './role_mappings_router';
|
||||
export { RoleMappings } from './role_mappings';
|
||||
|
|
|
@ -10,16 +10,12 @@ import { setMockActions, setMockValues } from '../../../__mocks__';
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiCheckbox } from '@elastic/eui';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import {
|
||||
AttributeSelector,
|
||||
DeleteMappingCallout,
|
||||
RoleSelector,
|
||||
} from '../../../shared/role_mapping';
|
||||
import { AttributeSelector, RoleSelector } from '../../../shared/role_mapping';
|
||||
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
|
@ -80,42 +76,37 @@ describe('RoleMapping', () => {
|
|||
});
|
||||
|
||||
it('renders', () => {
|
||||
setMockValues({ ...mockValues, roleMapping: wsRoleMapping });
|
||||
const wrapper = shallow(<RoleMapping />);
|
||||
|
||||
expect(wrapper.find(AttributeSelector)).toHaveLength(1);
|
||||
expect(wrapper.find(RoleSelector)).toHaveLength(2);
|
||||
expect(wrapper.find(RoleSelector)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns Loading when loading', () => {
|
||||
setMockValues({ ...mockValues, dataLoading: true });
|
||||
it('sets initial selected state when includeInAllGroups is true', () => {
|
||||
setMockValues({ ...mockValues, includeInAllGroups: true });
|
||||
const wrapper = shallow(<RoleMapping />);
|
||||
|
||||
expect(wrapper.find(Loading)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all');
|
||||
});
|
||||
|
||||
it('hides DeleteMappingCallout for new mapping', () => {
|
||||
const wrapper = shallow(<RoleMapping isNew />);
|
||||
|
||||
expect(wrapper.find(DeleteMappingCallout)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles group checkbox click', () => {
|
||||
it('handles all/specific groups radio change', () => {
|
||||
const wrapper = shallow(<RoleMapping />);
|
||||
wrapper
|
||||
.find(EuiCheckbox)
|
||||
.first()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
const radio = wrapper.find(EuiRadioGroup);
|
||||
radio.simulate('change', { target: { checked: false } });
|
||||
|
||||
expect(handleGroupSelectionChange).toHaveBeenCalledWith(groups[0].id, true);
|
||||
expect(handleAllGroupsSelectionChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('handles all groups checkbox click', () => {
|
||||
it('handles group checkbox click', async () => {
|
||||
const wrapper = shallow(<RoleMapping />);
|
||||
wrapper
|
||||
.find(EuiCheckbox)
|
||||
.last()
|
||||
.simulate('change', { target: { checked: true } });
|
||||
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(handleAllGroupsSelectionChange).toHaveBeenCalledWith(true);
|
||||
expect(handleGroupSelectionChange).toHaveBeenCalledWith([groups[0].name]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,91 +5,78 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCheckbox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiComboBox, EuiFormRow, EuiHorizontalRule, EuiRadioGroup, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import {
|
||||
AttributeSelector,
|
||||
DeleteMappingCallout,
|
||||
RoleSelector,
|
||||
RoleOptionLabel,
|
||||
RoleMappingFlyout,
|
||||
} from '../../../shared/role_mapping';
|
||||
import {
|
||||
ROLE_LABEL,
|
||||
ROLE_MAPPINGS_TITLE,
|
||||
ADD_ROLE_MAPPING_TITLE,
|
||||
MANAGE_ROLE_MAPPING_TITLE,
|
||||
} from '../../../shared/role_mapping/constants';
|
||||
import { ViewContentHeader } from '../../components/shared/view_content_header';
|
||||
|
||||
import { Role } from '../../types';
|
||||
|
||||
import {
|
||||
ADMIN_ROLE_TYPE_DESCRIPTION,
|
||||
USER_ROLE_TYPE_DESCRIPTION,
|
||||
GROUP_ASSIGNMENT_TITLE,
|
||||
GROUP_ASSIGNMENT_INVALID_ERROR,
|
||||
GROUP_ASSIGNMENT_ALL_GROUPS_LABEL,
|
||||
GROUP_ASSIGNMENT_LABEL,
|
||||
ALL_GROUPS_LABEL,
|
||||
ALL_GROUPS_DESCRIPTION,
|
||||
SPECIFIC_GROUPS_LABEL,
|
||||
SPECIFIC_GROUPS_DESCRIPTION,
|
||||
} from './constants';
|
||||
|
||||
import { RoleMappingsLogic } from './role_mappings_logic';
|
||||
|
||||
interface RoleType {
|
||||
type: Role;
|
||||
id: Role;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const roleTypes = [
|
||||
const roleOptions = [
|
||||
{
|
||||
type: 'admin',
|
||||
id: 'admin',
|
||||
description: ADMIN_ROLE_TYPE_DESCRIPTION,
|
||||
},
|
||||
{
|
||||
type: 'user',
|
||||
id: 'user',
|
||||
description: USER_ROLE_TYPE_DESCRIPTION,
|
||||
},
|
||||
] as RoleType[];
|
||||
|
||||
interface RoleMappingProps {
|
||||
isNew?: boolean;
|
||||
}
|
||||
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<RoleMappingProps> = ({ isNew }) => {
|
||||
const { roleId } = useParams() as { roleId: string };
|
||||
export const RoleMapping: React.FC = () => {
|
||||
const {
|
||||
initializeRoleMappings,
|
||||
initializeRoleMapping,
|
||||
handleSaveMapping,
|
||||
handleGroupSelectionChange,
|
||||
handleAllGroupsSelectionChange,
|
||||
handleAttributeValueChange,
|
||||
handleAttributeSelectorChange,
|
||||
handleDeleteMapping,
|
||||
handleRoleChange,
|
||||
handleAuthProviderChange,
|
||||
resetState,
|
||||
closeRoleMappingFlyout,
|
||||
} = useActions(RoleMappingsLogic);
|
||||
|
||||
const {
|
||||
attributes,
|
||||
elasticsearchRoles,
|
||||
dataLoading,
|
||||
roleType,
|
||||
attributeValue,
|
||||
attributeName,
|
||||
|
@ -99,117 +86,64 @@ export const RoleMapping: React.FC<RoleMappingProps> = ({ isNew }) => {
|
|||
availableAuthProviders,
|
||||
multipleAuthProvidersConfig,
|
||||
selectedAuthProviders,
|
||||
selectedOptions,
|
||||
roleMapping,
|
||||
} = useValues(RoleMappingsLogic);
|
||||
|
||||
useEffect(() => {
|
||||
initializeRoleMappings();
|
||||
initializeRoleMapping(roleId);
|
||||
return resetState;
|
||||
}, [roleId]);
|
||||
|
||||
if (dataLoading) return <Loading />;
|
||||
const isNew = !roleMapping;
|
||||
|
||||
const hasGroupAssignment = selectedGroups.size > 0 || includeInAllGroups;
|
||||
|
||||
const TITLE = isNew ? ADD_ROLE_MAPPING_TITLE : MANAGE_ROLE_MAPPING_TITLE;
|
||||
const SAVE_ROLE_MAPPING_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.roleMapping.saveRoleMappingButtonMessage',
|
||||
{
|
||||
defaultMessage: '{operation} role mapping',
|
||||
values: { operation: isNew ? 'Save' : 'Update' },
|
||||
}
|
||||
);
|
||||
|
||||
const saveRoleMappingButton = (
|
||||
<EuiButton disabled={!hasGroupAssignment} onClick={handleSaveMapping} fill>
|
||||
{SAVE_ROLE_MAPPING_LABEL}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SetPageChrome trail={[ROLE_MAPPINGS_TITLE, TITLE]} />
|
||||
<ViewContentHeader title={SAVE_ROLE_MAPPING_LABEL} action={saveRoleMappingButton} />
|
||||
<EuiSpacer size="l" />
|
||||
<div>
|
||||
<FlashMessages />
|
||||
<AttributeSelector
|
||||
attributeName={attributeName}
|
||||
attributeValue={attributeValue}
|
||||
attributes={attributes}
|
||||
elasticsearchRoles={elasticsearchRoles}
|
||||
disabled={!isNew}
|
||||
handleAttributeSelectorChange={handleAttributeSelectorChange}
|
||||
handleAttributeValueChange={handleAttributeValueChange}
|
||||
availableAuthProviders={availableAuthProviders}
|
||||
selectedAuthProviders={selectedAuthProviders}
|
||||
multipleAuthProvidersConfig={multipleAuthProvidersConfig}
|
||||
handleAuthProviderChange={handleAuthProviderChange}
|
||||
<RoleMappingFlyout
|
||||
disabled={!hasGroupAssignment}
|
||||
isNew={isNew}
|
||||
closeRoleMappingFlyout={closeRoleMappingFlyout}
|
||||
handleSaveMapping={handleSaveMapping}
|
||||
>
|
||||
<AttributeSelector
|
||||
attributeName={attributeName}
|
||||
attributeValue={attributeValue}
|
||||
attributes={attributes}
|
||||
elasticsearchRoles={elasticsearchRoles}
|
||||
disabled={!isNew}
|
||||
handleAttributeSelectorChange={handleAttributeSelectorChange}
|
||||
handleAttributeValueChange={handleAttributeValueChange}
|
||||
availableAuthProviders={availableAuthProviders}
|
||||
selectedAuthProviders={selectedAuthProviders}
|
||||
multipleAuthProvidersConfig={multipleAuthProvidersConfig}
|
||||
handleAuthProviderChange={handleAuthProviderChange}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<RoleSelector
|
||||
roleOptions={roleOptions}
|
||||
roleType={roleType}
|
||||
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>,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup alignItems="stretch">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} color="subdued" paddingSize="l">
|
||||
<EuiTitle size="s">
|
||||
<h3>{ROLE_LABEL}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
{roleTypes.map(({ type, description }) => (
|
||||
<RoleSelector
|
||||
key={type}
|
||||
roleType={roleType}
|
||||
onChange={handleRoleChange}
|
||||
roleTypeOption={type}
|
||||
description={description}
|
||||
/>
|
||||
))}
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} color="subdued" paddingSize="l">
|
||||
<EuiTitle size="s">
|
||||
<h3>{GROUP_ASSIGNMENT_TITLE}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<div>
|
||||
<EuiFormRow
|
||||
isInvalid={!hasGroupAssignment}
|
||||
error={[GROUP_ASSIGNMENT_INVALID_ERROR]}
|
||||
>
|
||||
<>
|
||||
{availableGroups.map(({ id, name }) => (
|
||||
<EuiCheckbox
|
||||
key={id}
|
||||
name={name}
|
||||
id={id}
|
||||
checked={selectedGroups.has(id)}
|
||||
onChange={(e) => {
|
||||
handleGroupSelectionChange(id, e.target.checked);
|
||||
}}
|
||||
label={name}
|
||||
disabled={includeInAllGroups}
|
||||
/>
|
||||
))}
|
||||
<EuiSpacer />
|
||||
<EuiCheckbox
|
||||
key="allGroups"
|
||||
name="allGroups"
|
||||
id="allGroups"
|
||||
checked={includeInAllGroups}
|
||||
onChange={(e) => {
|
||||
handleAllGroupsSelectionChange(e.target.checked);
|
||||
}}
|
||||
label={GROUP_ASSIGNMENT_ALL_GROUPS_LABEL}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{!isNew && <DeleteMappingCallout handleDeleteMapping={handleDeleteMapping} />}
|
||||
</div>
|
||||
</>
|
||||
</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>
|
||||
</RoleMappingFlyout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,16 +12,19 @@ import React from 'react';
|
|||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
|
||||
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import { RoleMappingsTable } from '../../../shared/role_mapping';
|
||||
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
import { RoleMappings } from './role_mappings';
|
||||
|
||||
describe('RoleMappings', () => {
|
||||
const initializeRoleMappings = jest.fn();
|
||||
const initializeRoleMapping = jest.fn();
|
||||
const handleDeleteMapping = jest.fn();
|
||||
const mockValues = {
|
||||
roleMappings: [wsRoleMapping],
|
||||
dataLoading: false,
|
||||
|
@ -31,6 +34,8 @@ describe('RoleMappings', () => {
|
|||
beforeEach(() => {
|
||||
setMockActions({
|
||||
initializeRoleMappings,
|
||||
initializeRoleMapping,
|
||||
handleDeleteMapping,
|
||||
});
|
||||
setMockValues(mockValues);
|
||||
});
|
||||
|
@ -54,4 +59,19 @@ describe('RoleMappings', () => {
|
|||
|
||||
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders RoleMapping flyout', () => {
|
||||
setMockValues({ ...mockValues, roleMappingFlyoutOpen: true });
|
||||
const wrapper = shallow(<RoleMappings />);
|
||||
|
||||
expect(wrapper.find(RoleMapping)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles button click', () => {
|
||||
setMockValues({ ...mockValues, roleMappings: [] });
|
||||
const wrapper = shallow(<RoleMappings />);
|
||||
wrapper.find(EuiEmptyPrompt).dive().find(EuiButton).simulate('click');
|
||||
|
||||
expect(initializeRoleMapping).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,28 +9,36 @@ import React, { useEffect } from 'react';
|
|||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
|
||||
import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
|
||||
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
|
||||
import { Loading } from '../../../shared/loading';
|
||||
import { AddRoleMappingButton, RoleMappingsTable } from '../../../shared/role_mapping';
|
||||
import { RoleMappingsTable } from '../../../shared/role_mapping';
|
||||
import {
|
||||
EMPTY_ROLE_MAPPINGS_TITLE,
|
||||
ROLE_MAPPING_ADD_BUTTON,
|
||||
ROLE_MAPPINGS_TITLE,
|
||||
ROLE_MAPPINGS_DESCRIPTION,
|
||||
} from '../../../shared/role_mapping/constants';
|
||||
import { ViewContentHeader } from '../../components/shared/view_content_header';
|
||||
import { getRoleMappingPath, ROLE_MAPPING_NEW_PATH } from '../../routes';
|
||||
|
||||
import { EMPTY_ROLE_MAPPINGS_BODY, ROLE_MAPPINGS_TABLE_HEADER } from './constants';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
import { RoleMappingsLogic } from './role_mappings_logic';
|
||||
|
||||
export const RoleMappings: React.FC = () => {
|
||||
const { initializeRoleMappings } = useActions(RoleMappingsLogic);
|
||||
const { initializeRoleMappings, initializeRoleMapping, handleDeleteMapping } = useActions(
|
||||
RoleMappingsLogic
|
||||
);
|
||||
|
||||
const { roleMappings, dataLoading, multipleAuthProvidersConfig } = useValues(RoleMappingsLogic);
|
||||
const {
|
||||
roleMappings,
|
||||
dataLoading,
|
||||
multipleAuthProvidersConfig,
|
||||
roleMappingFlyoutOpen,
|
||||
} = useValues(RoleMappingsLogic);
|
||||
|
||||
useEffect(() => {
|
||||
initializeRoleMappings();
|
||||
|
@ -38,7 +46,11 @@ export const RoleMappings: React.FC = () => {
|
|||
|
||||
if (dataLoading) return <Loading />;
|
||||
|
||||
const addMappingButton = <AddRoleMappingButton path={ROLE_MAPPING_NEW_PATH} />;
|
||||
const addMappingButton = (
|
||||
<EuiButton fill onClick={() => initializeRoleMapping()}>
|
||||
{ROLE_MAPPING_ADD_BUTTON}
|
||||
</EuiButton>
|
||||
);
|
||||
const emptyPrompt = (
|
||||
<EuiPanel paddingSize="l" color="subdued" hasBorder={false}>
|
||||
<EuiEmptyPrompt
|
||||
|
@ -55,8 +67,9 @@ export const RoleMappings: React.FC = () => {
|
|||
accessItemKey="groups"
|
||||
accessHeader={ROLE_MAPPINGS_TABLE_HEADER}
|
||||
addMappingButton={addMappingButton}
|
||||
getRoleMappingPath={getRoleMappingPath}
|
||||
shouldShowAuthProvider={multipleAuthProvidersConfig}
|
||||
initializeRoleMapping={initializeRoleMapping}
|
||||
handleDeleteMapping={handleDeleteMapping}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -64,6 +77,8 @@ export const RoleMappings: React.FC = () => {
|
|||
<>
|
||||
<SetPageChrome trail={[ROLE_MAPPINGS_TITLE]} />
|
||||
<ViewContentHeader title={ROLE_MAPPINGS_TITLE} description={ROLE_MAPPINGS_DESCRIPTION} />
|
||||
|
||||
{roleMappingFlyoutOpen && <RoleMapping />}
|
||||
<div>
|
||||
<FlashMessages />
|
||||
{roleMappings.length === 0 ? emptyPrompt : roleMappingsTable}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__';
|
||||
import { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__';
|
||||
import { LogicMounter } from '../../../__mocks__/kea.mock';
|
||||
|
||||
import { groups } from '../../__mocks__/groups.mock';
|
||||
|
@ -13,20 +13,20 @@ import { groups } from '../../__mocks__/groups.mock';
|
|||
import { nextTick } from '@kbn/test/jest';
|
||||
|
||||
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
|
||||
import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
|
||||
import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
|
||||
|
||||
import { RoleMappingsLogic } from './role_mappings_logic';
|
||||
|
||||
describe('RoleMappingsLogic', () => {
|
||||
const { http } = mockHttpValues;
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers;
|
||||
const { clearFlashMessages, flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
|
||||
const { mount } = new LogicMounter(RoleMappingsLogic);
|
||||
const defaultValues = {
|
||||
attributes: [],
|
||||
availableAuthProviders: [],
|
||||
elasticsearchRoles: [],
|
||||
roleMapping: null,
|
||||
roleMappingFlyoutOpen: false,
|
||||
roleMappings: [],
|
||||
roleType: 'admin',
|
||||
attributeValue: '',
|
||||
|
@ -37,6 +37,7 @@ describe('RoleMappingsLogic', () => {
|
|||
selectedGroups: new Set(),
|
||||
includeInAllGroups: false,
|
||||
selectedAuthProviders: [ANY_AUTH_PROVIDER],
|
||||
selectedOptions: [],
|
||||
};
|
||||
const roleGroup = {
|
||||
id: '123',
|
||||
|
@ -92,6 +93,7 @@ describe('RoleMappingsLogic', () => {
|
|||
expect(RoleMappingsLogic.values.selectedGroups).toEqual(
|
||||
new Set([wsRoleMapping.groups[0].id])
|
||||
);
|
||||
expect(RoleMappingsLogic.values.selectedOptions).toEqual([]);
|
||||
});
|
||||
|
||||
it('sets default group with new role mapping', () => {
|
||||
|
@ -121,10 +123,13 @@ describe('RoleMappingsLogic', () => {
|
|||
},
|
||||
});
|
||||
|
||||
RoleMappingsLogic.actions.handleGroupSelectionChange(otherGroup.id, true);
|
||||
RoleMappingsLogic.actions.handleGroupSelectionChange([group.id, otherGroup.id]);
|
||||
expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([group.id, otherGroup.id]));
|
||||
expect(RoleMappingsLogic.values.selectedOptions).toEqual([
|
||||
{ label: roleGroup.name, value: roleGroup.id },
|
||||
]);
|
||||
|
||||
RoleMappingsLogic.actions.handleGroupSelectionChange(otherGroup.id, false);
|
||||
RoleMappingsLogic.actions.handleGroupSelectionChange([group.id]);
|
||||
expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([group.id]));
|
||||
});
|
||||
|
||||
|
@ -223,6 +228,25 @@ describe('RoleMappingsLogic', () => {
|
|||
expect(RoleMappingsLogic.values.attributeName).toEqual('username');
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('openRoleMappingFlyout', () => {
|
||||
mount(mappingServerProps);
|
||||
RoleMappingsLogic.actions.openRoleMappingFlyout();
|
||||
|
||||
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('closeRoleMappingFlyout', () => {
|
||||
mount({
|
||||
...mappingServerProps,
|
||||
roleMappingFlyoutOpen: true,
|
||||
});
|
||||
RoleMappingsLogic.actions.closeRoleMappingFlyout();
|
||||
|
||||
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(false);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
|
@ -275,17 +299,21 @@ describe('RoleMappingsLogic', () => {
|
|||
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
|
||||
});
|
||||
|
||||
it('redirects when there is a 404 status', async () => {
|
||||
it('shows error when there is a 404 status', async () => {
|
||||
http.get.mockReturnValue(Promise.reject({ status: 404 }));
|
||||
RoleMappingsLogic.actions.initializeRoleMapping();
|
||||
await nextTick();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalled();
|
||||
expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSaveMapping', () => {
|
||||
it('calls API and navigates when new mapping', async () => {
|
||||
it('calls API and refreshes list when new mapping', async () => {
|
||||
const initializeRoleMappingsSpy = jest.spyOn(
|
||||
RoleMappingsLogic.actions,
|
||||
'initializeRoleMappings'
|
||||
);
|
||||
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
|
||||
|
||||
http.post.mockReturnValue(Promise.resolve(mappingServerProps));
|
||||
|
@ -304,10 +332,14 @@ describe('RoleMappingsLogic', () => {
|
|||
});
|
||||
await nextTick();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalled();
|
||||
expect(initializeRoleMappingsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls API and navigates when existing mapping', async () => {
|
||||
it('calls API and refreshes list when existing mapping', async () => {
|
||||
const initializeRoleMappingsSpy = jest.spyOn(
|
||||
RoleMappingsLogic.actions,
|
||||
'initializeRoleMappings'
|
||||
);
|
||||
RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
|
||||
|
||||
http.put.mockReturnValue(Promise.resolve(mappingServerProps));
|
||||
|
@ -329,7 +361,7 @@ describe('RoleMappingsLogic', () => {
|
|||
);
|
||||
await nextTick();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalled();
|
||||
expect(initializeRoleMappingsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles error', async () => {
|
||||
|
@ -343,6 +375,7 @@ describe('RoleMappingsLogic', () => {
|
|||
|
||||
describe('handleDeleteMapping', () => {
|
||||
let confirmSpy: any;
|
||||
const roleMappingId = 'r1';
|
||||
|
||||
beforeEach(() => {
|
||||
confirmSpy = jest.spyOn(window, 'confirm');
|
||||
|
@ -353,29 +386,27 @@ describe('RoleMappingsLogic', () => {
|
|||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('returns when no mapping', () => {
|
||||
RoleMappingsLogic.actions.handleDeleteMapping();
|
||||
|
||||
expect(http.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls API and navigates', async () => {
|
||||
it('calls API and refreshes list', async () => {
|
||||
const initializeRoleMappingsSpy = jest.spyOn(
|
||||
RoleMappingsLogic.actions,
|
||||
'initializeRoleMappings'
|
||||
);
|
||||
RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
|
||||
http.delete.mockReturnValue(Promise.resolve({}));
|
||||
RoleMappingsLogic.actions.handleDeleteMapping();
|
||||
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
|
||||
|
||||
expect(http.delete).toHaveBeenCalledWith(
|
||||
`/api/workplace_search/org/role_mappings/${wsRoleMapping.id}`
|
||||
`/api/workplace_search/org/role_mappings/${roleMappingId}`
|
||||
);
|
||||
await nextTick();
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalled();
|
||||
expect(initializeRoleMappingsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles error', async () => {
|
||||
RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
|
||||
http.delete.mockReturnValue(Promise.reject('this is an error'));
|
||||
RoleMappingsLogic.actions.handleDeleteMapping();
|
||||
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
|
||||
await nextTick();
|
||||
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
|
||||
|
@ -384,7 +415,7 @@ describe('RoleMappingsLogic', () => {
|
|||
it('will do nothing if not confirmed', async () => {
|
||||
RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
|
||||
window.confirm = () => false;
|
||||
RoleMappingsLogic.actions.handleDeleteMapping();
|
||||
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
|
||||
|
||||
expect(http.delete).not.toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
|
|
@ -7,16 +7,17 @@
|
|||
|
||||
import { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
clearFlashMessages,
|
||||
flashAPIErrors,
|
||||
setSuccessMessage,
|
||||
setErrorMessage,
|
||||
} from '../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
|
||||
import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
|
||||
import { AttributeName } from '../../../shared/types';
|
||||
import { ROLE_MAPPINGS_PATH } from '../../routes';
|
||||
import { RoleGroup, WSRoleMapping, Role } from '../../types';
|
||||
|
||||
import {
|
||||
|
@ -54,18 +55,17 @@ interface RoleMappingsActions {
|
|||
firstElasticsearchRole: string
|
||||
): { value: AttributeName; firstElasticsearchRole: string };
|
||||
handleAttributeValueChange(value: string): { value: string };
|
||||
handleDeleteMapping(): void;
|
||||
handleGroupSelectionChange(
|
||||
groupId: string,
|
||||
selected: boolean
|
||||
): { groupId: string; selected: boolean };
|
||||
handleDeleteMapping(roleMappingId: string): { roleMappingId: string };
|
||||
handleGroupSelectionChange(groupIds: string[]): { groupIds: string[] };
|
||||
handleRoleChange(roleType: Role): { roleType: Role };
|
||||
handleSaveMapping(): void;
|
||||
initializeRoleMapping(roleId?: string): { roleId?: string };
|
||||
initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
|
||||
initializeRoleMappings(): void;
|
||||
resetState(): void;
|
||||
setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails;
|
||||
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
|
||||
openRoleMappingFlyout(): void;
|
||||
closeRoleMappingFlyout(): void;
|
||||
}
|
||||
|
||||
interface RoleMappingsValues {
|
||||
|
@ -83,6 +83,8 @@ interface RoleMappingsValues {
|
|||
roleType: Role;
|
||||
selectedAuthProviders: string[];
|
||||
selectedGroups: Set<string>;
|
||||
roleMappingFlyoutOpen: boolean;
|
||||
selectedOptions: EuiComboBoxOptionOption[];
|
||||
}
|
||||
|
||||
export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappingsActions>>({
|
||||
|
@ -92,7 +94,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
setRoleMappingData: (data: RoleMappingServerDetails) => data,
|
||||
handleAuthProviderChange: (value: string[]) => ({ value }),
|
||||
handleRoleChange: (roleType: Role) => ({ roleType }),
|
||||
handleGroupSelectionChange: (groupId: string, selected: boolean) => ({ groupId, selected }),
|
||||
handleGroupSelectionChange: (groupIds: string[]) => ({ groupIds }),
|
||||
handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({
|
||||
value,
|
||||
firstElasticsearchRole,
|
||||
|
@ -101,9 +103,11 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }),
|
||||
resetState: true,
|
||||
initializeRoleMappings: true,
|
||||
initializeRoleMapping: (roleId?: string) => ({ roleId }),
|
||||
handleDeleteMapping: true,
|
||||
initializeRoleMapping: (roleMappingId?: string) => ({ roleMappingId }),
|
||||
handleDeleteMapping: (roleMappingId: string) => ({ roleMappingId }),
|
||||
handleSaveMapping: true,
|
||||
openRoleMappingFlyout: true,
|
||||
closeRoleMappingFlyout: false,
|
||||
},
|
||||
reducers: {
|
||||
dataLoading: [
|
||||
|
@ -152,6 +156,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
{
|
||||
setRoleMappingData: (_, { roleMapping }) => roleMapping || null,
|
||||
resetState: () => null,
|
||||
closeRoleMappingFlyout: () => null,
|
||||
},
|
||||
],
|
||||
roleType: [
|
||||
|
@ -178,6 +183,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
value === 'role' ? firstElasticsearchRole : '',
|
||||
handleAttributeValueChange: (_, { value }) => value,
|
||||
resetState: () => '',
|
||||
closeRoleMappingFlyout: () => '',
|
||||
},
|
||||
],
|
||||
attributeName: [
|
||||
|
@ -187,6 +193,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
roleMapping ? getFirstAttributeName(roleMapping) : 'username',
|
||||
handleAttributeSelectorChange: (_, { value }) => value,
|
||||
resetState: () => 'username',
|
||||
closeRoleMappingFlyout: () => 'username',
|
||||
},
|
||||
],
|
||||
selectedGroups: [
|
||||
|
@ -200,13 +207,10 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
.filter((group) => group.name === DEFAULT_GROUP_NAME)
|
||||
.map((group) => group.id)
|
||||
),
|
||||
handleGroupSelectionChange: (groups, { groupId, selected }) => {
|
||||
const newSelectedGroupNames = new Set(groups as Set<string>);
|
||||
if (selected) {
|
||||
newSelectedGroupNames.add(groupId);
|
||||
} else {
|
||||
newSelectedGroupNames.delete(groupId);
|
||||
}
|
||||
handleGroupSelectionChange: (_, { groupIds }) => {
|
||||
const newSelectedGroupNames = new Set() as Set<string>;
|
||||
groupIds.forEach((groupId) => newSelectedGroupNames.add(groupId));
|
||||
|
||||
return newSelectedGroupNames;
|
||||
},
|
||||
},
|
||||
|
@ -234,7 +238,27 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER],
|
||||
},
|
||||
],
|
||||
roleMappingFlyoutOpen: [
|
||||
false,
|
||||
{
|
||||
openRoleMappingFlyout: () => true,
|
||||
closeRoleMappingFlyout: () => false,
|
||||
initializeRoleMappings: () => false,
|
||||
initializeRoleMapping: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
selectors: ({ selectors }) => ({
|
||||
selectedOptions: [
|
||||
() => [selectors.selectedGroups, selectors.availableGroups],
|
||||
(selectedGroups, availableGroups) => {
|
||||
const selectedIds = Array.from(selectedGroups.values());
|
||||
return availableGroups
|
||||
.filter(({ id }: { id: string }) => selectedIds.includes(id))
|
||||
.map(({ id, name }: { id: string; name: string }) => ({ label: name, value: id }));
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions, values }) => ({
|
||||
initializeRoleMappings: async () => {
|
||||
const { http } = HttpLogic.values;
|
||||
|
@ -247,11 +271,10 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
initializeRoleMapping: async ({ roleId }) => {
|
||||
initializeRoleMapping: async ({ roleMappingId }) => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
const route = roleId
|
||||
? `/api/workplace_search/org/role_mappings/${roleId}`
|
||||
const route = roleMappingId
|
||||
? `/api/workplace_search/org/role_mappings/${roleMappingId}`
|
||||
: '/api/workplace_search/org/role_mappings/new';
|
||||
|
||||
try {
|
||||
|
@ -259,23 +282,20 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
actions.setRoleMappingData(response);
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
navigateToUrl(ROLE_MAPPINGS_PATH);
|
||||
setErrorMessage(ROLE_MAPPING_NOT_FOUND);
|
||||
} else {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
handleDeleteMapping: async () => {
|
||||
const { roleMapping } = values;
|
||||
if (!roleMapping) return;
|
||||
|
||||
handleDeleteMapping: async ({ roleMappingId }) => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
const route = `/api/workplace_search/org/role_mappings/${roleMapping.id}`;
|
||||
const route = `/api/workplace_search/org/role_mappings/${roleMappingId}`;
|
||||
|
||||
if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) {
|
||||
try {
|
||||
await http.delete(route);
|
||||
navigateToUrl(ROLE_MAPPINGS_PATH);
|
||||
actions.initializeRoleMappings();
|
||||
setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
|
@ -284,7 +304,6 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
},
|
||||
handleSaveMapping: async () => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
const {
|
||||
attributeName,
|
||||
attributeValue,
|
||||
|
@ -315,7 +334,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
|
||||
try {
|
||||
await request;
|
||||
navigateToUrl(ROLE_MAPPINGS_PATH);
|
||||
actions.initializeRoleMappings();
|
||||
setSuccessMessage(SUCCESS_MESSAGE);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
|
@ -324,5 +343,11 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
|
|||
resetState: () => {
|
||||
clearFlashMessages();
|
||||
},
|
||||
closeRoleMappingFlyout: () => {
|
||||
clearFlashMessages();
|
||||
},
|
||||
openRoleMappingFlyout: () => {
|
||||
clearFlashMessages();
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,26 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
import { RoleMappings } from './role_mappings';
|
||||
import { RoleMappingsRouter } from './role_mappings_router';
|
||||
|
||||
describe('RoleMappingsRouter', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<RoleMappingsRouter />);
|
||||
|
||||
expect(wrapper.find(Switch)).toHaveLength(1);
|
||||
expect(wrapper.find(Route)).toHaveLength(3);
|
||||
expect(wrapper.find(RoleMapping)).toHaveLength(2);
|
||||
expect(wrapper.find(RoleMappings)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -1,34 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
|
||||
import { NAV } from '../../constants';
|
||||
import { ROLE_MAPPING_NEW_PATH, ROLE_MAPPING_PATH, ROLE_MAPPINGS_PATH } from '../../routes';
|
||||
|
||||
import { RoleMapping } from './role_mapping';
|
||||
import { RoleMappings } from './role_mappings';
|
||||
|
||||
export const RoleMappingsRouter: React.FC = () => (
|
||||
<>
|
||||
<SetPageChrome trail={[NAV.ROLE_MAPPINGS]} />
|
||||
<Switch>
|
||||
<Route exact path={ROLE_MAPPING_NEW_PATH}>
|
||||
<RoleMapping isNew />
|
||||
</Route>
|
||||
<Route exact path={ROLE_MAPPINGS_PATH}>
|
||||
<RoleMappings />
|
||||
</Route>
|
||||
<Route path={ROLE_MAPPING_PATH}>
|
||||
<RoleMapping />
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
|
@ -7532,7 +7532,6 @@
|
|||
"xpack.enterpriseSearch.actions.updateButtonLabel": "更新",
|
||||
"xpack.enterpriseSearch.appSearch.actions.restoreDefaultsButonLabel": "デフォルトを復元",
|
||||
"xpack.enterpriseSearch.appSearch.adminRoleTypeDescription": "アカウント設定の管理を除き、管理者はすべての操作を実行できます。",
|
||||
"xpack.enterpriseSearch.appSearch.advancedRoleSelectorsTitle": "完全または限定エンジンアクセス",
|
||||
"xpack.enterpriseSearch.appSearch.analystRoleTypeDescription": "アナリストは、ドキュメント、クエリテスト、分析のみを表示できます。",
|
||||
"xpack.enterpriseSearch.appSearch.credentials.apiEndpoint": "エンドポイント",
|
||||
"xpack.enterpriseSearch.appSearch.credentials.apiKeys": "API キー",
|
||||
|
@ -7831,7 +7830,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.engine.searchUI.title": "Search UI",
|
||||
"xpack.enterpriseSearch.appSearch.engine.synonyms.title": "同義語",
|
||||
"xpack.enterpriseSearch.appSearch.engine.universalLanguage": "ユニバーサル",
|
||||
"xpack.enterpriseSearch.appSearch.engineAccessTitle": "エンジンアクセス",
|
||||
"xpack.enterpriseSearch.appSearch.engineCreation.form.engineLanguage.label": "エンジン言語",
|
||||
"xpack.enterpriseSearch.appSearch.engineCreation.form.engineName.allowedCharactersHelpText": "エンジン名には、小文字、数字、ハイフンのみを使用できます。",
|
||||
"xpack.enterpriseSearch.appSearch.engineCreation.form.engineName.label": "エンジン名",
|
||||
|
@ -7873,10 +7871,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.enginesOverview.table.column.language": "言語",
|
||||
"xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name": "名前",
|
||||
"xpack.enterpriseSearch.appSearch.enginesOverview.title": "エンジン概要",
|
||||
"xpack.enterpriseSearch.appSearch.fullEngineAccessDescription": "すべての現在のエンジンと将来のエンジンにアクセスします。",
|
||||
"xpack.enterpriseSearch.appSearch.fullEngineAccessTitle": "完全エンジンアクセス",
|
||||
"xpack.enterpriseSearch.appSearch.limitedEngineAccessDescription": "ユーザーアクセスを特定のエンジンに制限します。",
|
||||
"xpack.enterpriseSearch.appSearch.limitedEngineAccessTitle": "限定エンジンアクセス",
|
||||
"xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsDetail": "分析とログを管理するには、{visitSettingsLink}してください。",
|
||||
"xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsLinkText": "設定を表示",
|
||||
"xpack.enterpriseSearch.appSearch.logRetention.callout.disabledSinceTitle": "{logsTitle}は、{disabledDate}以降に無効にされました。",
|
||||
|
@ -7917,8 +7911,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "追加フィールドを非表示",
|
||||
"xpack.enterpriseSearch.appSearch.result.title": "ドキュメント{id}",
|
||||
"xpack.enterpriseSearch.appSearch.roleMapping.emptyRoleMappingsBody": "認証が成功したすべてのユーザーには所有者ロールが割り当てられ、すべてのエンジンにアクセスできます。デフォルト設定を無効にするには、新しいロールを追加します。",
|
||||
"xpack.enterpriseSearch.appSearch.roleMapping.saveRoleMappingButtonLabel": "ロールマッピングの保存",
|
||||
"xpack.enterpriseSearch.appSearch.roleMapping.updateRoleMappingButtonLabel": "ロールマッピングを更新",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingCreatedMessage": "ロールマッピングが正常に作成されました。",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage": "ロールマッピングが正常に削除されました",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingsEngineAccessHeading": "エンジンアクセス",
|
||||
|
@ -7927,7 +7919,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmButton": "ロールマッピングをリセット",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmTitle": "ロールマッピングをリセットしますか?",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingUpdatedMessage": "ロールマッピングが正常に更新されました。",
|
||||
"xpack.enterpriseSearch.appSearch.roleTitle": "ロール",
|
||||
"xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.buttonLabel": "サンプルエンジンを試す",
|
||||
"xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.description": "サンプルデータでエンジンをテストします。",
|
||||
"xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.title": "確認している場合",
|
||||
|
@ -8019,6 +8010,8 @@
|
|||
"xpack.enterpriseSearch.roleMapping.roleLabel": "ロール",
|
||||
"xpack.enterpriseSearch.roleMapping.roleMappingsDescription": "elasticsearch-nativeおよびelasticsearch-saml認証のロールマッピングを定義します。",
|
||||
"xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "ユーザーとロール",
|
||||
"xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel": "ロールマッピングの保存",
|
||||
"xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel": "ロールマッピングを更新",
|
||||
"xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.correct": "フィールド名には、小文字、数字、アンダースコアのみを使用できます。",
|
||||
"xpack.enterpriseSearch.schema.errorsTable.control.review": "見直し",
|
||||
"xpack.enterpriseSearch.schema.errorsTable.heading.error": "エラー",
|
||||
|
@ -8324,11 +8317,8 @@
|
|||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName": "デフォルト",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage": "このマッピングを完全に削除しますか?このアクションは元に戻せません。一部のユーザーがアクセスを失う可能性があります。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsBody": "新しいチームメンバーにはデフォルトで管理者ロールが割り当てられます。管理者はすべてにアクセスできます。デフォルト設定を無効にするには、新しいロールを作成します。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentAllGroupsLabel": "将来のグループを含むすべてのグループにあります。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError": "1つ以上の割り当てられたグループが必要です。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentTitle": "グループ割り当て",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader": "グループアクセス",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.saveRoleMappingButtonMessage": "{operation}ロールマッピング",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription": "ユーザーの機能アクセスは検索インターフェースと個人設定管理に制限されます。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMappingCreatedMessage": "ロールマッピングが正常に作成されました。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMappingDeletedMessage": "ロールマッピングが正常に削除されました",
|
||||
|
|
|
@ -7591,7 +7591,6 @@
|
|||
"xpack.enterpriseSearch.actions.updateButtonLabel": "更新",
|
||||
"xpack.enterpriseSearch.appSearch.actions.restoreDefaultsButonLabel": "还原默认值",
|
||||
"xpack.enterpriseSearch.appSearch.adminRoleTypeDescription": "管理员可以执行任何操作,但不包括管理帐户设置。",
|
||||
"xpack.enterpriseSearch.appSearch.advancedRoleSelectorsTitle": "完全或有限的引擎访问",
|
||||
"xpack.enterpriseSearch.appSearch.analystRoleTypeDescription": "分析人员仅可以查看文档、查询测试器和分析。",
|
||||
"xpack.enterpriseSearch.appSearch.credentials.apiEndpoint": "终端",
|
||||
"xpack.enterpriseSearch.appSearch.credentials.apiKeys": "API 密钥",
|
||||
|
@ -7896,7 +7895,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.engine.searchUI.title": "搜索 UI",
|
||||
"xpack.enterpriseSearch.appSearch.engine.synonyms.title": "同义词",
|
||||
"xpack.enterpriseSearch.appSearch.engine.universalLanguage": "通用",
|
||||
"xpack.enterpriseSearch.appSearch.engineAccessTitle": "引擎访问",
|
||||
"xpack.enterpriseSearch.appSearch.engineCreation.form.engineLanguage.label": "引擎语言",
|
||||
"xpack.enterpriseSearch.appSearch.engineCreation.form.engineName.allowedCharactersHelpText": "引擎名称只能包含小写字母、数字和连字符",
|
||||
"xpack.enterpriseSearch.appSearch.engineCreation.form.engineName.label": "引擎名称",
|
||||
|
@ -7939,10 +7937,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.enginesOverview.table.column.language": "语言",
|
||||
"xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name": "名称",
|
||||
"xpack.enterpriseSearch.appSearch.enginesOverview.title": "引擎概览",
|
||||
"xpack.enterpriseSearch.appSearch.fullEngineAccessDescription": "对所有当前和未来引擎的访问权限。",
|
||||
"xpack.enterpriseSearch.appSearch.fullEngineAccessTitle": "完全的引擎访问权限",
|
||||
"xpack.enterpriseSearch.appSearch.limitedEngineAccessDescription": "将用户访问限定于特定引擎:",
|
||||
"xpack.enterpriseSearch.appSearch.limitedEngineAccessTitle": "有限的引擎访问权限",
|
||||
"xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsDetail": "要管理分析和日志记录,请{visitSettingsLink}。",
|
||||
"xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsLinkText": "访问您的设置",
|
||||
"xpack.enterpriseSearch.appSearch.logRetention.callout.disabledSinceTitle": "自 {disabledDate}后,{logsTitle} 已禁用。",
|
||||
|
@ -7985,8 +7979,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.result.showAdditionalFields": "显示其他 {numberOfAdditionalFields, number} 个{numberOfAdditionalFields, plural, other {字段}}",
|
||||
"xpack.enterpriseSearch.appSearch.result.title": "文档 {id}",
|
||||
"xpack.enterpriseSearch.appSearch.roleMapping.emptyRoleMappingsBody": "成功验证的所有用户将被分配所有者角色,可访问所有引擎。添加新角色以覆盖默认值。",
|
||||
"xpack.enterpriseSearch.appSearch.roleMapping.saveRoleMappingButtonLabel": "保存角色映射",
|
||||
"xpack.enterpriseSearch.appSearch.roleMapping.updateRoleMappingButtonLabel": "更新角色映射",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingCreatedMessage": "角色映射已成功创建。",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage": "已成功删除角色映射",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingsEngineAccessHeading": "引擎访问",
|
||||
|
@ -7995,7 +7987,6 @@
|
|||
"xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmButton": "重置角色映射",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmTitle": "确定要重置角色映射?",
|
||||
"xpack.enterpriseSearch.appSearch.roleMappingUpdatedMessage": "角色映射已成功更新。",
|
||||
"xpack.enterpriseSearch.appSearch.roleTitle": "角色",
|
||||
"xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.buttonLabel": "试用示例引擎",
|
||||
"xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.description": "使用示例数据测试引擎。",
|
||||
"xpack.enterpriseSearch.appSearch.sampleEngineCreationCta.title": "刚做过测试?",
|
||||
|
@ -8087,6 +8078,8 @@
|
|||
"xpack.enterpriseSearch.roleMapping.roleLabel": "角色",
|
||||
"xpack.enterpriseSearch.roleMapping.roleMappingsDescription": "为 elasticsearch-native 和 elasticsearch-saml 身份验证定义角色映射。",
|
||||
"xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "用户和角色",
|
||||
"xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel": "保存角色映射",
|
||||
"xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel": "更新角色映射",
|
||||
"xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.correct": "字段名称只能包含小写字母、数字和下划线",
|
||||
"xpack.enterpriseSearch.schema.errorsTable.control.review": "复查",
|
||||
"xpack.enterpriseSearch.schema.errorsTable.heading.error": "错误",
|
||||
|
@ -8392,11 +8385,8 @@
|
|||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName": "默认",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage": "确定要永久删除此映射?此操作不可逆转,且某些用户可能会失去访问权限。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsBody": "默认情况下,会为新团队成员分配管理员角色。管理员可以访问任何内容。超级新角色以覆盖默认值。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentAllGroupsLabel": "加入所有组,包括未来组",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError": "至少需要一个分配的组。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentTitle": "组分配",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader": "组访问权限",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.saveRoleMappingButtonMessage": "{operation}角色映射",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription": "用户的功能访问权限仅限于搜索界面和个人设置管理。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMappingCreatedMessage": "角色映射已成功创建。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.roleMappingDeletedMessage": "已成功删除角色映射",
|
||||
|
|
Loading…
Reference in a new issue