[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:
Scotty Bollinger 2021-06-04 14:22:31 -05:00 committed by GitHub
parent 137778d124
commit fc511f9ec1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1048 additions and 1025 deletions

View file

@ -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[];

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { RoleMappingsRouter } from './role_mappings_router';
export { RoleMappings } from './role_mappings';

View file

@ -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]);
});
});

View file

@ -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>
);
};

View file

@ -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();
});
});

View file

@ -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 />

View file

@ -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();
});

View file

@ -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();
},
}),
});

View file

@ -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);
});
});

View file

@ -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>
);

View file

@ -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}`);
});
});

View file

@ -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 });

View file

@ -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);
});
});

View file

@ -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 && (

View file

@ -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';

View file

@ -52,6 +52,6 @@ export interface ASRoleMapping extends RoleMapping {
}
export interface AdvanceRoleType {
type: RoleTypes;
id: RoleTypes;
description: string;
}

View file

@ -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);
});
});

View file

@ -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>
);

View file

@ -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"]');

View file

@ -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>
);
};

View file

@ -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' }
);

View file

@ -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();
});
});

View file

@ -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>
);

View file

@ -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';

View file

@ -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
);
});
});

View file

@ -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>
);

View file

@ -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);

View file

@ -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>

View file

@ -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);
});
});

View file

@ -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" />
</>
);

View file

@ -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);
});
});

View file

@ -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>
);
};

View file

@ -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}>

View file

@ -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(

View file

@ -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 });

View file

@ -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.',
}
);

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { RoleMappingsRouter } from './role_mappings_router';
export { RoleMappings } from './role_mappings';

View file

@ -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]);
});
});

View file

@ -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>
);
};

View file

@ -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();
});
});

View file

@ -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}

View file

@ -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();

View file

@ -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();
},
}),
});

View file

@ -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);
});
});

View file

@ -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>
</>
);

View file

@ -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": "ロールマッピングが正常に削除されました",

View file

@ -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": "已成功删除角色映射",