Display user roles on user profile page (#161375)

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

## Summary

Adds a new section in the profile bar that highlights the assigned roles
of the current user.

The roles are rendered using the EUI Badges and if there are more roles
than one, then the section only renders the first one while the rest are
rendered in a Popover.

## Old UI
<img width="1088" alt="image"
src="2d28353a-bd0a-47fd-b0b8-c3e09fd4d6b0">


## New UI
#### For a user with a single role:
<img width="1058" alt="image"
src="ec101a0c-0776-4484-8dec-dab43dca5732">


#### For a user with more than one role
Show helper text and a button that opens a popover on click
<img width="311" alt="image"
src="d9834072-69d1-414a-ac75-fdbde60195c3">
#### Popover for users with long role names
Setting the badge group max width to 200px (arbitrary for now) allows
truncation of the role badges
<img width="270" alt="image"
src="ffac590c-e6e6-49dd-96ae-0288b7496714">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sid 2023-07-12 14:59:09 +02:00 committed by GitHub
parent 3827dde90a
commit 19f00ec3d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 168 additions and 10 deletions

View file

@ -395,4 +395,66 @@ describe('useUserProfileForm', () => {
});
});
});
describe('User roles section', () => {
it('should display the user roles', () => {
const data: UserProfileData = {};
const nonCloudUser = mockAuthenticatedUser({ elastic_cloud_user: false });
coreStart.settings.client.get.mockReturnValue(false);
coreStart.settings.client.isOverridden.mockReturnValue(true);
const testWrapper = mount(
<Providers
services={coreStart}
theme$={theme$}
history={history}
authc={authc}
securityApiClients={{
userProfiles: new UserProfileAPIClient(coreStart.http),
users: new UserAPIClient(coreStart.http),
}}
>
<UserProfile user={nonCloudUser} data={data} />
</Providers>
);
expect(testWrapper.exists('span[data-test-subj="userRoles"]')).toBeTruthy();
expect(testWrapper.exists('EuiButtonEmpty[data-test-subj="userRolesExpand"]')).toBeFalsy();
expect(testWrapper.exists('EuiBadgeGroup[data-test-subj="remainingRoles"]')).toBeFalsy();
});
it('should display a popover for users with more than one role', () => {
const data: UserProfileData = {};
const nonCloudUser = mockAuthenticatedUser({ elastic_cloud_user: false });
coreStart.settings.client.get.mockReturnValue(false);
coreStart.settings.client.isOverridden.mockReturnValue(true);
nonCloudUser.roles = [...nonCloudUser.roles, 'user-role-1', 'user-role-2'];
const testWrapper = mount(
<Providers
services={coreStart}
theme$={theme$}
history={history}
authc={authc}
securityApiClients={{
userProfiles: new UserProfileAPIClient(coreStart.http),
users: new UserAPIClient(coreStart.http),
}}
>
<UserProfile user={nonCloudUser} data={data} />
</Providers>
);
const extraRoles = nonCloudUser.roles.splice(1);
const userRolesExpandButton = testWrapper.find(
'EuiButtonEmpty[data-test-subj="userRolesExpand"]'
);
expect(userRolesExpandButton).toBeTruthy();
expect(userRolesExpandButton.text()).toEqual(`+${extraRoles.length} more`);
});
});
});

View file

@ -6,6 +6,8 @@
*/
import {
EuiBadge,
EuiBadgeGroup,
EuiButton,
EuiButtonEmpty,
EuiButtonGroup,
@ -21,6 +23,7 @@ import {
EuiKeyPadMenu,
EuiKeyPadMenuItem,
EuiPageTemplate_Deprecated as EuiPageTemplate,
EuiPopover,
EuiSpacer,
EuiText,
useEuiTheme,
@ -67,6 +70,20 @@ export interface UserProfileProps {
};
}
export interface UserDetailsEditorProps {
user: AuthenticatedUser;
}
export interface UserSettingsEditorProps {
formik: ReturnType<typeof useUserProfileForm>;
isThemeOverridden: boolean;
isOverriddenThemeDarkMode: boolean;
}
export interface UserRoleProps {
user: AuthenticatedUser;
}
export interface UserProfileFormValues {
user: {
full_name: string;
@ -85,7 +102,7 @@ export interface UserProfileFormValues {
avatarType: 'initials' | 'image';
}
function UserDetailsEditor({ user }: { user: AuthenticatedUser }) {
const UserDetailsEditor: FunctionComponent<UserDetailsEditorProps> = ({ user }) => {
const { services } = useKibana<CoreStart>();
const canChangeDetails = canUserChangeDetails(user, services.application.capabilities);
@ -142,17 +159,13 @@ function UserDetailsEditor({ user }: { user: AuthenticatedUser }) {
</FormRow>
</EuiDescribedFormGroup>
);
}
};
function UserSettingsEditor({
const UserSettingsEditor: FunctionComponent<UserSettingsEditorProps> = ({
formik,
isThemeOverridden,
isOverriddenThemeDarkMode,
}: {
formik: ReturnType<typeof useUserProfileForm>;
isThemeOverridden: boolean;
isOverriddenThemeDarkMode: boolean;
}) {
}) => {
if (!formik.values.data) {
return null;
}
@ -262,7 +275,7 @@ function UserSettingsEditor({
</FormRow>
</EuiDescribedFormGroup>
);
}
};
function UserAvatarEditor({
user,
@ -557,6 +570,68 @@ function UserPasswordEditor({
);
}
const UserRoles: FunctionComponent<UserRoleProps> = ({ user }) => {
const { euiTheme } = useEuiTheme();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen);
const closePopover = () => setIsPopoverOpen(false);
const [firstRole] = user.roles;
const remainingRoles = user.roles.slice(1);
const renderMoreRoles = () => {
const button = (
<EuiButtonEmpty size="xs" onClick={onButtonClick} data-test-subj="userRolesExpand">
<FormattedMessage
id="xpack.security.accountManagement.userProfile.rolesCountLabel"
defaultMessage="+{count} more"
values={{ count: remainingRoles.length }}
/>
</EuiButtonEmpty>
);
return (
<EuiPopover
panelPaddingSize="s"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
data-test-subj="userRolesPopover"
>
<EuiBadgeGroup
gutterSize="xs"
data-test-subj="remainingRoles"
style={{
maxWidth: '200px',
}}
>
{remainingRoles.map((role) => (
<EuiBadge color="hollow" key={role}>
{role}
</EuiBadge>
))}
</EuiBadgeGroup>
</EuiPopover>
);
};
return (
<>
<div
style={{
maxWidth: euiTheme.breakpoint.m / 6,
display: 'inline-block',
}}
>
<EuiBadge key={firstRole} color="hollow" data-test-subj={`role${firstRole}`}>
{firstRole}
</EuiBadge>
</div>
{remainingRoles.length ? renderMoreRoles() : null}
</>
);
};
export const UserProfile: FunctionComponent<UserProfileProps> = ({ user, data }) => {
const { euiTheme } = useEuiTheme();
const { services } = useKibana<CoreStart>();
@ -581,7 +656,7 @@ export const UserProfile: FunctionComponent<UserProfileProps> = ({ user, data })
defaultMessage="Username"
/>
),
description: user.username as string | undefined,
description: user.username as string | undefined | JSX.Element,
helpText: (
<FormattedMessage
id="xpack.security.accountManagement.userProfile.usernameHelpText"
@ -628,6 +703,27 @@ export const UserProfile: FunctionComponent<UserProfileProps> = ({ user, data })
});
}
rightSideItems.push({
title: (
<FormattedMessage
id="xpack.security.accountManagement.userProfile.rolesLabel"
defaultMessage="{roles, plural,
one {Role}
other {Roles}
}"
values={{ roles: user.roles.length }}
/>
),
description: <UserRoles user={user} />,
helpText: (
<FormattedMessage
id="xpack.security.accountManagement.userProfile.rolesHelpText"
defaultMessage="Roles control access and permissions across the Elastic Stack."
/>
),
testSubj: 'userRoles',
});
return (
<>
<FormikProvider value={formik}>