diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts index 2f9ff707f963..59010cb9ab8b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts @@ -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[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts index ce4b1de6e399..19062cf44c17 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { RoleMappingsRouter } from './role_mappings_router'; +export { RoleMappings } from './role_mappings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx index f50fc21d5ba5..2e179dc2b6ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx @@ -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(); - - expect(wrapper.find(AttributeSelector)).toHaveLength(1); - expect(wrapper.find(RoleSelector)).toHaveLength(5); - }); - - it('returns Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - - it('renders DeleteMappingCallout for existing mapping', () => { setMockValues({ ...mockValues, roleMapping: asRoleMapping }); const wrapper = shallow(); - 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(); - - 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(); - 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(); + + expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all'); + }); + + it('handles all/specific engines radio change', () => { + const wrapper = shallow(); + 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(); + 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]); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx index 610ceae8856f..0f201889b2f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -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 = ({ 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 = ({ isNew }) => { handleAttributeSelectorChange, handleAttributeValueChange, handleAuthProviderChange, - handleDeleteMapping, handleEngineSelectionChange, handleRoleChange, handleSaveMapping, - initializeRoleMapping, - resetState, + closeRoleMappingFlyout, } = useActions(RoleMappingsLogic); const { @@ -86,7 +55,6 @@ export const RoleMapping: React.FC = ({ isNew }) => { attributes, availableAuthProviders, availableEngines, - dataLoading, elasticsearchRoles, hasAdvancedRoles, multipleAuthProvidersConfig, @@ -94,154 +62,97 @@ export const RoleMapping: React.FC = ({ isNew }) => { roleType, selectedEngines, selectedAuthProviders, + selectedOptions, } = useValues(RoleMappingsLogic); - useEffect(() => { - initializeRoleMapping(roleId); - return resetState; - }, []); + const isNew = !roleMapping; + const hasEngineAssignment = selectedEngines.size > 0 || accessAllEngines; - if (dataLoading) return ; + 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 = ( - - {SAVE_ROLE_MAPPING_LABEL} - - ); + const roleOptions = hasAdvancedRoles + ? [...standardRoleOptions, ...advancedRoleOptions] + : standardRoleOptions; - const engineSelector = (engine: Engine) => ( - { - handleEngineSelectionChange(engine.name, e.target.checked); - }} - label={engine.name} - /> - ); - - const advancedRoleSelectors = ( - <> - - - {ADVANCED_ROLE_SELECTORS_TITLE} - - - {ADVANCED_ROLE_TYPES.map(({ type, description }) => ( - , + }, + { + id: 'specific', + label: ( + - ))} - > - ); + ), + }, + ]; return ( - <> - - - - - - - - - - - - {ROLE_TITLE} - - - - {FULL_ENGINE_ACCESS_TITLE} - - - {STANDARD_ROLE_TYPES.map(({ type, description }) => ( - - ))} - {hasAdvancedRoles && advancedRoleSelectors} - - - {hasAdvancedRoles && ( - - - - {ENGINE_ACCESS_TITLE} - - - - - - {FULL_ENGINE_ACCESS_TITLE} - - {FULL_ENGINE_ACCESS_DESCRIPTION} - > - } - /> - - - <> - - - {LIMITED_ENGINE_ACCESS_TITLE} - - {LIMITED_ENGINE_ACCESS_DESCRIPTION} - > - } - /> - {!accessAllEngines && ( - - {availableEngines.map((engine) => engineSelector(engine))} - - )} - > - - - - )} - - - {roleMapping && } - - > + + + + + + {hasAdvancedRoles && ( + <> + + + handleAccessAllEnginesChange(id === 'all')} + legend={{ + children: {ENGINE_ASSIGNMENT_LABEL}, + }} + /> + + + ({ label: name, value: name }))} + onChange={(options) => { + handleEngineSelectionChange(options.map(({ value }) => value as string)); + }} + fullWidth + isDisabled={accessAllEngines || !roleHasScopedEngines(roleType)} + /> + + > + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx index c6da903e2091..4ccb1fec0f03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx @@ -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(); + + expect(wrapper.find(RoleMapping)).toHaveLength(1); + }); + + it('handles button click', () => { + setMockValues({ ...mockValues, roleMappings: [] }); + const wrapper = shallow(); + wrapper.find(EuiEmptyPrompt).dive().find(EuiButton).simulate('click'); + + expect(initializeRoleMapping).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx index 86e2e51d29a7..61ed70f515f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx @@ -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 ; - const addMappingButton = ; + const addMappingButton = ( + initializeRoleMapping()}> + {ROLE_MAPPING_ADD_BUTTON} + + ); const roleMappingEmptyState = ( @@ -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 = () => { <> + + {roleMappingFlyoutOpen && } 0}> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts index ada17fc9a732..d0534a2a0be5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -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(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index 00b944d91cbc..6981f48159a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -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; + roleMappingFlyoutOpen: boolean; + selectedOptions: EuiComboBoxOptionOption[]; } export const RoleMappingsLogic = kea>({ @@ -98,21 +97,20 @@ export const RoleMappingsLogic = kea 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 roleMapping || null, resetState: () => null, + closeRoleMappingFlyout: () => null, }, ], roleType: [ @@ -185,7 +184,7 @@ export const RoleMappingsLogic = kea roleMapping ? roleMapping.accessAllEngines : true, handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType), - handleAccessAllEnginesChange: (accessAllEngines) => !accessAllEngines, + handleAccessAllEnginesChange: (_, { selected }) => selected, }, ], attributeValue: [ @@ -197,6 +196,7 @@ export const RoleMappingsLogic = kea value, resetState: () => '', + closeRoleMappingFlyout: () => '', }, ], attributeName: [ @@ -206,6 +206,7 @@ export const RoleMappingsLogic = kea value, resetState: () => 'username', + closeRoleMappingFlyout: () => 'username', }, ], selectedEngines: [ @@ -214,13 +215,9 @@ export const RoleMappingsLogic = kea 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); - if (selected) { - newSelectedEngineNames.add(engineName); - } else { - newSelectedEngineNames.delete(engineName); - } + handleEngineSelectionChange: (_, { engineNames }) => { + const newSelectedEngineNames = new Set() as Set; + engineNames.forEach((engineName) => newSelectedEngineNames.add(engineName)); return newSelectedEngineNames; }, @@ -250,7 +247,27 @@ export const RoleMappingsLogic = kea 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 { + 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 { const { http } = HttpLogic.values; - const { navigateToUrl } = KibanaLogic.values; const { attributeName, @@ -330,7 +344,7 @@ export const RoleMappingsLogic = kea { clearFlashMessages(); }, + closeRoleMappingFlyout: () => { + clearFlashMessages(); + }, + openRoleMappingFlyout: () => { + clearFlashMessages(); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx deleted file mode 100644 index e9fc40ba1dbb..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx +++ /dev/null @@ -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(); - - expect(wrapper.find(Switch)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(3); - expect(wrapper.find(RoleMapping)).toHaveLength(2); - expect(wrapper.find(RoleMappings)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx deleted file mode 100644 index 7aa8b4067d9e..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx +++ /dev/null @@ -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 = () => ( - - - - - - - - - - - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts deleted file mode 100644 index e72f2b90758a..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts +++ /dev/null @@ -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}`); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts deleted file mode 100644 index 109d3de1b86d..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts +++ /dev/null @@ -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 }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 8d33bd2d130e..08aab7af164e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -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); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 9b59e0e19a5d..a491efcb234d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -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> = (props) = {canViewRoleMappings && ( - + )} {canManageEngines && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 872db3e149b6..c8fb009fb31d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -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'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts index 8aa58d08b96d..f125a9dd13aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts @@ -52,6 +52,6 @@ export interface ASRoleMapping extends RoleMapping { } export interface AdvanceRoleType { - type: RoleTypes; + id: RoleTypes; description: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.test.tsx deleted file mode 100644 index a02f6c43225c..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.test.tsx +++ /dev/null @@ -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(); - - expect(wrapper.find(EuiButtonTo)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.tsx deleted file mode 100644 index 097302e0aa5f..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.tsx +++ /dev/null @@ -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 = ({ path }) => ( - - {ADD_ROLE_MAPPING_BUTTON} - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx index 504acf9ae1c6..2258496464ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx @@ -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(); + 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(); const select = wrapper.find('[data-test-subj="ExternalAttributeSelect"]'); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx index 0ee093ed934c..bb8bf4ab1abf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx @@ -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 = ({ handleAuthProviderChange = () => null, }) => { return ( - - - {ATTRIBUTE_SELECTOR_TITLE} - - + {availableAuthProviders && multipleAuthProvidersConfig && ( - - - - { - handleAuthProviderChange(options.map((o) => (o as ChildOption).value)); - }} - fullWidth - isDisabled={disabled} - /> - - - - + + { + handleAuthProviderChange(options.map((o) => o.value || '')); + }} + fullWidth + isDisabled={disabled} + /> + )} - - - - ({ value: attribute, text: attribute }))} - onChange={(e) => { - handleAttributeSelectorChange(e.target.value, elasticsearchRoles[0]); - }} - fullWidth - disabled={disabled} - /> - - - - - {attributeName === 'role' ? ( - ({ - value: elasticsearchRole, - text: elasticsearchRole, - }))} - onChange={(e) => { - handleAttributeValueChange(e.target.value); - }} - fullWidth - disabled={disabled} - /> - ) : ( - { - handleAttributeValueChange(e.target.value); - }} - fullWidth - disabled={disabled} - /> - )} - - - - + + ({ value: attribute, text: attribute }))} + onChange={(e) => { + handleAttributeSelectorChange(e.target.value, elasticsearchRoles[0]); + }} + fullWidth + disabled={disabled} + /> + + + {attributeName === 'role' ? ( + ({ + value: elasticsearchRole, + text: elasticsearchRole, + }))} + onChange={(e) => { + handleAttributeValueChange(e.target.value); + }} + fullWidth + disabled={disabled} + /> + ) : ( + { + handleAttributeValueChange(e.target.value); + }} + fullWidth + disabled={disabled} + /> + )} + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index a172fbae18d8..7c53e37437e8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -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' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.test.tsx deleted file mode 100644 index c7556ee20e26..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.test.tsx +++ /dev/null @@ -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(); - - expect(wrapper.find(EuiCallOut)).toHaveLength(1); - expect(wrapper.find(EuiButton).prop('onClick')).toEqual(handleDeleteMapping); - }); - - it('handles button click', () => { - const wrapper = shallow(); - wrapper.find(EuiButton).simulate('click'); - - expect(handleDeleteMapping).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.tsx deleted file mode 100644 index cb3c27038c56..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.tsx +++ /dev/null @@ -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 = ({ handleDeleteMapping }) => ( - - {DELETE_ROLE_MAPPING_DESCRIPTION} - - {DELETE_ROLE_MAPPING_BUTTON} - - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts index e6320dbb7fee..6f67bc682f33 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts @@ -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'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx new file mode 100644 index 000000000000..c0973bb2c950 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx @@ -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( + + + + ); + + 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( + + + + ); + + 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 + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx new file mode 100644 index 000000000000..bae991fef365 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx @@ -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 = ({ + children, + isNew, + disabled, + closeRoleMappingFlyout, + handleSaveMapping, +}) => ( + + + + + + {isNew ? ROLE_MAPPING_FLYOUT_CREATE_TITLE : ROLE_MAPPING_FLYOUT_UPDATE_TITLE} + + + + {ROLE_MAPPING_FLYOUT_DESCRIPTION} + + + + {children} + + + + + + {CANCEL_BUTTON_LABEL} + + + + {isNew ? ROLE_MAPPING_FLYOUT_CREATE_BUTTON : ROLE_MAPPING_FLYOUT_UPDATE_BUTTON} + + + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index e1c43dca581f..5ec84db478bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -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: , 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(); + wrapper.find('[data-test-subj="ManageButton"]').simulate('click'); + + expect(initializeRoleMapping).toHaveBeenCalled(); + }); + + it('handles delete click', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="DeleteButton"]').simulate('click'); + + expect(handleDeleteMapping).toHaveBeenCalled(); + }); + it('handles input change with special chars', () => { const wrapper = shallow(); const input = wrapper.find(EuiFieldSearch); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index a5f6fb368c96..d5d6d8b9cd22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -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 = ({ accessHeader, roleMappings, addMappingButton, - getRoleMappingPath, shouldShowAuthProvider, + initializeRoleMapping, + handleDeleteMapping, }) => { const [filterValue, updateValue] = useState(''); @@ -96,6 +98,23 @@ export const RoleMappingsTable: React.FC = ({ const getFirstAttributeName = (rules: RoleRules): string => Object.entries(rules)[0][0]; const getFirstAttributeValue = (rules: RoleRules): string => Object.entries(rules)[0][1]; + const rowActions = (id: string) => ( + <> + initializeRoleMapping(id)} + iconType="pencil" + aria-label={MANAGE_BUTTON_LABEL} + data-test-subj="ManageButton" + />{' '} + handleDeleteMapping(id)} + iconType="trash" + aria-label={DELETE_BUTTON_LABEL} + data-test-subj="DeleteButton" + /> + > + ); + return ( <> @@ -155,7 +174,7 @@ export const RoleMappingsTable: React.FC = ({ )} - {id && {MANAGE_BUTTON_LABEL}} + {id && rowActions(id)} {toolTip && } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.test.tsx new file mode 100644 index 000000000000..7a998f20e4d8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.test.tsx @@ -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(); + + expect(wrapper.find(EuiText)).toHaveLength(2); + expect(wrapper.find(EuiText).first().prop('children')).toBe('Foo'); + expect(wrapper.find(EuiSpacer)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.tsx new file mode 100644 index 000000000000..69d29c59dcd6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.tsx @@ -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 = ({ label, description }) => ( + <> + {label.charAt(0).toUpperCase() + label.toLowerCase().slice(1)} + + + {description} + + + > +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_selector.test.tsx index c34a8939801e..3f3c4abb0447 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_selector.test.tsx @@ -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(); - expect(wrapper.find(EuiRadio)).toHaveLength(1); + expect(wrapper.find(EuiRadioGroup)).toHaveLength(1); }); it('calls method on change', () => { const wrapper = shallow(); - 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(); - - expect(wrapper.find(EuiRadio).prop('checked')).toEqual(false); - }); - - it('sets checked attribute on radio when option matched type', () => { - const wrapper = shallow(); - const radio = wrapper.find(EuiRadio); - - expect(radio.prop('checked')).toEqual(true); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_selector.tsx index 9eef1a4e5d30..82780010632f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_selector.tsx @@ -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 = ({ - disabled, - roleType, - roleTypeOption, - description, - onChange, -}) => ( - - { - onChange(roleTypeOption); - }} - label={ - <> - - {startCase(roleTypeOption)} - - - - {description} - - > - } - /> - -); +export const RoleSelector: React.FC = ({ label, roleType, roleOptions, onChange }) => { + const options = roleOptions.map(({ id, description, disabled }) => ({ + id, + label: , + disabled, + })); + + return ( + + r.id === roleType)[0].id} + onChange={(id) => { + onChange(id); + }} + legend={{ + children: {label}, + }} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 54085a9cd446..ba5fb7c9d377 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -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 = (props) => { } restrictWidth readOnlyMode={readOnlyMode}> - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx index 7d3e19dfe626..a5a3d6b491bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx @@ -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( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 0a6b6ef89b2a..5e5d6e2c82b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -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 }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts index b4355ba7aa58..62494b447efa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts @@ -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.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/index.ts index ce4b1de6e399..19062cf44c17 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { RoleMappingsRouter } from './role_mappings_router'; +export { RoleMappings } from './role_mappings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx index 4742b741c964..c42a195bffba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx @@ -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(); 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(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all'); }); - it('hides DeleteMappingCallout for new mapping', () => { - const wrapper = shallow(); - - expect(wrapper.find(DeleteMappingCallout)).toHaveLength(0); - }); - - it('handles group checkbox click', () => { + it('handles all/specific groups radio change', () => { const wrapper = shallow(); - 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(); - 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]); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx index fb366883601a..9df0c82312d2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -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: , + }, + { + id: 'specific', + label: ( + + ), + }, +]; -export const RoleMapping: React.FC = ({ 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 = ({ isNew }) => { availableAuthProviders, multipleAuthProvidersConfig, selectedAuthProviders, + selectedOptions, + roleMapping, } = useValues(RoleMappingsLogic); - useEffect(() => { - initializeRoleMappings(); - initializeRoleMapping(roleId); - return resetState; - }, [roleId]); - - if (dataLoading) return ; + 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 = ( - - {SAVE_ROLE_MAPPING_LABEL} - - ); - return ( - <> - - - - - - + + + + + + handleAllGroupsSelectionChange(id === 'all')} + legend={{ + children: {GROUP_ASSIGNMENT_LABEL}, + }} /> - - - - - - {ROLE_LABEL} - - - {roleTypes.map(({ type, description }) => ( - - ))} - - - - - - {GROUP_ASSIGNMENT_TITLE} - - - - - <> - {availableGroups.map(({ id, name }) => ( - { - handleGroupSelectionChange(id, e.target.checked); - }} - label={name} - disabled={includeInAllGroups} - /> - ))} - - { - handleAllGroupsSelectionChange(e.target.checked); - }} - label={GROUP_ASSIGNMENT_ALL_GROUPS_LABEL} - /> - > - - - - - - - {!isNew && } - - > + + + ({ label: name, value: id }))} + onChange={(options) => { + handleGroupSelectionChange(options.map(({ value }) => value as string)); + }} + fullWidth + isDisabled={includeInAllGroups} + /> + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx index c6da903e2091..d7be753eec17 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx @@ -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(); + + expect(wrapper.find(RoleMapping)).toHaveLength(1); + }); + + it('handles button click', () => { + setMockValues({ ...mockValues, roleMappings: [] }); + const wrapper = shallow(); + wrapper.find(EuiEmptyPrompt).dive().find(EuiButton).simulate('click'); + + expect(initializeRoleMapping).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index 9ec0dfc0acef..e5a9a31d463b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -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 ; - const addMappingButton = ; + const addMappingButton = ( + initializeRoleMapping()}> + {ROLE_MAPPING_ADD_BUTTON} + + ); const emptyPrompt = ( { 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 = () => { <> + + {roleMappingFlyoutOpen && } {roleMappings.length === 0 ? emptyPrompt : roleMappingsTable} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts index 3c9cf01bbb4a..f45491334567 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -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(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index 0df7f0ba3756..85f3bb49ee50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -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; + roleMappingFlyoutOpen: boolean; + selectedOptions: EuiComboBoxOptionOption[]; } export const RoleMappingsLogic = kea>({ @@ -92,7 +94,7 @@ export const RoleMappingsLogic = kea 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 ({ 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 roleMapping || null, resetState: () => null, + closeRoleMappingFlyout: () => null, }, ], roleType: [ @@ -178,6 +183,7 @@ export const RoleMappingsLogic = kea value, resetState: () => '', + closeRoleMappingFlyout: () => '', }, ], attributeName: [ @@ -187,6 +193,7 @@ export const RoleMappingsLogic = kea value, resetState: () => 'username', + closeRoleMappingFlyout: () => 'username', }, ], selectedGroups: [ @@ -200,13 +207,10 @@ export const RoleMappingsLogic = kea group.name === DEFAULT_GROUP_NAME) .map((group) => group.id) ), - handleGroupSelectionChange: (groups, { groupId, selected }) => { - const newSelectedGroupNames = new Set(groups as Set); - if (selected) { - newSelectedGroupNames.add(groupId); - } else { - newSelectedGroupNames.delete(groupId); - } + handleGroupSelectionChange: (_, { groupIds }) => { + const newSelectedGroupNames = new Set() as Set; + groupIds.forEach((groupId) => newSelectedGroupNames.add(groupId)); + return newSelectedGroupNames; }, }, @@ -234,7 +238,27 @@ export const RoleMappingsLogic = kea 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 { + 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 { - 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 { const { http } = HttpLogic.values; - const { navigateToUrl } = KibanaLogic.values; const { attributeName, attributeValue, @@ -315,7 +334,7 @@ export const RoleMappingsLogic = kea { clearFlashMessages(); }, + closeRoleMappingFlyout: () => { + clearFlashMessages(); + }, + openRoleMappingFlyout: () => { + clearFlashMessages(); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx deleted file mode 100644 index e9fc40ba1dbb..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx +++ /dev/null @@ -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(); - - expect(wrapper.find(Switch)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(3); - expect(wrapper.find(RoleMapping)).toHaveLength(2); - expect(wrapper.find(RoleMappings)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx deleted file mode 100644 index fa5ab12c8afc..000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx +++ /dev/null @@ -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 = () => ( - <> - - - - - - - - - - - - - > -); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9b301d8cd04a..236108ccda8a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "ロールマッピングが正常に削除されました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b43d10cc0093..0e3edc387bbe 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "已成功删除角色映射",
{FULL_ENGINE_ACCESS_DESCRIPTION}
{LIMITED_ENGINE_ACCESS_DESCRIPTION}
{DELETE_ROLE_MAPPING_DESCRIPTION}
{ROLE_MAPPING_FLYOUT_DESCRIPTION}
{description}