Skip to content

Commit f61657c

Browse files
ryankeairnskibanamachineazasypkin
authored
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 <[email protected]> Co-authored-by: Aleh Zasypkin <[email protected]>
1 parent 40b648c commit f61657c

File tree

9 files changed

+190
-47
lines changed

9 files changed

+190
-47
lines changed

x-pack/plugins/cloud/public/user_menu_links.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] =>
1616
if (resetPasswordUrl) {
1717
userMenuLinks.push({
1818
label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', {
19-
defaultMessage: 'Cloud profile',
19+
defaultMessage: 'Profile',
2020
}),
21-
iconType: 'logoCloud',
21+
iconType: 'user',
2222
href: resetPasswordUrl,
2323
order: 100,
24+
setAsProfile: true,
2425
});
2526
}
2627

x-pack/plugins/security/public/account_management/account_management_page.test.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe('<AccountManagementPage>', () => {
6262
});
6363

6464
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(
65-
user.full_name
65+
`Settings for ${user.full_name}`
6666
);
6767
expect(wrapper.find('[data-test-subj="username"]').text()).toEqual(user.username);
6868
expect(wrapper.find('[data-test-subj="email"]').text()).toEqual(user.email);
@@ -83,7 +83,9 @@ describe('<AccountManagementPage>', () => {
8383
wrapper.update();
8484
});
8585

86-
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(user.username);
86+
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(
87+
`Settings for ${user.username}`
88+
);
8789
});
8890

8991
it(`displays a placeholder when no email address is provided`, async () => {

x-pack/plugins/security/public/account_management/account_management_page.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui
99
import React, { useEffect, useState } from 'react';
1010
import ReactDOM from 'react-dom';
1111

12+
import { FormattedMessage } from '@kbn/i18n/react';
1213
import type { PublicMethodsOf } from '@kbn/utility-types';
1314
import type { CoreStart, NotificationsStart } from 'src/core/public';
1415

@@ -40,7 +41,13 @@ export const AccountManagementPage = ({ userAPIClient, authc, notifications }: P
4041
<EuiPageBody restrictWidth>
4142
<EuiPanel>
4243
<EuiText data-test-subj={'userDisplayName'}>
43-
<h1>{getUserDisplayName(currentUser)}</h1>
44+
<h1>
45+
<FormattedMessage
46+
id="xpack.security.account.pageTitle"
47+
defaultMessage="Settings for {strongUsername}"
48+
values={{ strongUsername: <strong>{getUserDisplayName(currentUser)}</strong> }}
49+
/>
50+
</h1>
4451
</EuiText>
4552

4653
<EuiSpacer size="xl" />

x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx

+55-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77

8-
import { EuiHeaderSectionItemButton, EuiPopover } from '@elastic/eui';
8+
import { EuiContextMenuItem, EuiHeaderSectionItemButton, EuiPopover } from '@elastic/eui';
99
import React from 'react';
1010
import { BehaviorSubject } from 'rxjs';
1111

@@ -181,4 +181,58 @@ describe('SecurityNavControl', () => {
181181

182182
expect(findTestSubject(wrapper, 'logoutLink').text()).toBe('Log in');
183183
});
184+
185+
it('properly renders without a custom profile link.', async () => {
186+
const props = {
187+
user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })),
188+
editProfileUrl: '',
189+
logoutUrl: '',
190+
userMenuLinks$: new BehaviorSubject([
191+
{ label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 },
192+
{ label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2 },
193+
]),
194+
};
195+
196+
const wrapper = mountWithIntl(<SecurityNavControl {...props} />);
197+
await nextTick();
198+
wrapper.update();
199+
200+
expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([]);
201+
202+
wrapper.find(EuiHeaderSectionItemButton).simulate('click');
203+
204+
expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([
205+
'Profile',
206+
'link1',
207+
'link2',
208+
'Log out',
209+
]);
210+
});
211+
212+
it('properly renders with a custom profile link.', async () => {
213+
const props = {
214+
user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })),
215+
editProfileUrl: '',
216+
logoutUrl: '',
217+
userMenuLinks$: new BehaviorSubject([
218+
{ label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 },
219+
{ label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2, setAsProfile: true },
220+
]),
221+
};
222+
223+
const wrapper = mountWithIntl(<SecurityNavControl {...props} />);
224+
await nextTick();
225+
wrapper.update();
226+
227+
expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([]);
228+
229+
wrapper.find(EuiHeaderSectionItemButton).simulate('click');
230+
231+
expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([
232+
'link1',
233+
'link2',
234+
'Preferences',
235+
'Log out',
236+
]);
237+
});
184238
});

x-pack/plugins/security/public/nav_control/nav_control_component.tsx

+23-18
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface UserMenuLink {
3030
iconType: IconType;
3131
href: string;
3232
order?: number;
33+
setAsProfile?: boolean;
3334
}
3435

3536
interface Props {
@@ -123,35 +124,39 @@ export class SecurityNavControl extends Component<Props, State> {
123124
const isAnonymousUser = authenticatedUser?.authentication_provider.type === 'anonymous';
124125
const items: EuiContextMenuPanelItemDescriptor[] = [];
125126

127+
if (userMenuLinks.length) {
128+
const userMenuLinkMenuItems = userMenuLinks
129+
.sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB)
130+
.map(({ label, iconType, href }: UserMenuLink) => ({
131+
name: <EuiText>{label}</EuiText>,
132+
icon: <EuiIcon type={iconType} size="m" />,
133+
href,
134+
'data-test-subj': `userMenuLink__${label}`,
135+
}));
136+
items.push(...userMenuLinkMenuItems);
137+
}
138+
126139
if (!isAnonymousUser) {
140+
const hasCustomProfileLinks = userMenuLinks.some(({ setAsProfile }) => setAsProfile === true);
127141
const profileMenuItem = {
128142
name: (
129143
<FormattedMessage
130144
id="xpack.security.navControlComponent.editProfileLinkText"
131-
defaultMessage="Profile"
145+
defaultMessage="{profileOverridden, select, true{Preferences} other{Profile}}"
146+
values={{ profileOverridden: hasCustomProfileLinks }}
132147
/>
133148
),
134-
icon: <EuiIcon type="user" size="m" />,
149+
icon: <EuiIcon type={hasCustomProfileLinks ? 'controlsHorizontal' : 'user'} size="m" />,
135150
href: editProfileUrl,
136151
'data-test-subj': 'profileLink',
137152
};
138-
items.push(profileMenuItem);
139-
}
140153

141-
if (userMenuLinks.length) {
142-
const userMenuLinkMenuItems = userMenuLinks
143-
.sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB)
144-
.map(({ label, iconType, href }: UserMenuLink) => ({
145-
name: <EuiText>{label}</EuiText>,
146-
icon: <EuiIcon type={iconType} size="m" />,
147-
href,
148-
'data-test-subj': `userMenuLink__${label}`,
149-
}));
150-
151-
items.push(...userMenuLinkMenuItems, {
152-
isSeparator: true,
153-
key: 'securityNavControlComponent__userMenuLinksSeparator',
154-
});
154+
// Set this as the first link if there is no user-defined profile link
155+
if (!hasCustomProfileLinks) {
156+
items.unshift(profileMenuItem);
157+
} else {
158+
items.push(profileMenuItem);
159+
}
155160
}
156161

157162
const logoutMenuItem = {

x-pack/plugins/security/public/nav_control/nav_control_service.test.ts

+80-21
Original file line numberDiff line numberDiff line change
@@ -178,32 +178,26 @@ describe('SecurityNavControlService', () => {
178178
});
179179

180180
describe(`#start`, () => {
181-
it('should return functions to register and retrieve user menu links', () => {
182-
const license$ = new BehaviorSubject<ILicense>(validLicense);
181+
let navControlService: SecurityNavControlService;
182+
beforeEach(() => {
183+
const license$ = new BehaviorSubject<ILicense>({} as ILicense);
183184

184-
const navControlService = new SecurityNavControlService();
185+
navControlService = new SecurityNavControlService();
185186
navControlService.setup({
186187
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
187188
authc: securityMock.createSetup().authc,
188189
logoutUrl: '/some/logout/url',
189190
});
191+
});
190192

193+
it('should return functions to register and retrieve user menu links', () => {
191194
const coreStart = coreMock.createStart();
192195
const navControlServiceStart = navControlService.start({ core: coreStart });
193196
expect(navControlServiceStart).toHaveProperty('getUserMenuLinks$');
194197
expect(navControlServiceStart).toHaveProperty('addUserMenuLinks');
195198
});
196199

197200
it('should register custom user menu links to be displayed in the nav controls', (done) => {
198-
const license$ = new BehaviorSubject<ILicense>(validLicense);
199-
200-
const navControlService = new SecurityNavControlService();
201-
navControlService.setup({
202-
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
203-
authc: securityMock.createSetup().authc,
204-
logoutUrl: '/some/logout/url',
205-
});
206-
207201
const coreStart = coreMock.createStart();
208202
const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart });
209203
const userMenuLinks$ = getUserMenuLinks$();
@@ -231,15 +225,6 @@ describe('SecurityNavControlService', () => {
231225
});
232226

233227
it('should retrieve user menu links sorted by order', (done) => {
234-
const license$ = new BehaviorSubject<ILicense>(validLicense);
235-
236-
const navControlService = new SecurityNavControlService();
237-
navControlService.setup({
238-
securityLicense: new SecurityLicenseService().setup({ license$ }).license,
239-
authc: securityMock.createSetup().authc,
240-
logoutUrl: '/some/logout/url',
241-
});
242-
243228
const coreStart = coreMock.createStart();
244229
const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart });
245230
const userMenuLinks$ = getUserMenuLinks$();
@@ -305,5 +290,79 @@ describe('SecurityNavControlService', () => {
305290
done();
306291
});
307292
});
293+
294+
it('should allow adding a custom profile link', () => {
295+
const coreStart = coreMock.createStart();
296+
const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart });
297+
const userMenuLinks$ = getUserMenuLinks$();
298+
299+
addUserMenuLinks([
300+
{ label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3 },
301+
{ label: 'link1', href: 'path-to-link1', iconType: 'empty', order: 1, setAsProfile: true },
302+
]);
303+
304+
const onUserMenuLinksHandler = jest.fn();
305+
userMenuLinks$.subscribe(onUserMenuLinksHandler);
306+
307+
expect(onUserMenuLinksHandler).toHaveBeenCalledTimes(1);
308+
expect(onUserMenuLinksHandler).toHaveBeenCalledWith([
309+
{ label: 'link1', href: 'path-to-link1', iconType: 'empty', order: 1, setAsProfile: true },
310+
{ label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3 },
311+
]);
312+
});
313+
314+
it('should not allow adding more than one custom profile link', () => {
315+
const coreStart = coreMock.createStart();
316+
const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart });
317+
const userMenuLinks$ = getUserMenuLinks$();
318+
319+
expect(() => {
320+
addUserMenuLinks([
321+
{
322+
label: 'link3',
323+
href: 'path-to-link3',
324+
iconType: 'empty',
325+
order: 3,
326+
setAsProfile: true,
327+
},
328+
{
329+
label: 'link1',
330+
href: 'path-to-link1',
331+
iconType: 'empty',
332+
order: 1,
333+
setAsProfile: true,
334+
},
335+
]);
336+
}).toThrowErrorMatchingInlineSnapshot(
337+
`"Only one custom profile link can be passed at a time (found 2)"`
338+
);
339+
340+
// Adding a single custom profile link.
341+
addUserMenuLinks([
342+
{ label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3, setAsProfile: true },
343+
]);
344+
345+
expect(() => {
346+
addUserMenuLinks([
347+
{
348+
label: 'link1',
349+
href: 'path-to-link1',
350+
iconType: 'empty',
351+
order: 1,
352+
setAsProfile: true,
353+
},
354+
]);
355+
}).toThrowErrorMatchingInlineSnapshot(
356+
`"Only one custom profile link can be set. A custom profile link named link3 (path-to-link3) already exists"`
357+
);
358+
359+
const onUserMenuLinksHandler = jest.fn();
360+
userMenuLinks$.subscribe(onUserMenuLinksHandler);
361+
362+
expect(onUserMenuLinksHandler).toHaveBeenCalledTimes(1);
363+
expect(onUserMenuLinksHandler).toHaveBeenCalledWith([
364+
{ label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3, setAsProfile: true },
365+
]);
366+
});
308367
});
309368
});

x-pack/plugins/security/public/nav_control/nav_control_service.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ export class SecurityNavControlService {
7777
this.userMenuLinks$.pipe(map(this.sortUserMenuLinks), takeUntil(this.stop$)),
7878
addUserMenuLinks: (userMenuLinks: UserMenuLink[]) => {
7979
const currentLinks = this.userMenuLinks$.value;
80+
const hasCustomProfileLink = currentLinks.find(({ setAsProfile }) => setAsProfile === true);
81+
const passedCustomProfileLinkCount = userMenuLinks.filter(
82+
({ setAsProfile }) => setAsProfile === true
83+
).length;
84+
85+
if (hasCustomProfileLink && passedCustomProfileLinkCount > 0) {
86+
throw new Error(
87+
`Only one custom profile link can be set. A custom profile link named ${hasCustomProfileLink.label} (${hasCustomProfileLink.href}) already exists`
88+
);
89+
}
90+
91+
if (passedCustomProfileLinkCount > 1) {
92+
throw new Error(
93+
`Only one custom profile link can be passed at a time (found ${passedCustomProfileLinkCount})`
94+
);
95+
}
96+
8097
const newLinks = [...currentLinks, ...userMenuLinks];
8198
this.userMenuLinks$.next(newLinks);
8299
},

x-pack/plugins/translations/translations/ja-JP.json

-1
Original file line numberDiff line numberDiff line change
@@ -17946,7 +17946,6 @@
1794617946
"xpack.security.management.users.usersTitle": "ユーザー",
1794717947
"xpack.security.management.usersTitle": "ユーザー",
1794817948
"xpack.security.navControlComponent.accountMenuAriaLabel": "アカウントメニュー",
17949-
"xpack.security.navControlComponent.editProfileLinkText": "プロフィール",
1795017949
"xpack.security.navControlComponent.loginLinkText": "ログイン",
1795117950
"xpack.security.navControlComponent.logoutLinkText": "ログアウト",
1795217951
"xpack.security.overwrittenSession.continueAsUserText": "{username} として続行",

x-pack/plugins/translations/translations/zh-CN.json

-1
Original file line numberDiff line numberDiff line change
@@ -18196,7 +18196,6 @@
1819618196
"xpack.security.management.users.usersTitle": "用户",
1819718197
"xpack.security.management.usersTitle": "用户",
1819818198
"xpack.security.navControlComponent.accountMenuAriaLabel": "帐户菜单",
18199-
"xpack.security.navControlComponent.editProfileLinkText": "配置文件",
1820018199
"xpack.security.navControlComponent.loginLinkText": "登录",
1820118200
"xpack.security.navControlComponent.logoutLinkText": "注销",
1820218201
"xpack.security.overwrittenSession.continueAsUserText": "作为 {username} 继续",

0 commit comments

Comments
 (0)