mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* add clobe to role mapping and update functionalit in role to match UX * fix some I * fix jest test + fix table selection when canceling deletion * add tests around clone action in role mapping * fix i18n * remove i18n * review Greg I * fix styling + name * add explaination Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
parent
f537c11b86
commit
bbc00de080
14 changed files with 430 additions and 181 deletions
|
@ -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)}`;
|
||||
|
|
|
@ -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<Props> = ({
|
||||
roleMappingsAPI,
|
||||
|
@ -39,10 +41,12 @@ export const DeleteProvider: React.FunctionComponent<Props> = ({
|
|||
const [isDeleteInProgress, setIsDeleteInProgress] = useState(false);
|
||||
|
||||
const onSuccessCallback = useRef<OnSuccessCallback | null>(null);
|
||||
const onCancelCallback = useRef<OnCancelCallback | null>(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<Props> = ({
|
|||
setIsModalOpen(true);
|
||||
setRoleMappings(roleMappingsToDelete);
|
||||
onSuccessCallback.current = onSuccess;
|
||||
onCancelCallback.current = onCancel;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
|
@ -57,6 +62,13 @@ export const DeleteProvider: React.FunctionComponent<Props> = ({
|
|||
setRoleMappings([]);
|
||||
};
|
||||
|
||||
const handleCancelModel = () => {
|
||||
closeModal();
|
||||
if (onCancelCallback.current) {
|
||||
onCancelCallback.current();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRoleMappings = async () => {
|
||||
let result;
|
||||
|
||||
|
@ -161,7 +173,7 @@ export const DeleteProvider: React.FunctionComponent<Props> = ({
|
|||
}
|
||||
)
|
||||
}
|
||||
onCancel={closeModal}
|
||||
onCancel={handleCancelModel}
|
||||
onConfirm={deleteRoleMappings}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.security.management.roleMappings.deleteRoleMapping.confirmModal.cancelButtonLabel',
|
||||
|
|
|
@ -39,6 +39,7 @@ describe('EditRoleMappingPage', () => {
|
|||
return mountWithIntl(
|
||||
<KibanaContextProvider services={coreStart}>
|
||||
<EditRoleMappingPage
|
||||
action="edit"
|
||||
name={name}
|
||||
roleMappingsAPI={roleMappingsAPI}
|
||||
rolesAPIClient={rolesAPI}
|
||||
|
|
|
@ -51,6 +51,7 @@ interface State {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
action: 'edit' | 'clone';
|
||||
name?: string;
|
||||
roleMappingsAPI: PublicMethodsOf<RoleMappingsAPIClient>;
|
||||
rolesAPIClient: PublicMethodsOf<RolesAPIClient>;
|
||||
|
@ -295,13 +296,17 @@ export class EditRoleMappingPage extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
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<Props, State> {
|
|||
hasCompatibleRealms,
|
||||
canUseStoredScripts,
|
||||
canUseInlineScripts,
|
||||
roleMapping,
|
||||
roleMapping: {
|
||||
...roleMapping,
|
||||
name: this.cloningExistingRoleMapping() ? '' : roleMapping.name,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
this.props.notifications.toasts.addDanger({
|
||||
|
|
|
@ -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<typeof scopedHistoryMock.create>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<RolesAPIClient>;
|
||||
roleMappingsAPI: PublicMethodsOf<RoleMappingsAPIClient>;
|
||||
|
@ -63,6 +68,7 @@ interface State {
|
|||
}
|
||||
|
||||
export class RoleMappingsGridPage extends Component<Props, State> {
|
||||
private tableRef: React.RefObject<EuiInMemoryTable<RoleMapping>>;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -73,6 +79,7 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
selectedItems: [],
|
||||
error: undefined,
|
||||
};
|
||||
this.tableRef = React.createRef();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -224,7 +231,13 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
{(deleteRoleMappingsPrompt) => {
|
||||
return (
|
||||
<EuiButton
|
||||
onClick={() => 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<Props, State> {
|
|||
};
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
items={roleMappings!}
|
||||
itemId="name"
|
||||
columns={this.getColumnConfig()}
|
||||
search={search}
|
||||
sorting={sorting}
|
||||
selection={selection}
|
||||
pagination={pagination}
|
||||
loading={loadState === 'loadingTable'}
|
||||
message={message}
|
||||
isSelectable={true}
|
||||
rowProps={() => {
|
||||
return {
|
||||
'data-test-subj': 'roleMappingRow',
|
||||
};
|
||||
<DeleteProvider
|
||||
roleMappingsAPI={this.props.roleMappingsAPI}
|
||||
notifications={this.props.notifications}
|
||||
>
|
||||
{(deleteRoleMappingPrompt) => {
|
||||
return (
|
||||
<ActionsEuiTableFormatting>
|
||||
<EuiInMemoryTable
|
||||
items={roleMappings!}
|
||||
itemId="name"
|
||||
columns={this.getColumnConfig(deleteRoleMappingPrompt)}
|
||||
hasActions={true}
|
||||
search={search}
|
||||
sorting={sorting}
|
||||
selection={selection}
|
||||
pagination={pagination}
|
||||
loading={loadState === 'loadingTable'}
|
||||
message={message}
|
||||
isSelectable={true}
|
||||
ref={this.tableRef}
|
||||
rowProps={() => {
|
||||
return {
|
||||
'data-test-subj': 'roleMappingRow',
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</ActionsEuiTableFormatting>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DeleteProvider>
|
||||
);
|
||||
};
|
||||
|
||||
private getColumnConfig = () => {
|
||||
private getColumnConfig = (deleteRoleMappingPrompt: DeleteRoleMappings) => {
|
||||
const config = [
|
||||
{
|
||||
field: 'name',
|
||||
|
@ -357,72 +383,97 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
}),
|
||||
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 (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.security.management.roleMappings.actionEditTooltip',
|
||||
{ defaultMessage: 'Edit' }
|
||||
)}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.security.management.roleMappings.actionEditAriaLabel',
|
||||
{
|
||||
defaultMessage: `Edit '{name}'`,
|
||||
values: { name: record.name },
|
||||
}
|
||||
)}
|
||||
iconType="pencil"
|
||||
<EuiToolTip content={title}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={label}
|
||||
iconType="copy"
|
||||
color="primary"
|
||||
data-test-subj={`editRoleMappingButton-${record.name}`}
|
||||
data-test-subj={`cloneRoleMappingButton-${record.name}`}
|
||||
disabled={this.state.selectedItems.length >= 1}
|
||||
{...reactRouterNavigate(
|
||||
this.props.history,
|
||||
getEditRoleMappingHref(record.name)
|
||||
getCloneRoleMappingHref(record.name)
|
||||
)}
|
||||
/>
|
||||
>
|
||||
{title}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<DeleteProvider
|
||||
roleMappingsAPI={this.props.roleMappingsAPI}
|
||||
notifications={this.props.notifications}
|
||||
>
|
||||
{(deleteRoleMappingPrompt) => {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.security.management.roleMappings.actionDeleteTooltip',
|
||||
{ defaultMessage: 'Delete' }
|
||||
)}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.security.management.roleMappings.actionDeleteAriaLabel',
|
||||
{
|
||||
defaultMessage: `Delete '{name}'`,
|
||||
values: { name: record.name },
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj={`deleteRoleMappingButton-${record.name}`}
|
||||
onClick={() =>
|
||||
deleteRoleMappingPrompt([record], this.onRoleMappingsDeleted)
|
||||
}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}}
|
||||
</DeleteProvider>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiToolTip content={title}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={label}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj={`deleteRoleMappingButton-${record.name}`}
|
||||
disabled={this.state.selectedItems.length >= 1}
|
||||
onClick={() => deleteRoleMappingPrompt([record], this.onRoleMappingsDeleted)}
|
||||
>
|
||||
{title}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<EuiToolTip content={title}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={label}
|
||||
iconType="pencil"
|
||||
color="primary"
|
||||
data-test-subj={`editRoleMappingButton-${record.name}`}
|
||||
disabled={this.state.selectedItems.length >= 1}
|
||||
{...reactRouterNavigate(
|
||||
this.props.history,
|
||||
getEditRoleMappingHref(record.name)
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -438,6 +489,10 @@ export class RoleMappingsGridPage extends Component<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRoleMappingsDeleteCancel = () => {
|
||||
this.tableRef.current?.setSelection([]);
|
||||
};
|
||||
|
||||
private async checkPrivileges() {
|
||||
try {
|
||||
const { canManageRoleMappings, hasCompatibleRealms } =
|
||||
|
|
|
@ -100,7 +100,7 @@ describe('roleMappingsManagementApp', () => {
|
|||
expect(docTitle.reset).not.toHaveBeenCalled();
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
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":""}}}
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
@ -128,7 +128,7 @@ describe('roleMappingsManagementApp', () => {
|
|||
expect(docTitle.reset).not.toHaveBeenCalled();
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
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":""}}}
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<Breadcrumb text={breadcrumbObj.text} href={breadcrumbObj.href}>
|
||||
<EditRoleMappingPage
|
||||
action={action}
|
||||
name={decodedName}
|
||||
roleMappingsAPI={roleMappingsAPIClient}
|
||||
rolesAPIClient={new RolesAPIClient(core.http)}
|
||||
|
@ -105,7 +106,10 @@ export const roleMappingsManagementApp = Object.freeze({
|
|||
/>
|
||||
</Route>
|
||||
<Route path="/edit/:name?">
|
||||
<EditRoleMappingsPageWithBreadcrumbs />
|
||||
<EditRoleMappingsPageWithBreadcrumbs action="edit" />
|
||||
</Route>
|
||||
<Route path="/clone/:name">
|
||||
<EditRoleMappingsPageWithBreadcrumbs action="clone" />
|
||||
</Route>
|
||||
</Breadcrumb>
|
||||
</BreadcrumbsProvider>
|
||||
|
|
|
@ -144,31 +144,33 @@ describe('<RolesGridPage />', () => {
|
|||
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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<Props, State> {
|
||||
private tableRef: React.RefObject<EuiInMemoryTable<Role>>;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -72,6 +75,7 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
permissionDenied: false,
|
||||
includeReservedRoles: true,
|
||||
};
|
||||
this.tableRef = React.createRef();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -129,53 +133,56 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
/>
|
||||
) : null}
|
||||
|
||||
<EuiInMemoryTable
|
||||
itemId="name"
|
||||
responsive={false}
|
||||
columns={this.getColumnConfig()}
|
||||
hasActions={true}
|
||||
selection={{
|
||||
selectable: (role: Role) => !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<string, any>) => {
|
||||
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
|
||||
/>
|
||||
<ActionsEuiTableFormatting>
|
||||
<EuiInMemoryTable
|
||||
itemId="name"
|
||||
responsive={false}
|
||||
columns={this.getColumnConfig()}
|
||||
hasActions={true}
|
||||
selection={{
|
||||
selectable: (role: Role) => !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<string, any>) => {
|
||||
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
|
||||
/>
|
||||
</ActionsEuiTableFormatting>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -218,49 +225,99 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
}),
|
||||
width: '150px',
|
||||
actions: [
|
||||
{
|
||||
available: (role: Role) => !isRoleReadOnly(role),
|
||||
render: (role: Role) => {
|
||||
const title = i18n.translate('xpack.security.management.roles.editRoleActionName', {
|
||||
defaultMessage: `Edit {roleName}`,
|
||||
values: { roleName: role.name },
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={title}
|
||||
data-test-subj={`edit-role-action-${role.name}`}
|
||||
title={title}
|
||||
color={'primary'}
|
||||
iconType={'pencil'}
|
||||
{...reactRouterNavigate(
|
||||
this.props.history,
|
||||
getRoleManagementHref('edit', role.name)
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
available: (role: Role) => !isRoleReserved(role),
|
||||
isPrimary: true,
|
||||
render: (role: Role) => {
|
||||
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 (
|
||||
<EuiButtonIcon
|
||||
aria-label={title}
|
||||
data-test-subj={`clone-role-action-${role.name}`}
|
||||
title={title}
|
||||
color={'primary'}
|
||||
iconType={'copy'}
|
||||
{...reactRouterNavigate(
|
||||
this.props.history,
|
||||
getRoleManagementHref('clone', role.name)
|
||||
)}
|
||||
/>
|
||||
<EuiToolTip content={title}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={label}
|
||||
color={'primary'}
|
||||
data-test-subj={`clone-role-action-${role.name}`}
|
||||
disabled={this.state.selection.length >= 1}
|
||||
iconType={'copy'}
|
||||
{...reactRouterNavigate(
|
||||
this.props.history,
|
||||
getRoleManagementHref('clone', role.name)
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
available: (role: Role) => !role.metadata || !role.metadata._reserved,
|
||||
render: (role: Role) => {
|
||||
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 (
|
||||
<EuiToolTip content={title}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={label}
|
||||
color={'danger'}
|
||||
data-test-subj={`delete-role-action-${role.name}`}
|
||||
disabled={this.state.selection.length >= 1}
|
||||
iconType={'trash'}
|
||||
onClick={() => this.deleteOneRole(role)}
|
||||
>
|
||||
{title}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<EuiToolTip content={title}>
|
||||
<EuiButtonEmpty
|
||||
aria-label={label}
|
||||
color={'primary'}
|
||||
data-test-subj={`edit-role-action-${role.name}`}
|
||||
disabled={this.state.selection.length >= 1}
|
||||
iconType={'pencil'}
|
||||
{...reactRouterNavigate(
|
||||
this.props.history,
|
||||
getRoleManagementHref('edit', role.name)
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -337,6 +394,13 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
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<Props, State> {
|
|||
</EuiButton>
|
||||
);
|
||||
}
|
||||
|
||||
private renderToolsRight() {
|
||||
return (
|
||||
<EuiSwitch
|
||||
|
@ -401,6 +466,7 @@ export class RolesGridPage extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
private onCancelDelete = () => {
|
||||
this.setState({ showDeleteConfirmation: false });
|
||||
this.setState({ showDeleteConfirmation: false, selection: [] });
|
||||
this.tableRef.current?.setSelection([]);
|
||||
};
|
||||
}
|
||||
|
|
37
x-pack/plugins/security/public/management/table_utils.tsx
Normal file
37
x-pack/plugins/security/public/management/table_utils.tsx
Normal file
|
@ -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<ActionsEuiTableFormattingProps>(
|
||||
({ children }) => (
|
||||
<div
|
||||
css={css`
|
||||
.euiTableRowCell--hasActions .euiButtonEmpty .euiButtonContent {
|
||||
padding: 0px 0px;
|
||||
.euiButtonEmpty__text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
|
@ -19857,7 +19857,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": "これらのロールを削除しようとしています:",
|
||||
|
@ -19868,7 +19867,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": "利用可能なインデックスパターンのリストへのアクセス権が必要です。",
|
||||
|
|
|
@ -20152,7 +20152,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": "您即将删除以下角色:",
|
||||
|
@ -20163,7 +20162,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": "您需要访问可用索引模式列表的权限。",
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue