mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
3827dde90a
commit
19f00ec3d7
2 changed files with 168 additions and 10 deletions
|
@ -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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue