Refactor Roles Grid page from class component to functional component (#186278)

Closes https://github.com/elastic/kibana/issues/186388

## Summary 

This PR covers some of the pre-work required to modernize role
management in Kibana. It convert the older Class component to a
functional component. It also breaks up the EUI in-memory table into
it's component parts of EUI Search Bar, Filters and EUI Basic table.

### Checklist

- [x] Since there's no change to the functionality, tests are expected
to continue passing

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sid 2024-06-21 10:22:28 +02:00 committed by GitHub
parent 32e5360afc
commit 9ed2ad9faf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 488 additions and 415 deletions

View file

@ -44,6 +44,7 @@ const waitForRender = async (
describe('<RolesGridPage />', () => {
let apiClientMock: jest.Mocked<PublicMethodsOf<RolesAPIClient>>;
let history: ReturnType<typeof scopedHistoryMock.create>;
const { theme, i18n, analytics, notifications } = coreMock.createStart();
beforeEach(() => {
history = scopedHistoryMock.create();
@ -87,7 +88,11 @@ describe('<RolesGridPage />', () => {
<RolesGridPage
rolesAPIClient={apiClientMock}
history={history}
notifications={coreMock.createStart().notifications}
notifications={notifications}
i18n={i18n}
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -105,7 +110,11 @@ describe('<RolesGridPage />', () => {
<RolesGridPage
rolesAPIClient={apiClientMock}
history={history}
notifications={coreMock.createStart().notifications}
notifications={notifications}
i18n={i18n}
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -125,7 +134,11 @@ describe('<RolesGridPage />', () => {
<RolesGridPage
rolesAPIClient={apiClientMock}
history={history}
notifications={coreMock.createStart().notifications}
notifications={notifications}
i18n={i18n}
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
/>
);
await waitForRender(wrapper, (updatedWrapper) => {
@ -139,7 +152,11 @@ describe('<RolesGridPage />', () => {
<RolesGridPage
rolesAPIClient={apiClientMock}
history={history}
notifications={coreMock.createStart().notifications}
notifications={notifications}
i18n={i18n}
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -179,7 +196,11 @@ describe('<RolesGridPage />', () => {
<RolesGridPage
rolesAPIClient={apiClientMock}
history={history}
notifications={coreMock.createStart().notifications}
notifications={notifications}
i18n={i18n}
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
/>
);
const initialIconCount = wrapper.find(EuiIcon).length;
@ -189,50 +210,93 @@ describe('<RolesGridPage />', () => {
});
expect(wrapper.find(EuiBasicTable).props().items).toEqual([
{
name: 'disabled-role',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
transient_metadata: { enabled: false },
},
{
name: 'reserved-role',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
metadata: { _reserved: true },
},
{
name: 'special%chars%role',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
},
{
name: 'test-role-1',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
base: [],
spaces: [],
feature: {},
},
],
},
{
name: 'test-role-with-description',
description: 'role-description',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
base: [],
spaces: [],
feature: {},
},
],
},
{
name: 'reserved-role',
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
base: [],
spaces: [],
feature: {},
},
],
metadata: {
_reserved: true,
},
},
{
name: 'disabled-role',
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
base: [],
spaces: [],
feature: {},
},
],
transient_metadata: {
enabled: false,
},
},
{
name: 'special%chars%role',
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
base: [],
spaces: [],
feature: {},
},
],
},
]);
findTestSubject(wrapper, 'showReservedRolesSwitch').simulate('click');
expect(wrapper.find(EuiBasicTable).props().items).toEqual([
{
name: 'disabled-role',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
transient_metadata: { enabled: false },
},
{
name: 'special%chars%role',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
},
{
name: 'test-role-1',
elasticsearch: { cluster: [], indices: [], run_as: [] },
@ -244,6 +308,17 @@ describe('<RolesGridPage />', () => {
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
},
{
name: 'disabled-role',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
transient_metadata: { enabled: false },
},
{
name: 'special%chars%role',
elasticsearch: { cluster: [], indices: [], run_as: [] },
kibana: [{ base: [], spaces: [], feature: {} }],
},
]);
});
@ -252,7 +327,11 @@ describe('<RolesGridPage />', () => {
<RolesGridPage
rolesAPIClient={apiClientMock}
history={history}
notifications={coreMock.createStart().notifications}
notifications={notifications}
i18n={i18n}
buildFlavor={'traditional'}
analytics={analytics}
theme={theme}
readOnly
/>
);

View file

@ -5,28 +5,35 @@
* 2.0.
*/
import type { EuiBasicTableColumn, EuiSwitchEvent } from '@elastic/eui';
import type {
CriteriaWithPagination,
EuiBasicTableColumn,
EuiSwitchEvent,
Query,
} from '@elastic/eui';
import {
EuiBasicTable,
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
EuiPageHeader,
EuiSearchBar,
EuiSpacer,
EuiSwitch,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import _ from 'lodash';
import React, { Component } from 'react';
import React, { useEffect, useState } from 'react';
import type { FC } from 'react';
import type { BuildFlavor } from '@kbn/config';
import type { NotificationsStart, ScopedHistory } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { ConfirmDelete } from './confirm_delete';
@ -52,341 +59,90 @@ export interface Props extends StartServices {
cloudOrgUrl?: string;
}
interface State {
roles: Role[];
visibleRoles: Role[];
selection: Role[];
filter: string;
showDeleteConfirmation: boolean;
permissionDenied: boolean;
includeReservedRoles: boolean;
isLoading: boolean;
interface RolesTableState {
query: Query;
from: number;
size: number;
}
const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => {
return `/${action}${roleName ? `/${encodeURIComponent(roleName)}` : ''}`;
};
export class RolesGridPage extends Component<Props, State> {
static defaultProps: Partial<Props> = {
readOnly: false,
};
constructor(props: Props) {
super(props);
this.state = {
roles: [],
visibleRoles: [],
selection: [],
filter: '',
showDeleteConfirmation: false,
permissionDenied: false,
includeReservedRoles: true,
isLoading: false,
};
}
public componentDidMount() {
this.loadRoles();
}
public render() {
const { permissionDenied } = this.state;
return permissionDenied ? <PermissionDenied /> : this.getPageContent();
}
private getPageContent = () => {
const { isLoading } = this.state;
const customRolesEnabled = this.props.buildFlavor === 'serverless';
const rolesTitle = customRolesEnabled ? (
<FormattedMessage
id="xpack.security.management.roles.customRoleTitle"
defaultMessage="Custom Roles"
/>
) : (
<FormattedMessage id="xpack.security.management.roles.roleTitle" defaultMessage="Roles" />
);
const rolesDescription = customRolesEnabled ? (
<FormattedMessage
id="xpack.security.management.roles.customRolesSubtitle"
defaultMessage="In addition to the predefined roles on the system, you can create your own roles and provide your users with the exact set of privileges that they need."
/>
) : (
<FormattedMessage
id="xpack.security.management.roles.subtitle"
defaultMessage="Apply roles to groups of users and manage permissions across the stack."
/>
);
const emptyResultsMessage = customRolesEnabled ? (
<FormattedMessage
id="xpack.security.management.roles.noCustomRolesFound"
defaultMessage="No custom roles to show"
/>
) : (
<FormattedMessage
id="xpack.security.management.roles.noRolesFound"
defaultMessage="No items found"
/>
);
const pageRightSideItems = [
<EuiButton
data-test-subj="createRoleButton"
{...reactRouterNavigate(this.props.history, getRoleManagementHref('edit'))}
fill
iconType="plusInCircleFilled"
>
<FormattedMessage
id="xpack.security.management.roles.createRoleButtonLabel"
defaultMessage="Create role"
/>
</EuiButton>,
];
if (customRolesEnabled) {
pageRightSideItems.push(
<EuiButtonEmpty
href={this.props.cloudOrgUrl}
target="_blank"
iconSide="right"
iconType="popout"
>
<FormattedMessage
id="xpack.security.management.roles.assignRolesLinkLabel"
defaultMessage="Assign roles"
/>
</EuiButtonEmpty>
);
}
const getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: boolean) => {
return roles.filter((role) => {
const normalized = `${role.name}`.toLowerCase();
const normalizedQuery = filter.toLowerCase();
return (
<>
<EuiPageHeader
bottomBorder
data-test-subj="rolesGridPageHeader"
pageTitle={rolesTitle}
description={rolesDescription}
rightSideItems={this.props.readOnly ? undefined : pageRightSideItems}
/>
<EuiSpacer size="l" />
{this.state.showDeleteConfirmation ? (
<ConfirmDelete
onCancel={this.onCancelDelete}
rolesToDelete={this.state.selection.map((role) => role.name)}
callback={this.handleDelete}
cloudOrgUrl={this.props.cloudOrgUrl}
{...this.props}
/>
) : null}
<EuiInMemoryTable
data-test-subj="rolesTable"
itemId="name"
columns={this.getColumnConfig()}
selection={
this.props.readOnly
? undefined
: {
selectable: (role: Role) => !role.metadata || !role.metadata._reserved,
selectableMessage: (selectable: boolean) =>
!selectable ? 'Role is reserved' : '',
onSelectionChange: (selection: Role[]) => this.setState({ selection }),
selected: this.state.selection,
}
}
pagination={{
initialPageSize: 20,
pageSizeOptions: [10, 20, 30, 50, 100],
}}
message={emptyResultsMessage}
items={this.state.visibleRoles}
loading={isLoading}
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={{ 'data-test-subj': 'roleRow' }}
/>
</>
normalized.indexOf(normalizedQuery) !== -1 && (includeReservedRoles || !isRoleReserved(role))
);
};
});
};
private getColumnConfig = () => {
const config: Array<EuiBasicTableColumn<Role>> = [
{
field: 'name',
name: i18n.translate('xpack.security.management.roles.nameColumnName', {
defaultMessage: 'Role',
}),
sortable: true,
render: (name: string) => {
return (
<EuiText color="subdued" size="s">
<EuiLink
data-test-subj="roleRowName"
{...reactRouterNavigate(this.props.history, getRoleManagementHref('edit', name))}
>
{name}
</EuiLink>
</EuiText>
);
},
},
{
field: 'description',
name: i18n.translate('xpack.security.management.roles.descriptionColumnName', {
defaultMessage: 'Role Description',
}),
sortable: true,
truncateText: { lines: 3 },
render: (description: string, record: Role) => {
return (
<EuiToolTip position="top" content={description} display="block">
<EuiText
color="subdued"
size="s"
data-test-subj={`roleRowDescription-${record.name}`}
>
{description}
</EuiText>
</EuiToolTip>
);
},
},
];
if (this.props.buildFlavor !== 'serverless') {
config.push({
field: 'metadata',
name: i18n.translate('xpack.security.management.roles.statusColumnName', {
defaultMessage: 'Status',
}),
sortable: (role: Role) => isRoleEnabled(role) && !isRoleDeprecated(role),
render: (_metadata: Role['metadata'], record: Role) => {
return this.getRoleStatusBadges(record);
},
});
const DEFAULT_TABLE_STATE = {
query: EuiSearchBar.Query.MATCH_ALL,
sort: {
field: 'creation' as const,
direction: 'desc' as const,
},
from: 0,
size: 25,
filters: {},
};
export const RolesGridPage: FC<Props> = ({
notifications,
rolesAPIClient,
history,
readOnly,
buildFlavor,
cloudOrgUrl,
analytics,
theme,
i18n: i18nStart,
}) => {
const [roles, setRoles] = useState<Role[]>([]);
const [visibleRoles, setVisibleRoles] = useState<Role[]>([]);
const [selection, setSelection] = useState<Role[]>([]);
const [filter, setFilter] = useState<string>('');
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
const [permissionDenied, setPermissionDenied] = useState<boolean>(false);
const [includeReservedRoles, setIncludeReservedRoles] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [tableState, setTableState] = useState<RolesTableState>(DEFAULT_TABLE_STATE);
useEffect(() => {
loadRoles();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const loadRoles = async () => {
try {
setIsLoading(true);
const rolesFromApi = await rolesAPIClient.getRoles();
setRoles(rolesFromApi);
setVisibleRoles(getVisibleRoles(rolesFromApi, filter, includeReservedRoles));
} catch (e) {
if (_.get(e, 'body.statusCode') === 403) {
setPermissionDenied(true);
} else {
notifications.toasts.addDanger(
i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', {
defaultMessage: 'Error fetching roles: {message}',
values: { message: _.get(e, 'body.message', '') },
})
);
}
} finally {
setIsLoading(false);
}
if (!this.props.readOnly) {
config.push({
name: i18n.translate('xpack.security.management.roles.actionsColumnName', {
defaultMessage: 'Actions',
}),
width: '150px',
actions: [
{
type: 'icon',
icon: 'copy',
isPrimary: true,
available: (role: Role) => !isRoleReserved(role),
name: i18n.translate('xpack.security.management.roles.cloneRoleActionName', {
defaultMessage: 'Clone',
}),
description: (role: Role) =>
i18n.translate('xpack.security.management.roles.cloneRoleActionLabel', {
defaultMessage: 'Clone {roleName}',
values: { roleName: role.name },
}),
href: (role: Role) =>
reactRouterNavigate(this.props.history, getRoleManagementHref('clone', role.name))
.href,
onClick: (role: Role, event: React.MouseEvent) =>
reactRouterNavigate(
this.props.history,
getRoleManagementHref('clone', role.name)
).onClick(event),
'data-test-subj': (role: Role) => `clone-role-action-${role.name}`,
},
{
type: 'icon',
icon: 'trash',
color: 'danger',
name: i18n.translate('xpack.security.management.roles.deleteRoleActionName', {
defaultMessage: 'Delete',
}),
description: (role: Role) =>
i18n.translate('xpack.security.management.roles.deleteRoleActionLabel', {
defaultMessage: `Delete {roleName}`,
values: { roleName: role.name },
}),
'data-test-subj': (role: Role) => `delete-role-action-${role.name}`,
onClick: (role: Role) => this.deleteOneRole(role),
available: (role: Role) => !role.metadata || !role.metadata._reserved,
},
{
isPrimary: true,
type: 'icon',
icon: 'pencil',
name: i18n.translate('xpack.security.management.roles.editRoleActionName', {
defaultMessage: 'Edit',
}),
description: (role: Role) =>
i18n.translate('xpack.security.management.roles.editRoleActionLabel', {
defaultMessage: `Edit {roleName}`,
values: { roleName: role.name },
}),
'data-test-subj': (role: Role) => `edit-role-action-${role.name}`,
href: (role: Role) =>
reactRouterNavigate(this.props.history, getRoleManagementHref('edit', role.name))
.href,
onClick: (role: Role, event: React.MouseEvent) =>
reactRouterNavigate(
this.props.history,
getRoleManagementHref('edit', role.name)
).onClick(event),
available: (role: Role) => !isRoleReadOnly(role),
enabled: () => this.state.selection.length === 0,
},
],
});
}
return config;
};
private getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: boolean) => {
return roles.filter((role) => {
const normalized = `${role.name}`.toLowerCase();
const normalizedQuery = filter.toLowerCase();
return (
normalized.indexOf(normalizedQuery) !== -1 &&
(includeReservedRoles || !isRoleReserved(role))
);
});
const onIncludeReservedRolesChange = (e: EuiSwitchEvent) => {
setIncludeReservedRoles(e.target.checked);
setVisibleRoles(getVisibleRoles(roles, filter, e.target.checked));
};
private onIncludeReservedRolesChange = (e: EuiSwitchEvent) => {
this.setState({
includeReservedRoles: e.target.checked,
visibleRoles: this.getVisibleRoles(this.state.roles, this.state.filter, e.target.checked),
});
};
private getRoleStatusBadges = (role: Role) => {
const getRoleStatusBadges = (role: Role) => {
const enabled = isRoleEnabled(role);
const deprecated = isRoleDeprecated(role);
const reserved = isRoleReserved(role);
@ -428,52 +184,18 @@ export class RolesGridPage extends Component<Props, State> {
);
};
private handleDelete = () => {
this.setState({
selection: [],
showDeleteConfirmation: false,
});
this.loadRoles();
const handleDelete = () => {
setSelection([]);
setShowDeleteConfirmation(false);
loadRoles();
};
private deleteOneRole = (roleToDelete: Role) => {
this.setState({
selection: [roleToDelete],
showDeleteConfirmation: true,
});
const deleteOneRole = (roleToDelete: Role) => {
setSelection([roleToDelete]);
setShowDeleteConfirmation(true);
};
private async loadRoles() {
try {
this.setState({ isLoading: true });
const roles = await this.props.rolesAPIClient.getRoles();
this.setState({
roles,
visibleRoles: this.getVisibleRoles(
roles,
this.state.filter,
this.state.includeReservedRoles
),
});
} catch (e) {
if (_.get(e, 'body.statusCode') === 403) {
this.setState({ permissionDenied: true });
} else {
this.props.notifications.toasts.addDanger(
i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', {
defaultMessage: 'Error fetching roles: {message}',
values: { message: _.get(e, 'body.message', '') },
})
);
}
} finally {
this.setState({ isLoading: false });
}
}
private renderToolsLeft() {
const { selection } = this.state;
const renderToolsLeft = () => {
if (selection.length === 0) {
return;
}
@ -482,7 +204,7 @@ export class RolesGridPage extends Component<Props, State> {
<EuiButton
data-test-subj="deleteRoleButton"
color="danger"
onClick={() => this.setState({ showDeleteConfirmation: true })}
onClick={() => setShowDeleteConfirmation(true)}
>
<FormattedMessage
id="xpack.security.management.roles.deleteSelectedRolesButtonLabel"
@ -493,10 +215,10 @@ export class RolesGridPage extends Component<Props, State> {
/>
</EuiButton>
);
}
};
private renderToolsRight() {
if (this.props.buildFlavor !== 'serverless') {
const renderToolsRight = () => {
if (buildFlavor !== 'serverless') {
return (
<EuiSwitch
data-test-subj="showReservedRolesSwitch"
@ -506,13 +228,285 @@ export class RolesGridPage extends Component<Props, State> {
defaultMessage="Show reserved roles"
/>
}
checked={this.state.includeReservedRoles}
onChange={this.onIncludeReservedRolesChange}
checked={includeReservedRoles}
onChange={onIncludeReservedRolesChange}
/>
);
}
}
private onCancelDelete = () => {
this.setState({ showDeleteConfirmation: false });
};
}
const onTableChange = ({ page, sort }: CriteriaWithPagination<Role>) => {
const newState = {
...tableState,
from: page?.index! * page?.size!,
size: page?.size!,
};
setTableState(newState);
};
const getColumnConfig = (): Array<EuiBasicTableColumn<Role>> => {
const config: Array<EuiBasicTableColumn<Role>> = [
{
field: 'name',
name: i18n.translate('xpack.security.management.roles.nameColumnName', {
defaultMessage: 'Role',
}),
sortable: true,
render: (name: string) => (
<EuiText color="subdued" size="s">
<EuiLink
data-test-subj="roleRowName"
{...reactRouterNavigate(history, getRoleManagementHref('edit', name))}
>
{name}
</EuiLink>
</EuiText>
),
},
{
field: 'description',
name: i18n.translate('xpack.security.management.roles.descriptionColumnName', {
defaultMessage: 'Role Description',
}),
sortable: true,
truncateText: { lines: 3 },
render: (description: string, record: Role) => (
<EuiToolTip position="top" content={description} display="block">
<EuiText color="subdued" size="s" data-test-subj={`roleRowDescription-${record.name}`}>
{description}
</EuiText>
</EuiToolTip>
),
},
];
if (buildFlavor !== 'serverless') {
config.push({
field: 'metadata',
name: i18n.translate('xpack.security.management.roles.statusColumnName', {
defaultMessage: 'Status',
}),
sortable: (role: Role) => isRoleEnabled(role) && !isRoleDeprecated(role),
render: (_metadata: Role['metadata'], record: Role) => getRoleStatusBadges(record),
});
}
if (!readOnly) {
config.push({
name: i18n.translate('xpack.security.management.roles.actionsColumnName', {
defaultMessage: 'Actions',
}),
width: '150px',
actions: [
{
type: 'icon',
icon: 'copy',
isPrimary: true,
available: (role: Role) => !isRoleReserved(role),
name: i18n.translate('xpack.security.management.roles.cloneRoleActionName', {
defaultMessage: 'Clone',
}),
description: (role: Role) =>
i18n.translate('xpack.security.management.roles.cloneRoleActionLabel', {
defaultMessage: 'Clone {roleName}',
values: { roleName: role.name },
}),
href: (role: Role) =>
reactRouterNavigate(history, getRoleManagementHref('clone', role.name)).href,
onClick: (role: Role, event: React.MouseEvent) =>
reactRouterNavigate(history, getRoleManagementHref('clone', role.name)).onClick(
event
),
'data-test-subj': (role: Role) => `clone-role-action-${role.name}`,
},
{
type: 'icon',
icon: 'trash',
color: 'danger',
name: i18n.translate('xpack.security.management.roles.deleteRoleActionName', {
defaultMessage: 'Delete',
}),
description: (role: Role) =>
i18n.translate('xpack.security.management.roles.deleteRoleActionLabel', {
defaultMessage: `Delete {roleName}`,
values: { roleName: role.name },
}),
'data-test-subj': (role: Role) => `delete-role-action-${role.name}`,
onClick: (role: Role) => deleteOneRole(role),
available: (role: Role) => !role.metadata || !role.metadata._reserved,
},
{
isPrimary: true,
type: 'icon',
icon: 'pencil',
name: i18n.translate('xpack.security.management.roles.editRoleActionName', {
defaultMessage: 'Edit',
}),
description: (role: Role) =>
i18n.translate('xpack.security.management.roles.editRoleActionLabel', {
defaultMessage: `Edit {roleName}`,
values: { roleName: role.name },
}),
'data-test-subj': (role: Role) => `edit-role-action-${role.name}`,
href: (role: Role) =>
reactRouterNavigate(history, getRoleManagementHref('edit', role.name)).href,
onClick: (role: Role, event: React.MouseEvent) =>
reactRouterNavigate(history, getRoleManagementHref('edit', role.name)).onClick(event),
available: (role: Role) => !isRoleReadOnly(role),
enabled: () => selection.length === 0,
},
],
});
}
return config;
};
const onCancelDelete = () => {
setShowDeleteConfirmation(false);
};
const pagination = {
pageIndex: tableState.from / tableState.size,
pageSize: tableState.size,
totalItemCount: visibleRoles.length,
pageSizeOptions: [25, 50, 100],
};
return permissionDenied ? (
<PermissionDenied />
) : (
<>
<KibanaPageTemplate.Header
bottomBorder
data-test-subj="rolesGridPageHeader"
pageTitle={
buildFlavor === 'serverless' ? (
<FormattedMessage
id="xpack.security.management.roles.customRoleTitle"
defaultMessage="Custom Roles"
/>
) : (
<FormattedMessage
id="xpack.security.management.roles.roleTitle"
defaultMessage="Roles"
/>
)
}
description={
buildFlavor === 'serverless' ? (
<FormattedMessage
id="xpack.security.management.roles.customRolesSubtitle"
defaultMessage="In addition to the predefined roles on the system, you can create your own roles and provide your users with the exact set of privileges that they need."
/>
) : (
<FormattedMessage
id="xpack.security.management.roles.subtitle"
defaultMessage="Apply roles to groups of users and manage permissions across the stack."
/>
)
}
rightSideItems={
readOnly
? undefined
: [
<EuiButton
data-test-subj="createRoleButton"
{...reactRouterNavigate(history, getRoleManagementHref('edit'))}
fill
iconType="plusInCircleFilled"
>
<FormattedMessage
id="xpack.security.management.roles.createRoleButtonLabel"
defaultMessage="Create role"
/>
</EuiButton>,
buildFlavor === 'serverless' && (
<EuiButtonEmpty
href={cloudOrgUrl}
target="_blank"
iconSide="right"
iconType="popout"
>
<FormattedMessage
id="xpack.security.management.roles.assignRolesLinkLabel"
defaultMessage="Assign roles"
/>
</EuiButtonEmpty>
),
]
}
/>
<EuiSpacer size="l" />
<KibanaPageTemplate.Section paddingSize="none">
{showDeleteConfirmation ? (
<ConfirmDelete
onCancel={onCancelDelete}
rolesToDelete={selection.map((role) => role.name)}
callback={handleDelete}
cloudOrgUrl={cloudOrgUrl}
notifications={notifications}
rolesAPIClient={rolesAPIClient}
buildFlavor={buildFlavor}
theme={theme}
analytics={analytics}
i18n={i18nStart}
/>
) : null}
<EuiSearchBar
box={{
incremental: true,
'data-test-subj': 'searchRoles',
}}
onChange={(query: Record<string, any>) => {
setFilter(query.queryText);
setVisibleRoles(getVisibleRoles(roles, query.queryText, includeReservedRoles));
}}
toolsLeft={renderToolsLeft()}
toolsRight={renderToolsRight()}
/>
<EuiSpacer size="s" />
<EuiBasicTable
data-test-subj="rolesTable"
itemId="name"
columns={getColumnConfig()}
selection={
readOnly
? undefined
: {
selectable: (role: Role) => !role.metadata || !role.metadata._reserved,
selectableMessage: (selectable: boolean) =>
!selectable ? 'Role is reserved' : '',
onSelectionChange: (value: Role[]) => setSelection(value),
selected: selection,
}
}
onChange={onTableChange}
pagination={pagination}
noItemsMessage={
buildFlavor === 'serverless' ? (
<FormattedMessage
id="xpack.security.management.roles.noCustomRolesFound"
defaultMessage="No custom roles to show"
/>
) : (
<FormattedMessage
id="xpack.security.management.roles.noRolesFound"
defaultMessage="No items found"
/>
)
}
items={visibleRoles}
loading={isLoading}
sorting={{
sort: {
field: 'name',
direction: 'asc',
},
}}
rowProps={{ 'data-test-subj': 'roleRow' }}
/>
</KibanaPageTemplate.Section>
</>
);
};