mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
32e5360afc
commit
9ed2ad9faf
2 changed files with 488 additions and 415 deletions
|
@ -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
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue