diff --git a/x-pack/plugins/security/public/management/management_urls.ts b/x-pack/plugins/security/public/management/management_urls.ts index 4a950b2ba1a16..371de4fb7f481 100644 --- a/x-pack/plugins/security/public/management/management_urls.ts +++ b/x-pack/plugins/security/public/management/management_urls.ts @@ -9,3 +9,8 @@ export const EDIT_ROLE_MAPPING_PATH = `/edit`; export const getEditRoleMappingHref = (roleMappingName: string) => `${EDIT_ROLE_MAPPING_PATH}/${encodeURIComponent(roleMappingName)}`; + +export const CLONE_ROLE_MAPPING_PATH = `/clone`; + +export const getCloneRoleMappingHref = (roleMappingName: string) => + `${CLONE_ROLE_MAPPING_PATH}/${encodeURIComponent(roleMappingName)}`; diff --git a/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx index f9194860ddded..9130a6ba1830c 100644 --- a/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -24,10 +24,12 @@ interface Props { export type DeleteRoleMappings = ( roleMappings: RoleMapping[], - onSuccess?: OnSuccessCallback + onSuccess?: OnSuccessCallback, + onCancel?: OnCancelCallback ) => void; type OnSuccessCallback = (deletedRoleMappings: string[]) => void; +type OnCancelCallback = () => void; export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI, @@ -39,10 +41,12 @@ export const DeleteProvider: React.FunctionComponent = ({ const [isDeleteInProgress, setIsDeleteInProgress] = useState(false); const onSuccessCallback = useRef(null); + const onCancelCallback = useRef(null); const deleteRoleMappingsPrompt: DeleteRoleMappings = ( roleMappingsToDelete, - onSuccess = () => undefined + onSuccess = () => undefined, + onCancel = () => undefined ) => { if (!roleMappingsToDelete || !roleMappingsToDelete.length) { throw new Error('No Role Mappings specified for delete'); @@ -50,6 +54,7 @@ export const DeleteProvider: React.FunctionComponent = ({ setIsModalOpen(true); setRoleMappings(roleMappingsToDelete); onSuccessCallback.current = onSuccess; + onCancelCallback.current = onCancel; }; const closeModal = () => { @@ -57,6 +62,13 @@ export const DeleteProvider: React.FunctionComponent = ({ setRoleMappings([]); }; + const handleCancelModel = () => { + closeModal(); + if (onCancelCallback.current) { + onCancelCallback.current(); + } + }; + const deleteRoleMappings = async () => { let result; @@ -161,7 +173,7 @@ export const DeleteProvider: React.FunctionComponent = ({ } ) } - onCancel={closeModal} + onCancel={handleCancelModel} onConfirm={deleteRoleMappings} cancelButtonText={i18n.translate( 'xpack.security.management.roleMappings.deleteRoleMapping.confirmModal.cancelButtonLabel', diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx index b624da2cd88b4..af7a2fb8d5240 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx @@ -39,6 +39,7 @@ describe('EditRoleMappingPage', () => { return mountWithIntl( ; rolesAPIClient: PublicMethodsOf; @@ -295,13 +296,17 @@ export class EditRoleMappingPage extends Component { }); }; - private editingExistingRoleMapping = () => typeof this.props.name === 'string'; + private editingExistingRoleMapping = () => + typeof this.props.name === 'string' && this.props.action === 'edit'; + + private cloningExistingRoleMapping = () => + typeof this.props.name === 'string' && this.props.action === 'clone'; private async loadAppData() { try { const [features, roleMapping] = await Promise.all([ this.props.roleMappingsAPI.checkRoleMappingFeatures(), - this.editingExistingRoleMapping() + this.editingExistingRoleMapping() || this.cloningExistingRoleMapping() ? this.props.roleMappingsAPI.getRoleMapping(this.props.name!) : Promise.resolve({ name: '', @@ -327,7 +332,10 @@ export class EditRoleMappingPage extends Component { hasCompatibleRealms, canUseStoredScripts, canUseInlineScripts, - roleMapping, + roleMapping: { + ...roleMapping, + name: this.cloningExistingRoleMapping() ? '' : roleMapping.name, + }, }); } catch (e) { this.props.notifications.toasts.addDanger({ diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx index 5f237e6504d32..d9009d49b592b 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -10,7 +10,7 @@ import { act } from '@testing-library/react'; import React from 'react'; import { findTestSubject, mountWithIntl, nextTick } from '@kbn/test/jest'; -import type { CoreStart, ScopedHistory } from 'src/core/public'; +import type { CoreStart } from 'src/core/public'; import { coreMock, scopedHistoryMock } from 'src/core/public/mocks'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; @@ -21,7 +21,7 @@ import { EmptyPrompt } from './empty_prompt'; import { RoleMappingsGridPage } from './role_mappings_grid_page'; describe('RoleMappingsGridPage', () => { - let history: ScopedHistory; + let history: ReturnType; let coreStart: CoreStart; const renderView = ( @@ -44,6 +44,7 @@ describe('RoleMappingsGridPage', () => { beforeEach(() => { history = scopedHistoryMock.create(); + history.createHref.mockImplementation((location) => location.pathname!); coreStart = coreMock.createStart(); }); @@ -188,6 +189,7 @@ describe('RoleMappingsGridPage', () => { expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); + findTestSubject(wrapper, `euiCollapsedItemActionsButton`).simulate('click'); findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); @@ -246,4 +248,55 @@ describe('RoleMappingsGridPage', () => { `"The kibana_user role is deprecated. I don't like you."` ); }); + + it('renders role mapping actions as appropriate', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + success: true, + }, + ]); + + const wrapper = renderView(roleMappingsAPI); + await nextTick(); + wrapper.update(); + + const editButton = wrapper.find( + 'EuiButtonEmpty[data-test-subj="editRoleMappingButton-some-realm"]' + ); + expect(editButton).toHaveLength(1); + expect(editButton.prop('href')).toBe('/edit/some-realm'); + + const cloneButton = wrapper.find( + 'EuiButtonEmpty[data-test-subj="cloneRoleMappingButton-some-realm"]' + ); + expect(cloneButton).toHaveLength(1); + expect(cloneButton.prop('href')).toBe('/clone/some-realm'); + + const actionMenuButton = wrapper.find( + 'EuiButtonIcon[data-test-subj="euiCollapsedItemActionsButton"]' + ); + expect(actionMenuButton).toHaveLength(1); + + actionMenuButton.simulate('click'); + wrapper.update(); + + const deleteButton = wrapper.find( + 'EuiButtonEmpty[data-test-subj="deleteRoleMappingButton-some-realm"]' + ); + expect(deleteButton).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index ec386d75228e8..373f3366f36c8 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -6,7 +6,7 @@ */ import { EuiButton, - EuiButtonIcon, + EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, @@ -32,18 +32,23 @@ import type { import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import type { Role, RoleMapping } from '../../../../common/model'; import { DisabledBadge, EnabledBadge } from '../../badges'; -import { EDIT_ROLE_MAPPING_PATH, getEditRoleMappingHref } from '../../management_urls'; +import { + EDIT_ROLE_MAPPING_PATH, + getCloneRoleMappingHref, + getEditRoleMappingHref, +} from '../../management_urls'; import { RoleTableDisplay } from '../../role_table_display'; import type { RolesAPIClient } from '../../roles'; +import { ActionsEuiTableFormatting } from '../../table_utils'; import { DeleteProvider, NoCompatibleRealms, PermissionDenied, SectionLoading, } from '../components'; +import type { DeleteRoleMappings } from '../components/delete_provider/delete_provider'; import type { RoleMappingsAPIClient } from '../role_mappings_api_client'; import { EmptyPrompt } from './empty_prompt'; - interface Props { rolesAPIClient: PublicMethodsOf; roleMappingsAPI: PublicMethodsOf; @@ -63,6 +68,7 @@ interface State { } export class RoleMappingsGridPage extends Component { + private tableRef: React.RefObject>; constructor(props: any) { super(props); this.state = { @@ -73,6 +79,7 @@ export class RoleMappingsGridPage extends Component { selectedItems: [], error: undefined, }; + this.tableRef = React.createRef(); } public componentDidMount() { @@ -224,7 +231,13 @@ export class RoleMappingsGridPage extends Component { {(deleteRoleMappingsPrompt) => { return ( deleteRoleMappingsPrompt(selectedItems, this.onRoleMappingsDeleted)} + onClick={() => + deleteRoleMappingsPrompt( + selectedItems, + this.onRoleMappingsDeleted, + this.onRoleMappingsDeleteCancel + ) + } color="danger" data-test-subj="bulkDeleteActionButton" > @@ -260,27 +273,40 @@ export class RoleMappingsGridPage extends Component { }; return ( - { - return { - 'data-test-subj': 'roleMappingRow', - }; + + {(deleteRoleMappingPrompt) => { + return ( + + { + return { + 'data-test-subj': 'roleMappingRow', + }; + }} + /> + + ); }} - /> + ); }; - private getColumnConfig = () => { + private getColumnConfig = (deleteRoleMappingPrompt: DeleteRoleMappings) => { const config = [ { field: 'name', @@ -357,72 +383,97 @@ export class RoleMappingsGridPage extends Component { }), actions: [ { + isPrimary: true, render: (record: RoleMapping) => { + const title = i18n.translate( + 'xpack.security.management.roleMappings.actionCloneTooltip', + { defaultMessage: 'Clone' } + ); + const label = i18n.translate( + 'xpack.security.management.roleMappings.actionCloneAriaLabel', + { + defaultMessage: `Clone '{name}'`, + values: { name: record.name }, + } + ); return ( - - + = 1} {...reactRouterNavigate( this.props.history, - getEditRoleMappingHref(record.name) + getCloneRoleMappingHref(record.name) )} - /> + > + {title} + + + ); + }, + }, + { + render: (record: RoleMapping) => { + const title = i18n.translate( + 'xpack.security.management.roleMappings.actionDeleteTooltip', + { defaultMessage: 'Delete' } + ); + const label = i18n.translate( + 'xpack.security.management.roleMappings.actionDeleteAriaLabel', + { + defaultMessage: `Delete '{name}'`, + values: { name: record.name }, + } + ); + return ( + + = 1} + onClick={() => deleteRoleMappingPrompt([record], this.onRoleMappingsDeleted)} + > + {title} + ); }, }, { + isPrimary: true, render: (record: RoleMapping) => { + const label = i18n.translate( + 'xpack.security.management.roleMappings.actionEditAriaLabel', + { + defaultMessage: `Edit '{name}'`, + values: { name: record.name }, + } + ); + const title = i18n.translate( + 'xpack.security.management.roleMappings.actionEditTooltip', + { defaultMessage: 'Edit' } + ); return ( - - - - {(deleteRoleMappingPrompt) => { - return ( - - - deleteRoleMappingPrompt([record], this.onRoleMappingsDeleted) - } - /> - - ); - }} - - - + + = 1} + {...reactRouterNavigate( + this.props.history, + getEditRoleMappingHref(record.name) + )} + > + {title} + + ); }, }, @@ -438,6 +489,10 @@ export class RoleMappingsGridPage extends Component { } }; + private onRoleMappingsDeleteCancel = () => { + this.tableRef.current?.setSelection([]); + }; + private async checkPrivileges() { try { const { canManageRoleMappings, hasCompatibleRealms } = diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx index f6d17327b7118..892c7940675d3 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx @@ -100,7 +100,7 @@ describe('roleMappingsManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Mapping Edit Page: {"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} + Role Mapping Edit Page: {"action":"edit","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
`); @@ -128,7 +128,7 @@ describe('roleMappingsManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Mapping Edit Page: {"name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}} + Role Mapping Edit Page: {"action":"edit","name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}}
`); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx index 22d09e9e2a678..41e6a9562612d 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -56,7 +56,7 @@ export const roleMappingsManagementApp = Object.freeze({ const roleMappingsAPIClient = new RoleMappingsAPIClient(core.http); - const EditRoleMappingsPageWithBreadcrumbs = () => { + const EditRoleMappingsPageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { const { name } = useParams<{ name?: string }>(); // Additional decoding is a workaround for a bug in react-router's version of the `history` module. @@ -64,7 +64,7 @@ export const roleMappingsManagementApp = Object.freeze({ const decodedName = name ? tryDecodeURIComponent(name) : undefined; const breadcrumbObj = - name && decodedName + action === 'edit' && name && decodedName ? { text: decodedName, href: `/edit/${encodeURIComponent(name)}` } : { text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { @@ -75,6 +75,7 @@ export const roleMappingsManagementApp = Object.freeze({ return ( - + + + + diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 9194fea271442..aa507cf823eff 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -144,31 +144,33 @@ describe('', () => { expect(wrapper.find(PermissionDenied)).toHaveLength(0); - let editButton = wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-test-role-1"]'); + let editButton = wrapper.find('EuiButtonEmpty[data-test-subj="edit-role-action-test-role-1"]'); expect(editButton).toHaveLength(1); expect(editButton.prop('href')).toBe('/edit/test-role-1'); editButton = wrapper.find( - 'EuiButtonIcon[data-test-subj="edit-role-action-special%chars%role"]' + 'EuiButtonEmpty[data-test-subj="edit-role-action-special%chars%role"]' ); expect(editButton).toHaveLength(1); expect(editButton.prop('href')).toBe('/edit/special%25chars%25role'); - let cloneButton = wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-test-role-1"]'); + let cloneButton = wrapper.find( + 'EuiButtonEmpty[data-test-subj="clone-role-action-test-role-1"]' + ); expect(cloneButton).toHaveLength(1); expect(cloneButton.prop('href')).toBe('/clone/test-role-1'); cloneButton = wrapper.find( - 'EuiButtonIcon[data-test-subj="clone-role-action-special%chars%role"]' + 'EuiButtonEmpty[data-test-subj="clone-role-action-special%chars%role"]' ); expect(cloneButton).toHaveLength(1); expect(cloneButton.prop('href')).toBe('/clone/special%25chars%25role'); expect( - wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-disabled-role"]') + wrapper.find('EuiButtonEmpty[data-test-subj="edit-role-action-disabled-role"]') ).toHaveLength(1); expect( - wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-disabled-role"]') + wrapper.find('EuiButtonEmpty[data-test-subj="clone-role-action-disabled-role"]') ).toHaveLength(1); }); diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 909c5b1193cd9..d34a8bfea27bf 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -8,7 +8,7 @@ import type { EuiBasicTableColumn, EuiSwitchEvent } from '@elastic/eui'; import { EuiButton, - EuiButtonIcon, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, @@ -17,6 +17,7 @@ import { EuiSpacer, EuiSwitch, EuiText, + EuiToolTip, } from '@elastic/eui'; import _ from 'lodash'; import React, { Component } from 'react'; @@ -36,6 +37,7 @@ import { isRoleReserved, } from '../../../../common/model'; import { DeprecatedBadge, DisabledBadge, ReservedBadge } from '../../badges'; +import { ActionsEuiTableFormatting } from '../../table_utils'; import type { RolesAPIClient } from '../roles_api_client'; import { ConfirmDelete } from './confirm_delete'; import { PermissionDenied } from './permission_denied'; @@ -61,6 +63,7 @@ const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { }; export class RolesGridPage extends Component { + private tableRef: React.RefObject>; constructor(props: Props) { super(props); this.state = { @@ -72,6 +75,7 @@ export class RolesGridPage extends Component { permissionDenied: false, includeReservedRoles: true, }; + this.tableRef = React.createRef(); } public componentDidMount() { @@ -129,53 +133,56 @@ export class RolesGridPage extends Component { /> ) : null} - !role.metadata || !role.metadata._reserved, - selectableMessage: (selectable: boolean) => (!selectable ? 'Role is reserved' : ''), - onSelectionChange: (selection: Role[]) => this.setState({ selection }), - }} - pagination={{ - initialPageSize: 20, - pageSizeOptions: [10, 20, 30, 50, 100], - }} - items={this.state.visibleRoles} - loading={roles.length === 0} - search={{ - toolsLeft: this.renderToolsLeft(), - toolsRight: this.renderToolsRight(), - box: { - incremental: true, - 'data-test-subj': 'searchRoles', - }, - onChange: (query: Record) => { - this.setState({ - filter: query.queryText, - visibleRoles: this.getVisibleRoles( - this.state.roles, - query.queryText, - this.state.includeReservedRoles - ), - }); - }, - }} - sorting={{ - sort: { - field: 'name', - direction: 'asc', - }, - }} - rowProps={() => { - return { - 'data-test-subj': 'roleRow', - }; - }} - isSelectable - /> + + !role.metadata || !role.metadata._reserved, + selectableMessage: (selectable: boolean) => (!selectable ? 'Role is reserved' : ''), + onSelectionChange: (selection: Role[]) => this.setState({ selection }), + }} + pagination={{ + initialPageSize: 20, + pageSizeOptions: [10, 20, 30, 50, 100], + }} + items={this.state.visibleRoles} + loading={roles.length === 0} + search={{ + toolsLeft: this.renderToolsLeft(), + toolsRight: this.renderToolsRight(), + box: { + incremental: true, + 'data-test-subj': 'searchRoles', + }, + onChange: (query: Record) => { + this.setState({ + filter: query.queryText, + visibleRoles: this.getVisibleRoles( + this.state.roles, + query.queryText, + this.state.includeReservedRoles + ), + }); + }, + }} + sorting={{ + sort: { + field: 'name', + direction: 'asc', + }, + }} + ref={this.tableRef} + rowProps={(role: Role) => { + return { + 'data-test-subj': `roleRow`, + }; + }} + isSelectable + /> + ); }; @@ -219,48 +226,98 @@ export class RolesGridPage extends Component { width: '150px', actions: [ { - available: (role: Role) => !isRoleReadOnly(role), + available: (role: Role) => !isRoleReserved(role), + isPrimary: true, render: (role: Role) => { - const title = i18n.translate('xpack.security.management.roles.editRoleActionName', { - defaultMessage: `Edit {roleName}`, + const title = i18n.translate('xpack.security.management.roles.cloneRoleActionName', { + defaultMessage: `Clone`, + }); + + const label = i18n.translate('xpack.security.management.roles.cloneRoleActionLabel', { + defaultMessage: `Clone {roleName}`, values: { roleName: role.name }, }); return ( - + + = 1} + iconType={'copy'} + {...reactRouterNavigate( + this.props.history, + getRoleManagementHref('clone', role.name) + )} + > + {title} + + ); }, }, { - available: (role: Role) => !isRoleReserved(role), + available: (role: Role) => !role.metadata || !role.metadata._reserved, render: (role: Role) => { - const title = i18n.translate('xpack.security.management.roles.cloneRoleActionName', { - defaultMessage: `Clone {roleName}`, + const title = i18n.translate('xpack.security.management.roles.deleteRoleActionName', { + defaultMessage: `Delete`, + }); + + const label = i18n.translate( + 'xpack.security.management.roles.deleteRoleActionLabel', + { + defaultMessage: `Delete {roleName}`, + values: { roleName: role.name }, + } + ); + + return ( + + = 1} + iconType={'trash'} + onClick={() => this.deleteOneRole(role)} + > + {title} + + + ); + }, + }, + { + available: (role: Role) => !isRoleReadOnly(role), + enable: () => this.state.selection.length === 0, + isPrimary: true, + render: (role: Role) => { + const title = i18n.translate('xpack.security.management.roles.editRoleActionName', { + defaultMessage: `Edit`, + }); + + const label = i18n.translate('xpack.security.management.roles.editRoleActionLabel', { + defaultMessage: `Edit {roleName}`, values: { roleName: role.name }, }); return ( - + + = 1} + iconType={'pencil'} + {...reactRouterNavigate( + this.props.history, + getRoleManagementHref('edit', role.name) + )} + > + {title} + + ); }, }, @@ -337,6 +394,13 @@ export class RolesGridPage extends Component { this.loadRoles(); }; + private deleteOneRole = (roleToDelete: Role) => { + this.setState({ + selection: [roleToDelete], + showDeleteConfirmation: true, + }); + }; + private async loadRoles() { try { const roles = await this.props.rolesAPIClient.getRoles(); @@ -385,6 +449,7 @@ export class RolesGridPage extends Component {
); } + private renderToolsRight() { return ( { ); } private onCancelDelete = () => { - this.setState({ showDeleteConfirmation: false }); + this.setState({ showDeleteConfirmation: false, selection: [] }); + this.tableRef.current?.setSelection([]); }; } diff --git a/x-pack/plugins/security/public/management/table_utils.tsx b/x-pack/plugins/security/public/management/table_utils.tsx new file mode 100644 index 0000000000000..3f240daf3bd03 --- /dev/null +++ b/x-pack/plugins/security/public/management/table_utils.tsx @@ -0,0 +1,37 @@ +/* + * 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 { css } from '@emotion/react'; +import type { ReactNode } from 'react'; +import React from 'react'; + +interface ActionsEuiTableFormattingProps { + children: ReactNode; +} + +/* + * Notes to future engineer: + * We created this component because as this time EUI actions table where not allowing to pass + * props href on an action. In our case, we want our actions to work with href + * and onClick. Then the problem is that the design did not match with EUI example, therefore + * we are doing some css magic to only have icon showing up when user is hovering a row + */ +export const ActionsEuiTableFormatting = React.memo( + ({ children }) => ( +
+ {children} +
+ ) +); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d8bef37f6dc8f..1a5335ef93e72 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19835,7 +19835,6 @@ "xpack.security.management.roleMappings.rolesColumnName": "ロール", "xpack.security.management.roleMappingsTitle": "ロールマッピング", "xpack.security.management.roles.actionsColumnName": "アクション", - "xpack.security.management.roles.cloneRoleActionName": "{roleName} を複製", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "キャンセル", "xpack.security.management.roles.confirmDelete.deleteButtonLabel": "削除", "xpack.security.management.roles.confirmDelete.removingRolesDescription": "これらのロールを削除しようとしています:", @@ -19846,7 +19845,6 @@ "xpack.security.management.roles.deleteSelectedRolesButtonLabel": "ロール {numSelected} {numSelected, plural, one { } other {}} を削除しました", "xpack.security.management.roles.deletingRolesWarningMessage": "この操作は元に戻すことができません。", "xpack.security.management.roles.deniedPermissionTitle": "ロールを管理するにはパーミッションが必要です", - "xpack.security.management.roles.editRoleActionName": "{roleName} を編集", "xpack.security.management.roles.fetchingRolesErrorMessage": "ロールの取得中にエラーが発生:{message}", "xpack.security.management.roles.nameColumnName": "ロール", "xpack.security.management.roles.noIndexPatternsPermission": "利用可能なインデックスパターンのリストへのアクセス権が必要です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 30951b200dbda..529b692e3552e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20130,7 +20130,6 @@ "xpack.security.management.roleMappings.roleTemplates": "{templateCount, plural, other {# 个角色模板}}已定义", "xpack.security.management.roleMappingsTitle": "角色映射", "xpack.security.management.roles.actionsColumnName": "操作", - "xpack.security.management.roles.cloneRoleActionName": "克隆 {roleName}", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "取消", "xpack.security.management.roles.confirmDelete.deleteButtonLabel": "删除", "xpack.security.management.roles.confirmDelete.removingRolesDescription": "您即将删除以下角色:", @@ -20141,7 +20140,6 @@ "xpack.security.management.roles.deleteSelectedRolesButtonLabel": "删除 {numSelected} 个角色{numSelected, plural, other {}}", "xpack.security.management.roles.deletingRolesWarningMessage": "此操作无法撤消。", "xpack.security.management.roles.deniedPermissionTitle": "您需要用于管理角色的权限", - "xpack.security.management.roles.editRoleActionName": "编辑 {roleName}", "xpack.security.management.roles.fetchingRolesErrorMessage": "获取用户时出错:{message}", "xpack.security.management.roles.nameColumnName": "角色", "xpack.security.management.roles.noIndexPatternsPermission": "您需要访问可用索引模式列表的权限。", diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 190323b8aaf18..54c92c4815b5d 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -81,7 +81,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('allows a role mapping to be deleted', async () => { - await testSubjects.click(`deleteRoleMappingButton-new_role_mapping`); + await testSubjects.click('euiCollapsedItemActionsButton'); + await testSubjects.click('deleteRoleMappingButton-new_role_mapping'); await testSubjects.click('confirmModalConfirmButton'); await testSubjects.existOrFail('deletedRoleMappingSuccessToast'); }); @@ -162,6 +163,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } }); + it('allows a role mapping to be cloned', async () => { + await testSubjects.click('cloneRoleMappingButton-a_enabled_role_mapping'); + await testSubjects.setValue('roleMappingFormNameInput', 'cloned_role_mapping'); + await testSubjects.click('saveRoleMappingButton'); + await testSubjects.existOrFail('savedRoleMappingSuccessToast'); + const rows = await testSubjects.findAll('roleMappingRow'); + expect(rows.length).to.eql(mappings.length + 1); + }); + it('allows a role mapping to be edited', async () => { await testSubjects.click('roleMappingName'); await testSubjects.click('saveRoleMappingButton');