Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update text and icons to align with Cloud #86394

Merged
merged 14 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions x-pack/plugins/cloud/public/user_menu_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface UserMenuLink {
iconType: IconType;
href: string;
order?: number;
setAsProfile?: boolean;
}

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

if (userMenuLinks.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I see we moved profile link to the bottom now, do we really want to always display Profile link after any custom user links? I thought it should be the first by default and pushed down by the custom links only if their order is lower (assuming we set some default order for default profile link) or what is the plan?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great observation. The most immediate need is alignment with Cloud which means placing their link atop the list and the location of 'Preferences' (after custom links) likewise aligns neatly with their current order.

This is an admittedly simplistic approach, to start - yet it meets the objective - and I would like to see further capabilities such as an order added, subsequently. I personally don't feel comfortable adding that logic, so would you be ok with tracking this separately and handling it post-merge/as the need arises?

Copy link
Contributor Author

@ryankeairns ryankeairns Feb 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait! I see that the cloud links are defining an order, but its only being used to sort within custom links.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this latest round of updates, I decided to go with simply adding the default profile link in either the first (if no other custom links are set as profile) or last position. The last position being coupled with changing the label to Preferences and changing the icon.

We could get fancier here and so something like set an order value on the default profile link, but I'm not sure if/when we'd need that, thus the simpler approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could get fancier here and so something like set an order value on the default profile link, but I'm not sure if/when we'd need that, thus the simpler approach.

Agree, makes sense to me 👍

const userMenuLinkMenuItems = userMenuLinks
.sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB)
.map(({ label, iconType, href }: UserMenuLink) => ({
name: <EuiText>{label}</EuiText>,
icon: <EuiIcon type={iconType} size="m" />,
href,
'data-test-subj': `userMenuLink__${label}`,
}));
items.push(...userMenuLinkMenuItems);
}

if (!isAnonymousUser) {
const hasCustomProfileLinks = userMenuLinks.some(({ setAsProfile }) => setAsProfile === true);
const profileMenuItem = {
name: (
<FormattedMessage
id="xpack.security.navControlComponent.editProfileLinkText"
defaultMessage="Profile"
defaultMessage="{profileOverridden, select, true{Preferences} other{Profile}}"
values={{ profileOverridden: hasCustomProfileLinks }}
/>
),
icon: <EuiIcon type="user" size="m" />,
icon: <EuiIcon type={hasCustomProfileLinks ? 'controlsHorizontal' : '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)
.map(({ label, iconType, href }: UserMenuLink) => ({
name: <EuiText>{label}</EuiText>,
icon: <EuiIcon type={iconType} size="m" />,
href,
'data-test-subj': `userMenuLink__${label}`,
}));

items.push(...userMenuLinkMenuItems, {
isSeparator: true,
key: 'securityNavControlComponent__userMenuLinksSeparator',
});
// Set this as the first link if there is no user-defined profile link
if (!hasCustomProfileLinks) {
items.unshift(profileMenuItem);
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: would you mind adding a simple unit test for this new behavior?

items.push(profileMenuItem);
}
}

const logoutMenuItem = {
Expand Down
101 changes: 80 additions & 21 deletions x-pack/plugins/security/public/nav_control/nav_control_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,32 +178,26 @@ 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$');
expect(navControlServiceStart).toHaveProperty('addUserMenuLinks');
});

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$();
Expand Down Expand Up @@ -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$();
Expand Down Expand Up @@ -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 },
]);
});
});
});
17 changes: 17 additions & 0 deletions x-pack/plugins/security/public/nav_control/nav_control_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: how do you feel about adding two simple unit tests for for these new if branches?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like that is a good idea, however I do not have experience writing them 😬

A few options:

  1. I can create a separate issue for adding them later (post-merge)
  2. Perhaps you can point me at a simple example
  3. I hate to ask, but if its small and you have any time, could I ask you to add them? (here or post-merge)

Thanks for your feedback. It has been very helpful!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries at all!

  1. Perhaps you can point me at a simple example
  2. I hate to ask, but if its small and you have any time, could I ask you to add them?

We have a couple of tests for nav_control_service.tsx here and for nav_control_component.tsx here. You can run them in a watch mode with node scripts/jest --watch x-pack/plugins/security/public/nav_control/.

But please don't waste too much of your time on this if you're stuck - just let me know and I'll push the tests to your branch my morning tomorrow and we'll merge it with a green CI. I'd prefer to keep the changes with the relevant tests in the same PR if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the information. If you don't mind pushing in the morning, I would be very grateful and can learn from what you add here. My afternoon is, unfortunately, eaten up by a dentist appoint or else I'd make an attempt myself :/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b3b8872

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);
},
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -17947,7 +17947,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} として続行",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -18197,7 +18197,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} 继续",
Expand Down