Update text and icons to align with Cloud (#86394)

* Update text and icons to align with Cloud

* Update test to reflect new page title prefix

* Change links conditionally

* Simplify profile link logic

* Add setAsProfile prop for overriding default link

* Address feedback

* remove translations since message has changed

* Tidying up

* Add unit tests.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
This commit is contained in:
Ryan Keairns 2021-03-11 08:53:46 -06:00 committed by GitHub
parent 40b648cd06
commit f61657c5e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 191 additions and 48 deletions

View file

@ -16,11 +16,12 @@ export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] =>
if (resetPasswordUrl) {
userMenuLinks.push({
label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', {
defaultMessage: 'Cloud profile',
defaultMessage: 'Profile',
}),
iconType: 'logoCloud',
iconType: 'user',
href: resetPasswordUrl,
order: 100,
setAsProfile: true,
});
}

View file

@ -62,7 +62,7 @@ describe('<AccountManagementPage>', () => {
});
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(
user.full_name
`Settings for ${user.full_name}`
);
expect(wrapper.find('[data-test-subj="username"]').text()).toEqual(user.username);
expect(wrapper.find('[data-test-subj="email"]').text()).toEqual(user.email);
@ -83,7 +83,9 @@ describe('<AccountManagementPage>', () => {
wrapper.update();
});
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(user.username);
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(
`Settings for ${user.username}`
);
});
it(`displays a placeholder when no email address is provided`, async () => {

View file

@ -9,6 +9,7 @@ import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { CoreStart, NotificationsStart } from 'src/core/public';
@ -40,7 +41,13 @@ export const AccountManagementPage = ({ userAPIClient, authc, notifications }: P
<EuiPageBody restrictWidth>
<EuiPanel>
<EuiText data-test-subj={'userDisplayName'}>
<h1>{getUserDisplayName(currentUser)}</h1>
<h1>
<FormattedMessage
id="xpack.security.account.pageTitle"
defaultMessage="Settings for {strongUsername}"
values={{ strongUsername: <strong>{getUserDisplayName(currentUser)}</strong> }}
/>
</h1>
</EuiText>
<EuiSpacer size="xl" />

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiHeaderSectionItemButton, EuiPopover } from '@elastic/eui';
import { EuiContextMenuItem, EuiHeaderSectionItemButton, EuiPopover } from '@elastic/eui';
import React from 'react';
import { BehaviorSubject } from 'rxjs';
@ -181,4 +181,58 @@ describe('SecurityNavControl', () => {
expect(findTestSubject(wrapper, 'logoutLink').text()).toBe('Log in');
});
it('properly renders without a custom profile link.', async () => {
const props = {
user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })),
editProfileUrl: '',
logoutUrl: '',
userMenuLinks$: new BehaviorSubject([
{ label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 },
{ label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2 },
]),
};
const wrapper = mountWithIntl(<SecurityNavControl {...props} />);
await nextTick();
wrapper.update();
expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([]);
wrapper.find(EuiHeaderSectionItemButton).simulate('click');
expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([
'Profile',
'link1',
'link2',
'Log out',
]);
});
it('properly renders with a custom profile link.', async () => {
const props = {
user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })),
editProfileUrl: '',
logoutUrl: '',
userMenuLinks$: new BehaviorSubject([
{ label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 },
{ label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2, setAsProfile: true },
]),
};
const wrapper = mountWithIntl(<SecurityNavControl {...props} />);
await nextTick();
wrapper.update();
expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([]);
wrapper.find(EuiHeaderSectionItemButton).simulate('click');
expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([
'link1',
'link2',
'Preferences',
'Log out',
]);
});
});

View file

@ -30,6 +30,7 @@ export interface UserMenuLink {
iconType: IconType;
href: string;
order?: number;
setAsProfile?: boolean;
}
interface Props {
@ -123,21 +124,6 @@ export class SecurityNavControl extends Component<Props, State> {
const isAnonymousUser = authenticatedUser?.authentication_provider.type === 'anonymous';
const items: EuiContextMenuPanelItemDescriptor[] = [];
if (!isAnonymousUser) {
const profileMenuItem = {
name: (
<FormattedMessage
id="xpack.security.navControlComponent.editProfileLinkText"
defaultMessage="Profile"
/>
),
icon: <EuiIcon type="user" size="m" />,
href: editProfileUrl,
'data-test-subj': 'profileLink',
};
items.push(profileMenuItem);
}
if (userMenuLinks.length) {
const userMenuLinkMenuItems = userMenuLinks
.sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB)
@ -147,11 +133,30 @@ export class SecurityNavControl extends Component<Props, State> {
href,
'data-test-subj': `userMenuLink__${label}`,
}));
items.push(...userMenuLinkMenuItems);
}
items.push(...userMenuLinkMenuItems, {
isSeparator: true,
key: 'securityNavControlComponent__userMenuLinksSeparator',
});
if (!isAnonymousUser) {
const hasCustomProfileLinks = userMenuLinks.some(({ setAsProfile }) => setAsProfile === true);
const profileMenuItem = {
name: (
<FormattedMessage
id="xpack.security.navControlComponent.editProfileLinkText"
defaultMessage="{profileOverridden, select, true{Preferences} other{Profile}}"
values={{ profileOverridden: hasCustomProfileLinks }}
/>
),
icon: <EuiIcon type={hasCustomProfileLinks ? 'controlsHorizontal' : 'user'} size="m" />,
href: editProfileUrl,
'data-test-subj': 'profileLink',
};
// Set this as the first link if there is no user-defined profile link
if (!hasCustomProfileLinks) {
items.unshift(profileMenuItem);
} else {
items.push(profileMenuItem);
}
}
const logoutMenuItem = {

View file

@ -178,16 +178,19 @@ describe('SecurityNavControlService', () => {
});
describe(`#start`, () => {
it('should return functions to register and retrieve user menu links', () => {
const license$ = new BehaviorSubject<ILicense>(validLicense);
let navControlService: SecurityNavControlService;
beforeEach(() => {
const license$ = new BehaviorSubject<ILicense>({} as ILicense);
const navControlService = new SecurityNavControlService();
navControlService = new SecurityNavControlService();
navControlService.setup({
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
authc: securityMock.createSetup().authc,
logoutUrl: '/some/logout/url',
});
});
it('should return functions to register and retrieve user menu links', () => {
const coreStart = coreMock.createStart();
const navControlServiceStart = navControlService.start({ core: coreStart });
expect(navControlServiceStart).toHaveProperty('getUserMenuLinks$');
@ -195,15 +198,6 @@ describe('SecurityNavControlService', () => {
});
it('should register custom user menu links to be displayed in the nav controls', (done) => {
const license$ = new BehaviorSubject<ILicense>(validLicense);
const navControlService = new SecurityNavControlService();
navControlService.setup({
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
authc: securityMock.createSetup().authc,
logoutUrl: '/some/logout/url',
});
const coreStart = coreMock.createStart();
const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart });
const userMenuLinks$ = getUserMenuLinks$();
@ -231,15 +225,6 @@ describe('SecurityNavControlService', () => {
});
it('should retrieve user menu links sorted by order', (done) => {
const license$ = new BehaviorSubject<ILicense>(validLicense);
const navControlService = new SecurityNavControlService();
navControlService.setup({
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
authc: securityMock.createSetup().authc,
logoutUrl: '/some/logout/url',
});
const coreStart = coreMock.createStart();
const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart });
const userMenuLinks$ = getUserMenuLinks$();
@ -305,5 +290,79 @@ describe('SecurityNavControlService', () => {
done();
});
});
it('should allow adding a custom profile link', () => {
const coreStart = coreMock.createStart();
const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart });
const userMenuLinks$ = getUserMenuLinks$();
addUserMenuLinks([
{ label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3 },
{ label: 'link1', href: 'path-to-link1', iconType: 'empty', order: 1, setAsProfile: true },
]);
const onUserMenuLinksHandler = jest.fn();
userMenuLinks$.subscribe(onUserMenuLinksHandler);
expect(onUserMenuLinksHandler).toHaveBeenCalledTimes(1);
expect(onUserMenuLinksHandler).toHaveBeenCalledWith([
{ label: 'link1', href: 'path-to-link1', iconType: 'empty', order: 1, setAsProfile: true },
{ label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3 },
]);
});
it('should not allow adding more than one custom profile link', () => {
const coreStart = coreMock.createStart();
const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart });
const userMenuLinks$ = getUserMenuLinks$();
expect(() => {
addUserMenuLinks([
{
label: 'link3',
href: 'path-to-link3',
iconType: 'empty',
order: 3,
setAsProfile: true,
},
{
label: 'link1',
href: 'path-to-link1',
iconType: 'empty',
order: 1,
setAsProfile: true,
},
]);
}).toThrowErrorMatchingInlineSnapshot(
`"Only one custom profile link can be passed at a time (found 2)"`
);
// Adding a single custom profile link.
addUserMenuLinks([
{ label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3, setAsProfile: true },
]);
expect(() => {
addUserMenuLinks([
{
label: 'link1',
href: 'path-to-link1',
iconType: 'empty',
order: 1,
setAsProfile: true,
},
]);
}).toThrowErrorMatchingInlineSnapshot(
`"Only one custom profile link can be set. A custom profile link named link3 (path-to-link3) already exists"`
);
const onUserMenuLinksHandler = jest.fn();
userMenuLinks$.subscribe(onUserMenuLinksHandler);
expect(onUserMenuLinksHandler).toHaveBeenCalledTimes(1);
expect(onUserMenuLinksHandler).toHaveBeenCalledWith([
{ label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3, setAsProfile: true },
]);
});
});
});

View file

@ -77,6 +77,23 @@ export class SecurityNavControlService {
this.userMenuLinks$.pipe(map(this.sortUserMenuLinks), takeUntil(this.stop$)),
addUserMenuLinks: (userMenuLinks: UserMenuLink[]) => {
const currentLinks = this.userMenuLinks$.value;
const hasCustomProfileLink = currentLinks.find(({ setAsProfile }) => setAsProfile === true);
const passedCustomProfileLinkCount = userMenuLinks.filter(
({ setAsProfile }) => setAsProfile === true
).length;
if (hasCustomProfileLink && passedCustomProfileLinkCount > 0) {
throw new Error(
`Only one custom profile link can be set. A custom profile link named ${hasCustomProfileLink.label} (${hasCustomProfileLink.href}) already exists`
);
}
if (passedCustomProfileLinkCount > 1) {
throw new Error(
`Only one custom profile link can be passed at a time (found ${passedCustomProfileLinkCount})`
);
}
const newLinks = [...currentLinks, ...userMenuLinks];
this.userMenuLinks$.next(newLinks);
},

View file

@ -17946,7 +17946,6 @@
"xpack.security.management.users.usersTitle": "ユーザー",
"xpack.security.management.usersTitle": "ユーザー",
"xpack.security.navControlComponent.accountMenuAriaLabel": "アカウントメニュー",
"xpack.security.navControlComponent.editProfileLinkText": "プロフィール",
"xpack.security.navControlComponent.loginLinkText": "ログイン",
"xpack.security.navControlComponent.logoutLinkText": "ログアウト",
"xpack.security.overwrittenSession.continueAsUserText": "{username} として続行",

View file

@ -18196,7 +18196,6 @@
"xpack.security.management.users.usersTitle": "用户",
"xpack.security.management.usersTitle": "用户",
"xpack.security.navControlComponent.accountMenuAriaLabel": "帐户菜单",
"xpack.security.navControlComponent.editProfileLinkText": "配置文件",
"xpack.security.navControlComponent.loginLinkText": "登录",
"xpack.security.navControlComponent.logoutLinkText": "注销",
"xpack.security.overwrittenSession.continueAsUserText": "作为 {username} 继续",