Skip to content

Commit

Permalink
Adds cloud links to user menu (#82803)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryan Keairns <contactryank@gmail.com>
  • Loading branch information
cqliu1 and ryankeairns committed Nov 10, 2020
1 parent 00ca555 commit 4dba10c
Show file tree
Hide file tree
Showing 17 changed files with 424 additions and 61 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/cloud/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "cloud"],
"optionalPlugins": ["usageCollection", "home"],
"optionalPlugins": ["usageCollection", "home", "security"],
"server": true,
"ui": true
}
2 changes: 1 addition & 1 deletion x-pack/plugins/cloud/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { PluginInitializerContext } from '../../../../src/core/public';
import { CloudPlugin } from './plugin';

export { CloudSetup } from './plugin';
export { CloudSetup, CloudConfigType } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new CloudPlugin(initializerContext);
}
18 changes: 18 additions & 0 deletions x-pack/plugins/cloud/public/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

function createSetupMock() {
return {
cloudId: 'mock-cloud-id',
isCloudEnabled: true,
resetPasswordUrl: 'reset-password-url',
accountUrl: 'account-url',
};
}

export const cloudMock = {
createSetup: createSetupMock,
};
28 changes: 22 additions & 6 deletions x-pack/plugins/cloud/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,63 @@

import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import { i18n } from '@kbn/i18n';
import { SecurityPluginStart } from '../../security/public';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
import { ELASTIC_SUPPORT_LINK } from '../common/constants';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { createUserMenuLinks } from './user_menu_links';

interface CloudConfigType {
export interface CloudConfigType {
id?: string;
resetPasswordUrl?: string;
deploymentUrl?: string;
accountUrl?: string;
}

interface CloudSetupDependencies {
home?: HomePublicPluginSetup;
}

interface CloudStartDependencies {
security?: SecurityPluginStart;
}

export interface CloudSetup {
cloudId?: string;
cloudDeploymentUrl?: string;
isCloudEnabled: boolean;
resetPasswordUrl?: string;
accountUrl?: string;
}

export class CloudPlugin implements Plugin<CloudSetup> {
private config!: CloudConfigType;
private isCloudEnabled: boolean;

constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get<CloudConfigType>();
this.isCloudEnabled = false;
}

public async setup(core: CoreSetup, { home }: CloudSetupDependencies) {
const { id, resetPasswordUrl, deploymentUrl } = this.config;
const isCloudEnabled = getIsCloudEnabled(id);
this.isCloudEnabled = getIsCloudEnabled(id);

if (home) {
home.environment.update({ cloud: isCloudEnabled });
if (isCloudEnabled) {
home.environment.update({ cloud: this.isCloudEnabled });
if (this.isCloudEnabled) {
home.tutorials.setVariable('cloud', { id, resetPasswordUrl });
}
}

return {
cloudId: id,
cloudDeploymentUrl: deploymentUrl,
isCloudEnabled,
isCloudEnabled: this.isCloudEnabled,
};
}

public start(coreStart: CoreStart) {
public start(coreStart: CoreStart, { security }: CloudStartDependencies) {
const { deploymentUrl } = this.config;
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
if (deploymentUrl) {
Expand All @@ -63,5 +74,10 @@ export class CloudPlugin implements Plugin<CloudSetup> {
href: deploymentUrl,
});
}

if (security && this.isCloudEnabled) {
const userMenuLinks = createUserMenuLinks(this.config);
security.navControlService.addUserMenuLinks(userMenuLinks);
}
}
}
38 changes: 38 additions & 0 deletions x-pack/plugins/cloud/public/user_menu_links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';
import { UserMenuLink } from '../../security/public';
import { CloudConfigType } from '.';

export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => {
const { resetPasswordUrl, accountUrl } = config;
const userMenuLinks = [] as UserMenuLink[];

if (resetPasswordUrl) {
userMenuLinks.push({
label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', {
defaultMessage: 'Cloud profile',
}),
iconType: 'logoCloud',
href: resetPasswordUrl,
order: 100,
});
}

if (accountUrl) {
userMenuLinks.push({
label: i18n.translate('xpack.cloud.userMenuLinks.accountLinkText', {
defaultMessage: 'Account & Billing',
}),
iconType: 'gear',
href: accountUrl,
order: 200,
});
}

return userMenuLinks;
};
2 changes: 2 additions & 0 deletions x-pack/plugins/cloud/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const configSchema = schema.object({
apm: schema.maybe(apmConfigSchema),
resetPasswordUrl: schema.maybe(schema.string()),
deploymentUrl: schema.maybe(schema.string()),
accountUrl: schema.maybe(schema.string()),
});

export type CloudConfigType = TypeOf<typeof configSchema>;
Expand All @@ -32,6 +33,7 @@ export const config: PluginConfigDescriptor<CloudConfigType> = {
id: true,
resetPasswordUrl: true,
deploymentUrl: true,
accountUrl: true,
},
schema: configSchema,
};
1 change: 1 addition & 0 deletions x-pack/plugins/security/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
export { SecurityPluginSetup, SecurityPluginStart };
export { AuthenticatedUser } from '../common/model';
export { SecurityLicense, SecurityLicenseFeatures } from '../common/licensing';
export { UserMenuLink } from '../public/nav_control';

export const plugin: PluginInitializer<
SecurityPluginSetup,
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/security/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { authenticationMock } from './authentication/index.mock';
import { createSessionTimeoutMock } from './session/session_timeout.mock';
import { licenseMock } from '../common/licensing/index.mock';
import { navControlServiceMock } from './nav_control/index.mock';

function createSetupMock() {
return {
Expand All @@ -15,7 +16,13 @@ function createSetupMock() {
license: licenseMock.create(),
};
}
function createStartMock() {
return {
navControlService: navControlServiceMock.createStart(),
};
}

export const securityMock = {
createSetup: createSetupMock,
createStart: createStartMock,
};
14 changes: 14 additions & 0 deletions x-pack/plugins/security/public/nav_control/index.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { SecurityNavControlServiceStart } from '.';

export const navControlServiceMock = {
createStart: (): jest.Mocked<SecurityNavControlServiceStart> => ({
getUserMenuLinks$: jest.fn(),
addUserMenuLinks: jest.fn(),
}),
};
3 changes: 2 additions & 1 deletion x-pack/plugins/security/public/nav_control/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/

export { SecurityNavControlService } from './nav_control_service';
export { SecurityNavControlService, SecurityNavControlServiceStart } from './nav_control_service';
export { UserMenuLink } from './nav_control_component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.chrNavControl__userMenu {
.euiContextMenuPanelTitle {
// Uppercased by default, override to match actual username
text-transform: none;
}

.euiContextMenuItem {
// Temp fix for EUI issue https://github.com/elastic/eui/issues/3092
line-height: normal;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import React from 'react';
import { BehaviorSubject } from 'rxjs';
import { shallowWithIntl, nextTick, mountWithIntl } from 'test_utils/enzyme_helpers';
import { SecurityNavControl } from './nav_control_component';
import { AuthenticatedUser } from '../../common/model';
Expand All @@ -17,6 +18,7 @@ describe('SecurityNavControl', () => {
user: new Promise(() => {}) as Promise<AuthenticatedUser>,
editProfileUrl: '',
logoutUrl: '',
userMenuLinks$: new BehaviorSubject([]),
};

const wrapper = shallowWithIntl(<SecurityNavControl {...props} />);
Expand All @@ -42,6 +44,7 @@ describe('SecurityNavControl', () => {
user: Promise.resolve({ full_name: 'foo' }) as Promise<AuthenticatedUser>,
editProfileUrl: '',
logoutUrl: '',
userMenuLinks$: new BehaviorSubject([]),
};

const wrapper = shallowWithIntl(<SecurityNavControl {...props} />);
Expand Down Expand Up @@ -70,6 +73,7 @@ describe('SecurityNavControl', () => {
user: Promise.resolve({ full_name: 'foo' }) as Promise<AuthenticatedUser>,
editProfileUrl: '',
logoutUrl: '',
userMenuLinks$: new BehaviorSubject([]),
};

const wrapper = mountWithIntl(<SecurityNavControl {...props} />);
Expand All @@ -91,6 +95,7 @@ describe('SecurityNavControl', () => {
user: Promise.resolve({ full_name: 'foo' }) as Promise<AuthenticatedUser>,
editProfileUrl: '',
logoutUrl: '',
userMenuLinks$: new BehaviorSubject([]),
};

const wrapper = mountWithIntl(<SecurityNavControl {...props} />);
Expand All @@ -107,4 +112,37 @@ describe('SecurityNavControl', () => {
expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(1);
expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1);
});

it('renders a popover with additional user menu links registered by other plugins', async () => {
const props = {
user: Promise.resolve({ full_name: 'foo' }) as Promise<AuthenticatedUser>,
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 },
{ label: 'link3', href: 'path-to-link-3', iconType: 'empty', order: 3 },
]),
};

const wrapper = mountWithIntl(<SecurityNavControl {...props} />);
await nextTick();
wrapper.update();

expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(0);
expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(0);
expect(findTestSubject(wrapper, 'userMenuLink__link1')).toHaveLength(0);
expect(findTestSubject(wrapper, 'userMenuLink__link2')).toHaveLength(0);
expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(0);
expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(0);

wrapper.find(EuiHeaderSectionItemButton).simulate('click');

expect(findTestSubject(wrapper, 'userMenu')).toHaveLength(1);
expect(findTestSubject(wrapper, 'profileLink')).toHaveLength(1);
expect(findTestSubject(wrapper, 'userMenuLink__link1')).toHaveLength(1);
expect(findTestSubject(wrapper, 'userMenuLink__link2')).toHaveLength(1);
expect(findTestSubject(wrapper, 'userMenuLink__link3')).toHaveLength(1);
expect(findTestSubject(wrapper, 'logoutLink')).toHaveLength(1);
});
});
Loading

0 comments on commit 4dba10c

Please sign in to comment.