From fc511f9ec1d8fc9d6f8f1eb1dc4c4ef514f985a4 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Fri, 4 Jun 2021 14:22:31 -0500 Subject: [PATCH] [Enterprise Search] Convert Role mappings for both apps to use flyouts (#101198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add RoleOptionLabel component * Refactor RoleSelector to use EuiRadioGroup Previously, we used individual radio buttons in a map in the component. However the new designs have a shared label and work best in the EuiRadioGroup component. * Add reducer and actions to logic file for flyout visibility * Remove redirects in favor of refreshing lists With the existing multi-page view, we redirect after creating, editing or deleting a mapping. We now simply refresh the list after the action. Also as a part of this commit, we show a hard-coded error message if a user tries to navigate to a non-existant mapping, instead of redirecting to a 404 page * Add RoleMappingFlyout component * Refactor AttributeSelector No longer uses a panel or has the need for flex groups - Also added a test for 100% coverage * Refactor RoleMappingsTable - Use EuiButtonIcons instead of Link - Manage button now triggers flyout instead of linking to route * Remove AddRoleMappingButton We can just use an EuiButton to trigger the flyout * Convert to use RoleSelector syntax - Passes the entire array to the component instead of mapping. - Uses ‘id’ instead of ‘type’ to match EUI component - For App Search, as per design and PM direction, dropping labels for advanced and standard roles and showing them all in the same list. - Removed unused constant and i18ns * Move constants to shared Will do a lot more of this in a future PR * Remove DeleteMappingCallout - This now an action in the row - Also added tests for correct titles for 100% test coverage * Remove routers and routes - SPA FTW * No longer pass isNew as prop - Determine based on existence of Role Mapping instead * No longer need to initialze role mapping in the component This will become a flyout and the intialization will be triggered when the button in the table is clicked. * Remove flash messages This will be handled globally in the main component. * Wrap components with flyout Also add to main RoleMappings views * Add form row validation for App Search * Remove unnecessary layout components - Don’t need the panel, headings, spacer, and Flex components - Also removed constants and i18n from unused headings * Wire up handleDeleteMapping to take ID param The method now passes the ID directly from the table row action item * Add EuiPortal wrapper for flyout Without this, the flyout was was under the overlay. Hide whitespace changes on this commit * Add spacer to better match design * Update constants for new copy from design * Replace all engines/groups radio and group/engine selectors - The designs call for a radio group and a combo box, instead of separate radios and a list of checkboxes - Also added a spacer to each layout * Remove util that is no longer needed - This was used for generating routes that are no longer there - Also removed unused test file from a component deleted in an earlier PR - Fix test since spacer was added * Add missing i18n constant * Add back missing scoped engine check * Rename roleId -> roleMappingId * Use shared constant for “Cancel” Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/role_mappings/constants.ts | 60 ++-- .../components/role_mappings/index.ts | 2 +- .../role_mappings/role_mapping.test.tsx | 71 ++--- .../components/role_mappings/role_mapping.tsx | 281 ++++++------------ .../role_mappings/role_mappings.test.tsx | 22 +- .../role_mappings/role_mappings.tsx | 33 +- .../role_mappings/role_mappings_logic.test.ts | 90 ++++-- .../role_mappings/role_mappings_logic.ts | 106 ++++--- .../role_mappings_router.test.tsx | 26 -- .../role_mappings/role_mappings_router.tsx | 29 -- .../components/role_mappings/utils.test.ts | 16 - .../components/role_mappings/utils.ts | 12 - .../applications/app_search/index.test.tsx | 6 +- .../public/applications/app_search/index.tsx | 4 +- .../public/applications/app_search/routes.ts | 2 - .../app_search/utils/role/types.ts | 2 +- .../add_role_mapping_button.test.tsx | 22 -- .../role_mapping/add_role_mapping_button.tsx | 22 -- .../role_mapping/attribute_selector.test.tsx | 8 + .../role_mapping/attribute_selector.tsx | 140 ++++----- .../shared/role_mapping/constants.ts | 59 ++++ .../delete_mapping_callout.test.tsx | 31 -- .../role_mapping/delete_mapping_callout.tsx | 29 -- .../applications/shared/role_mapping/index.ts | 4 +- .../role_mapping/role_mapping_flyout.test.tsx | 64 ++++ .../role_mapping/role_mapping_flyout.tsx | 90 ++++++ .../role_mapping/role_mappings_table.test.tsx | 20 +- .../role_mapping/role_mappings_table.tsx | 29 +- .../role_mapping/role_option_label.test.tsx | 24 ++ .../shared/role_mapping/role_option_label.tsx | 26 ++ .../role_mapping/role_selector.test.tsx | 30 +- .../shared/role_mapping/role_selector.tsx | 68 ++--- .../applications/workplace_search/index.tsx | 4 +- .../workplace_search/routes.test.tsx | 8 - .../applications/workplace_search/routes.ts | 3 - .../views/role_mappings/constants.ts | 42 ++- .../views/role_mappings/index.ts | 2 +- .../views/role_mappings/role_mapping.test.tsx | 49 ++- .../views/role_mappings/role_mapping.tsx | 226 +++++--------- .../role_mappings/role_mappings.test.tsx | 22 +- .../views/role_mappings/role_mappings.tsx | 29 +- .../role_mappings/role_mappings_logic.test.ts | 79 +++-- .../role_mappings/role_mappings_logic.ts | 93 +++--- .../role_mappings_router.test.tsx | 26 -- .../role_mappings/role_mappings_router.tsx | 34 --- .../translations/translations/ja-JP.json | 14 +- .../translations/translations/zh-CN.json | 14 +- 47 files changed, 1048 insertions(+), 1025 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx 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: