mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Cloud] Update support and user profile header menus (#160535)
This commit is contained in:
parent
a5d4d9d948
commit
dea3423b2f
34 changed files with 383 additions and 134 deletions
|
@ -15,7 +15,7 @@ import { EuiLink } from '@elastic/eui';
|
|||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { InternalInjectedMetadataStart } from '@kbn/core-injected-metadata-browser-internal';
|
||||
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import { type DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import { mountReactNode } from '@kbn/core-mount-utils-browser-internal';
|
||||
import type { NotificationsStart } from '@kbn/core-notifications-browser';
|
||||
|
@ -33,8 +33,11 @@ import type {
|
|||
ChromeSetProjectBreadcrumbsParams,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
|
||||
import type { SideNavComponent as ISideNavComponent } from '@kbn/core-chrome-browser';
|
||||
import { KIBANA_ASK_ELASTIC_LINK } from './constants';
|
||||
import type {
|
||||
SideNavComponent as ISideNavComponent,
|
||||
ChromeHelpMenuLink,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
|
||||
import { DocTitleService } from './doc_title';
|
||||
import { NavControlsService } from './nav_controls';
|
||||
import { NavLinksService } from './nav_links';
|
||||
|
@ -135,7 +138,7 @@ export class ChromeService {
|
|||
>(undefined);
|
||||
const badge$ = new BehaviorSubject<ChromeBadge | undefined>(undefined);
|
||||
const customNavLink$ = new BehaviorSubject<ChromeNavLink | undefined>(undefined);
|
||||
const helpSupportUrl$ = new BehaviorSubject<string>(KIBANA_ASK_ELASTIC_LINK);
|
||||
const helpSupportUrl$ = new BehaviorSubject<string>(docLinks.links.kibana.askElastic);
|
||||
const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true');
|
||||
const chromeStyle$ = new BehaviorSubject<ChromeStyle>('classic');
|
||||
|
||||
|
@ -168,6 +171,7 @@ export class ChromeService {
|
|||
const recentlyAccessed = await this.recentlyAccessed.start({ http });
|
||||
const docTitle = this.docTitle.start();
|
||||
const { customBranding$ } = customBranding;
|
||||
const helpMenuLinks$ = navControls.getHelpMenuLinks$();
|
||||
|
||||
// erase chrome fields from a previous app while switching to a next app
|
||||
application.currentAppId$.subscribe(() => {
|
||||
|
@ -301,12 +305,13 @@ export class ChromeService {
|
|||
breadcrumbs$={currentProjectBreadcrumbs$}
|
||||
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
|
||||
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
|
||||
helpMenuLinks$={helpMenuLinks$}
|
||||
navControlsLeft$={navControls.getLeft$()}
|
||||
navControlsCenter$={navControls.getCenter$()}
|
||||
navControlsRight$={navControls.getRight$()}
|
||||
loadingCount$={http.getLoadingCount$()}
|
||||
homeHref$={projectNavigation.getProjectHome$()}
|
||||
kibanaDocLink={docLinks.links.kibana.guide}
|
||||
docLinks={docLinks}
|
||||
kibanaVersion={injectedMetadata.getKibanaVersion()}
|
||||
prependBasePath={http.basePath.prepend}
|
||||
>
|
||||
|
@ -329,10 +334,12 @@ export class ChromeService {
|
|||
breadcrumbsAppendExtension$={breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$))}
|
||||
customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))}
|
||||
kibanaDocLink={docLinks.links.kibana.guide}
|
||||
docLinks={docLinks}
|
||||
forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()}
|
||||
globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$}
|
||||
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
|
||||
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
|
||||
helpMenuLinks$={helpMenuLinks$}
|
||||
homeHref={http.basePath.prepend('/app/home')}
|
||||
isVisible$={this.isVisible$}
|
||||
kibanaVersion={injectedMetadata.getKibanaVersion()}
|
||||
|
@ -399,6 +406,8 @@ export class ChromeService {
|
|||
|
||||
setHelpSupportUrl: (url: string) => helpSupportUrl$.next(url),
|
||||
|
||||
getHelpSupportUrl$: () => helpSupportUrl$.pipe(takeUntil(this.stop$)),
|
||||
|
||||
getIsNavDrawerLocked$: () => getIsNavDrawerLocked$,
|
||||
|
||||
getCustomNavLink$: () => customNavLink$.pipe(takeUntil(this.stop$)),
|
||||
|
@ -407,6 +416,10 @@ export class ChromeService {
|
|||
customNavLink$.next(customNavLink);
|
||||
},
|
||||
|
||||
setHelpMenuLinks: (helpMenuLinks: ChromeHelpMenuLink[]) => {
|
||||
navControls.setHelpMenuLinks(helpMenuLinks);
|
||||
},
|
||||
|
||||
setHeaderBanner: (headerBanner?: ChromeUserBanner) => {
|
||||
headerBanner$.next(headerBanner);
|
||||
},
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const KIBANA_FEEDBACK_LINK =
|
||||
'https://www.elastic.co/products/kibana/feedback?blade=kibanafeedback';
|
||||
export const KIBANA_ASK_ELASTIC_LINK =
|
||||
'https://www.elastic.co/products/kibana/ask-elastic?blade=kibanaaskelastic';
|
||||
export const GITHUB_CREATE_ISSUE_LINK = 'https://github.com/elastic/kibana/issues/new/choose';
|
|
@ -9,7 +9,11 @@
|
|||
import { sortBy } from 'lodash';
|
||||
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import type { ChromeNavControl, ChromeNavControls } from '@kbn/core-chrome-browser';
|
||||
import type {
|
||||
ChromeNavControl,
|
||||
ChromeNavControls,
|
||||
ChromeHelpMenuLink,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
|
||||
/** @internal */
|
||||
export class NavControlsService {
|
||||
|
@ -20,6 +24,7 @@ export class NavControlsService {
|
|||
const navControlsRight$ = new BehaviorSubject<ReadonlySet<ChromeNavControl>>(new Set());
|
||||
const navControlsCenter$ = new BehaviorSubject<ReadonlySet<ChromeNavControl>>(new Set());
|
||||
const navControlsExtension$ = new BehaviorSubject<ReadonlySet<ChromeNavControl>>(new Set());
|
||||
const helpMenuLinks$ = new BehaviorSubject<ChromeHelpMenuLink[]>([]);
|
||||
|
||||
return {
|
||||
// In the future, registration should be moved to the setup phase. This
|
||||
|
@ -36,6 +41,14 @@ export class NavControlsService {
|
|||
registerExtension: (navControl: ChromeNavControl) =>
|
||||
navControlsExtension$.next(new Set([...navControlsExtension$.value.values(), navControl])),
|
||||
|
||||
setHelpMenuLinks: (links: ChromeHelpMenuLink[]) => {
|
||||
// This extension point is only intended to be used once by the cloud integration > cloud_links plugin
|
||||
if (helpMenuLinks$.value.length > 0) {
|
||||
throw new Error(`Help menu links have already been set`);
|
||||
}
|
||||
helpMenuLinks$.next(links);
|
||||
},
|
||||
|
||||
getLeft$: () =>
|
||||
navControlsLeft$.pipe(
|
||||
map((controls) => sortBy([...controls.values()], 'order')),
|
||||
|
@ -56,6 +69,7 @@ export class NavControlsService {
|
|||
map((controls) => sortBy([...controls.values()], 'order')),
|
||||
takeUntil(this.stop$)
|
||||
),
|
||||
getHelpMenuLinks$: () => helpMenuLinks$.pipe(takeUntil(this.stop$)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { StubBrowserStorage, mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
||||
import { docLinksServiceMock } from '@kbn/core-doc-links-browser-mocks';
|
||||
import type { ChromeBreadcrumbsAppendExtension } from '@kbn/core-chrome-browser';
|
||||
import { Header } from './header';
|
||||
|
||||
|
@ -30,6 +31,7 @@ function mockProps() {
|
|||
isVisible$: new BehaviorSubject(true),
|
||||
customBranding$: new BehaviorSubject({}),
|
||||
kibanaDocLink: '/docs',
|
||||
docLinks: docLinksServiceMock.createStartContract(),
|
||||
navLinks$: new BehaviorSubject([]),
|
||||
customNavLink$: new BehaviorSubject(undefined),
|
||||
recentlyAccessed$: new BehaviorSubject([]),
|
||||
|
@ -87,6 +89,7 @@ describe('Header', () => {
|
|||
customNavLink$={customNavLink$}
|
||||
breadcrumbsAppendExtension$={breadcrumbsAppendExtension$}
|
||||
headerBanner$={headerBanner$}
|
||||
helpMenuLinks$={of([])}
|
||||
/>
|
||||
);
|
||||
expect(component.find('EuiHeader').exists()).toBeFalsy();
|
||||
|
|
|
@ -27,6 +27,7 @@ import type {
|
|||
ChromeBreadcrumb,
|
||||
ChromeNavControl,
|
||||
ChromeNavLink,
|
||||
ChromeHelpMenuLink,
|
||||
ChromeRecentlyAccessedHistoryItem,
|
||||
ChromeBreadcrumbsAppendExtension,
|
||||
ChromeHelpExtension,
|
||||
|
@ -34,6 +35,7 @@ import type {
|
|||
ChromeUserBanner,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
import { CustomBranding } from '@kbn/core-custom-branding-common';
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import { LoadingIndicator } from '../loading_indicator';
|
||||
import type { OnIsLockedUpdate } from './types';
|
||||
import { CollapsibleNav } from './collapsible_nav';
|
||||
|
@ -59,12 +61,14 @@ export interface HeaderProps {
|
|||
homeHref: string;
|
||||
isVisible$: Observable<boolean>;
|
||||
kibanaDocLink: string;
|
||||
docLinks: DocLinksStart;
|
||||
navLinks$: Observable<ChromeNavLink[]>;
|
||||
recentlyAccessed$: Observable<ChromeRecentlyAccessedHistoryItem[]>;
|
||||
forceAppSwitcherNavigation$: Observable<boolean>;
|
||||
globalHelpExtensionMenuLinks$: Observable<ChromeGlobalHelpExtensionMenuLink[]>;
|
||||
helpExtension$: Observable<ChromeHelpExtension | undefined>;
|
||||
helpSupportUrl$: Observable<string>;
|
||||
helpMenuLinks$: Observable<ChromeHelpMenuLink[]>;
|
||||
navControlsLeft$: Observable<readonly ChromeNavControl[]>;
|
||||
navControlsCenter$: Observable<readonly ChromeNavControl[]>;
|
||||
navControlsRight$: Observable<readonly ChromeNavControl[]>;
|
||||
|
@ -79,6 +83,7 @@ export interface HeaderProps {
|
|||
export function Header({
|
||||
kibanaVersion,
|
||||
kibanaDocLink,
|
||||
docLinks,
|
||||
application,
|
||||
basePath,
|
||||
onIsLockedUpdate,
|
||||
|
@ -163,7 +168,9 @@ export function Header({
|
|||
globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$}
|
||||
helpExtension$={observables.helpExtension$}
|
||||
helpSupportUrl$={observables.helpSupportUrl$}
|
||||
defaultContentLinks$={observables.helpMenuLinks$}
|
||||
kibanaDocLink={kibanaDocLink}
|
||||
docLinks={docLinks}
|
||||
kibanaVersion={kibanaVersion}
|
||||
navigateToUrl={application.navigateToUrl}
|
||||
/>,
|
||||
|
|
|
@ -10,6 +10,8 @@ import React from 'react';
|
|||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
||||
import { docLinksServiceMock } from '@kbn/core-doc-links-browser-mocks';
|
||||
|
||||
import { HeaderHelpMenu } from './header_help_menu';
|
||||
|
||||
describe('HeaderHelpMenu', () => {
|
||||
|
@ -26,14 +28,23 @@ describe('HeaderHelpMenu', () => {
|
|||
helpSupportUrl$={helpSupportUrl$}
|
||||
kibanaVersion={'version'}
|
||||
kibanaDocLink={''}
|
||||
docLinks={docLinksServiceMock.createStartContract()}
|
||||
defaultContentLinks$={of([])}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find('EuiButtonEmpty').length).toBe(1); // only the toggle view on/off button
|
||||
component.find('EuiButtonEmpty').simulate('click');
|
||||
|
||||
// 4 default links + the toggle button
|
||||
expect(component.find('EuiButtonEmpty').length).toBe(5);
|
||||
const buttons = component.find('EuiButtonEmpty');
|
||||
const buttonTexts = buttons.map((button) => button.text()).filter((text) => text.trim() !== '');
|
||||
|
||||
expect(buttonTexts).toEqual([
|
||||
'Kibana documentation',
|
||||
'Ask Elastic',
|
||||
'Give feedback',
|
||||
'Open an issue in GitHub',
|
||||
]);
|
||||
});
|
||||
|
||||
test('it renders the global custom content + the default content', () => {
|
||||
|
@ -63,6 +74,8 @@ describe('HeaderHelpMenu', () => {
|
|||
helpSupportUrl$={helpSupportUrl$}
|
||||
kibanaVersion={'version'}
|
||||
kibanaDocLink={''}
|
||||
docLinks={docLinksServiceMock.createStartContract()}
|
||||
defaultContentLinks$={of([])}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -29,17 +29,56 @@ import type {
|
|||
ChromeHelpExtension,
|
||||
ChromeGlobalHelpExtensionMenuLink,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
import { GITHUB_CREATE_ISSUE_LINK, KIBANA_FEEDBACK_LINK } from '../../constants';
|
||||
import type { ChromeHelpMenuLink } from '@kbn/core-chrome-browser/src';
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
|
||||
import { HeaderExtension } from './header_extension';
|
||||
import { isModifiedOrPrevented } from './nav_link';
|
||||
|
||||
const buildDefaultContentLinks = ({
|
||||
kibanaDocLink,
|
||||
docLinks,
|
||||
helpSupportUrl,
|
||||
}: {
|
||||
kibanaDocLink: string;
|
||||
docLinks: DocLinksStart;
|
||||
helpSupportUrl: string;
|
||||
}): ChromeHelpMenuLink[] => [
|
||||
{
|
||||
title: i18n.translate('core.ui.chrome.headerGlobalNav.helpMenuKibanaDocumentationTitle', {
|
||||
defaultMessage: 'Kibana documentation',
|
||||
}),
|
||||
href: kibanaDocLink,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('core.ui.chrome.headerGlobalNav.helpMenuAskElasticTitle', {
|
||||
defaultMessage: 'Ask Elastic',
|
||||
}),
|
||||
href: helpSupportUrl,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('core.ui.chrome.headerGlobalNav.helpMenuGiveFeedbackTitle', {
|
||||
defaultMessage: 'Give feedback',
|
||||
}),
|
||||
href: docLinks.links.kibana.feedback,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('core.ui.chrome.headerGlobalNav.helpMenuOpenGitHubIssueTitle', {
|
||||
defaultMessage: 'Open an issue in GitHub',
|
||||
}),
|
||||
href: docLinks.links.kibana.createGithubIssue,
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
navigateToUrl: InternalApplicationStart['navigateToUrl'];
|
||||
globalHelpExtensionMenuLinks$: Observable<ChromeGlobalHelpExtensionMenuLink[]>;
|
||||
helpExtension$: Observable<ChromeHelpExtension | undefined>;
|
||||
helpSupportUrl$: Observable<string>;
|
||||
defaultContentLinks$: Observable<ChromeHelpMenuLink[]>;
|
||||
kibanaVersion: string;
|
||||
kibanaDocLink: string;
|
||||
docLinks: DocLinksStart;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -47,6 +86,7 @@ interface State {
|
|||
helpExtension?: ChromeHelpExtension;
|
||||
helpSupportUrl: string;
|
||||
globalHelpExtensionMenuLinks: ChromeGlobalHelpExtensionMenuLink[];
|
||||
defaultContentLinks: ChromeHelpMenuLink[];
|
||||
}
|
||||
|
||||
export class HeaderHelpMenu extends Component<Props, State> {
|
||||
|
@ -60,6 +100,7 @@ export class HeaderHelpMenu extends Component<Props, State> {
|
|||
helpExtension: undefined,
|
||||
helpSupportUrl: '',
|
||||
globalHelpExtensionMenuLinks: [],
|
||||
defaultContentLinks: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -67,14 +108,21 @@ export class HeaderHelpMenu extends Component<Props, State> {
|
|||
this.subscription = combineLatest(
|
||||
this.props.helpExtension$,
|
||||
this.props.helpSupportUrl$,
|
||||
this.props.globalHelpExtensionMenuLinks$
|
||||
).subscribe(([helpExtension, helpSupportUrl, globalHelpExtensionMenuLinks]) => {
|
||||
this.setState({
|
||||
helpExtension,
|
||||
helpSupportUrl,
|
||||
globalHelpExtensionMenuLinks,
|
||||
});
|
||||
});
|
||||
this.props.globalHelpExtensionMenuLinks$,
|
||||
this.props.defaultContentLinks$
|
||||
).subscribe(
|
||||
([helpExtension, helpSupportUrl, globalHelpExtensionMenuLinks, defaultContentLinks]) => {
|
||||
this.setState({
|
||||
helpExtension,
|
||||
helpSupportUrl,
|
||||
globalHelpExtensionMenuLinks,
|
||||
defaultContentLinks:
|
||||
defaultContentLinks.length === 0
|
||||
? buildDefaultContentLinks({ ...this.props, helpSupportUrl })
|
||||
: defaultContentLinks,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -137,58 +185,33 @@ export class HeaderHelpMenu extends Component<Props, State> {
|
|||
<div style={{ maxWidth: 240 }}>
|
||||
{globalCustomContent}
|
||||
{defaultContent}
|
||||
{(defaultContent || customContent) && <EuiHorizontalRule margin="m" />}
|
||||
{customContent}
|
||||
{customContent && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
{customContent}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDefaultContent() {
|
||||
const { kibanaDocLink } = this.props;
|
||||
const { helpSupportUrl } = this.state;
|
||||
const { defaultContentLinks } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButtonEmpty href={kibanaDocLink} target="_blank" size="s" flush="left">
|
||||
<FormattedMessage
|
||||
id="core.ui.chrome.headerGlobalNav.helpMenuKibanaDocumentationTitle"
|
||||
defaultMessage="Kibana documentation"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiButtonEmpty href={helpSupportUrl} target="_blank" size="s" flush="left">
|
||||
<FormattedMessage
|
||||
id="core.ui.chrome.headerGlobalNav.helpMenuAskElasticTitle"
|
||||
defaultMessage="Ask Elastic"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiButtonEmpty href={KIBANA_FEEDBACK_LINK} target="_blank" size="s" flush="left">
|
||||
<FormattedMessage
|
||||
id="core.ui.chrome.headerGlobalNav.helpMenuGiveFeedbackTitle"
|
||||
defaultMessage="Give feedback"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiButtonEmpty
|
||||
href={GITHUB_CREATE_ISSUE_LINK}
|
||||
target="_blank"
|
||||
size="s"
|
||||
iconType="logoGithub"
|
||||
flush="left"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="core.ui.chrome.headerGlobalNav.helpMenuOpenGitHubIssueTitle"
|
||||
defaultMessage="Open an issue in GitHub"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
{defaultContentLinks.map(({ href, title, iconType }, i) => {
|
||||
const isLast = i === defaultContentLinks.length - 1;
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<EuiButtonEmpty href={href} target="_blank" size="s" flush="left" iconType={iconType}>
|
||||
{title}
|
||||
</EuiButtonEmpty>
|
||||
{!isLast && <EuiSpacer size="xs" />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { EuiHeader } from '@elastic/eui';
|
||||
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
||||
import { docLinksServiceMock } from '@kbn/core-doc-links-browser-mocks';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
|
@ -20,10 +21,11 @@ describe('Header', () => {
|
|||
application: mockApplication,
|
||||
breadcrumbs$: Rx.of([]),
|
||||
actionMenu$: Rx.of(undefined),
|
||||
kibanaDocLink: 'app/help/doclinks',
|
||||
docLinks: docLinksServiceMock.createStartContract(),
|
||||
globalHelpExtensionMenuLinks$: Rx.of([]),
|
||||
helpExtension$: Rx.of(undefined),
|
||||
helpSupportUrl$: Rx.of('app/help'),
|
||||
helpMenuLinks$: Rx.of([]),
|
||||
homeHref$: Rx.of('app/home'),
|
||||
kibanaVersion: '8.9',
|
||||
loadingCount$: Rx.of(0),
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
ChromeBreadcrumb,
|
||||
ChromeGlobalHelpExtensionMenuLink,
|
||||
ChromeHelpExtension,
|
||||
ChromeHelpMenuLink,
|
||||
ChromeNavControl,
|
||||
} from '@kbn/core-chrome-browser/src';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
|
@ -34,6 +35,8 @@ import { Router } from '@kbn/shared-ux-router';
|
|||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { Observable, debounceTime } from 'rxjs';
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
|
||||
import { HeaderActionMenu, useHeaderActionMenuMounter } from '../header/header_action_menu';
|
||||
import { HeaderBreadcrumbs } from '../header/header_breadcrumbs';
|
||||
import { HeaderHelpMenu } from '../header/header_help_menu';
|
||||
|
@ -87,11 +90,12 @@ const headerStrings = {
|
|||
export interface Props {
|
||||
breadcrumbs$: Observable<ChromeBreadcrumb[]>;
|
||||
actionMenu$: Observable<MountPoint | undefined>;
|
||||
kibanaDocLink: string;
|
||||
docLinks: DocLinksStart;
|
||||
children: React.ReactNode;
|
||||
globalHelpExtensionMenuLinks$: Observable<ChromeGlobalHelpExtensionMenuLink[]>;
|
||||
helpExtension$: Observable<ChromeHelpExtension | undefined>;
|
||||
helpSupportUrl$: Observable<string>;
|
||||
helpMenuLinks$: Observable<ChromeHelpMenuLink[]>;
|
||||
homeHref$: Observable<string | undefined>;
|
||||
kibanaVersion: string;
|
||||
application: InternalApplicationStart;
|
||||
|
@ -158,10 +162,10 @@ const Logo = (
|
|||
|
||||
export const ProjectHeader = ({
|
||||
application,
|
||||
kibanaDocLink,
|
||||
kibanaVersion,
|
||||
children,
|
||||
prependBasePath,
|
||||
docLinks,
|
||||
...observables
|
||||
}: Props) => {
|
||||
const [navId] = useState(htmlIdGenerator()());
|
||||
|
@ -239,7 +243,9 @@ export const ProjectHeader = ({
|
|||
globalHelpExtensionMenuLinks$={observables.globalHelpExtensionMenuLinks$}
|
||||
helpExtension$={observables.helpExtension$}
|
||||
helpSupportUrl$={observables.helpSupportUrl$}
|
||||
kibanaDocLink={kibanaDocLink}
|
||||
defaultContentLinks$={observables.helpMenuLinks$}
|
||||
kibanaDocLink={docLinks.links.elasticStackGetStarted}
|
||||
docLinks={docLinks}
|
||||
kibanaVersion={kibanaVersion}
|
||||
navigateToUrl={application.navigateToUrl}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { DeeplyMockedKeys } from '@kbn/utility-types-jest';
|
||||
import type { ChromeBadge, ChromeBreadcrumb } from '@kbn/core-chrome-browser';
|
||||
|
@ -41,6 +41,8 @@ const createStartContractMock = () => {
|
|||
getCenter$: jest.fn(),
|
||||
getRight$: jest.fn(),
|
||||
getExtension$: jest.fn(),
|
||||
setHelpMenuLinks: jest.fn(),
|
||||
getHelpMenuLinks$: jest.fn(),
|
||||
},
|
||||
setIsVisible: jest.fn(),
|
||||
getIsVisible$: jest.fn(),
|
||||
|
@ -54,7 +56,9 @@ const createStartContractMock = () => {
|
|||
registerGlobalHelpExtensionMenuLink: jest.fn(),
|
||||
getHelpExtension$: jest.fn(),
|
||||
setHelpExtension: jest.fn(),
|
||||
setHelpMenuLinks: jest.fn(),
|
||||
setHelpSupportUrl: jest.fn(),
|
||||
getHelpSupportUrl$: jest.fn(() => of('https://www.elastic.co/support')),
|
||||
getIsNavDrawerLocked$: jest.fn(),
|
||||
getCustomNavLink$: jest.fn(),
|
||||
setCustomNavLink: jest.fn(),
|
||||
|
|
|
@ -14,6 +14,7 @@ export type {
|
|||
ChromeBreadcrumbsAppendExtension,
|
||||
ChromeDocTitle,
|
||||
ChromeGlobalHelpExtensionMenuLink,
|
||||
ChromeHelpMenuLink,
|
||||
ChromeHelpExtension,
|
||||
ChromeHelpExtensionLinkBase,
|
||||
ChromeHelpExtensionMenuCustomLink,
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { Observable } from 'rxjs';
|
|||
import type { ChromeNavLink, ChromeNavLinks } from './nav_links';
|
||||
import type { ChromeRecentlyAccessed } from './recently_accessed';
|
||||
import type { ChromeDocTitle } from './doc_title';
|
||||
import type { ChromeNavControls } from './nav_controls';
|
||||
import type { ChromeHelpMenuLink, ChromeNavControls } from './nav_controls';
|
||||
import type { ChromeHelpExtension } from './help_extension';
|
||||
import type { ChromeBreadcrumb, ChromeBreadcrumbsAppendExtension } from './breadcrumb';
|
||||
import type { ChromeBadge, ChromeStyle, ChromeUserBanner } from './types';
|
||||
|
@ -106,6 +106,11 @@ export interface ChromeStart {
|
|||
*/
|
||||
setCustomNavLink(newCustomNavLink?: Partial<ChromeNavLink>): void;
|
||||
|
||||
/**
|
||||
* Override the default links shown in the help menu
|
||||
*/
|
||||
setHelpMenuLinks(links: ChromeHelpMenuLink[]): void;
|
||||
|
||||
/**
|
||||
* Get the list of the registered global help extension menu links
|
||||
*/
|
||||
|
@ -134,6 +139,11 @@ export interface ChromeStart {
|
|||
*/
|
||||
setHelpSupportUrl(url: string): void;
|
||||
|
||||
/**
|
||||
* Get the support URL shown in the help menu
|
||||
*/
|
||||
getHelpSupportUrl$(): Observable<string>;
|
||||
|
||||
/**
|
||||
* Get an observable of the current locked state of the nav drawer.
|
||||
*/
|
||||
|
|
|
@ -20,7 +20,7 @@ export type {
|
|||
ChromeHelpExtensionMenuGitHubLink,
|
||||
ChromeGlobalHelpExtensionMenuLink,
|
||||
} from './help_extension';
|
||||
export type { ChromeNavControls, ChromeNavControl } from './nav_controls';
|
||||
export type { ChromeNavControls, ChromeNavControl, ChromeHelpMenuLink } from './nav_controls';
|
||||
export type { ChromeNavLinks, ChromeNavLink } from './nav_links';
|
||||
export type {
|
||||
ChromeRecentlyAccessed,
|
||||
|
|
|
@ -15,6 +15,13 @@ export interface ChromeNavControl {
|
|||
mount: MountPoint;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ChromeHelpMenuLink {
|
||||
title: string;
|
||||
href: string;
|
||||
iconType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ChromeNavControls | APIs} for registering new controls to be displayed in the navigation bar.
|
||||
*
|
||||
|
@ -44,6 +51,9 @@ export interface ChromeNavControls {
|
|||
/** Register an extension to be presented to the left of the top-right side of the chrome header. */
|
||||
registerExtension(navControl: ChromeNavControl): void;
|
||||
|
||||
/** Set the help menu links */
|
||||
setHelpMenuLinks(links: ChromeHelpMenuLink[]): void;
|
||||
|
||||
/** @internal */
|
||||
getLeft$(): Observable<ChromeNavControl[]>;
|
||||
|
||||
|
@ -55,4 +65,7 @@ export interface ChromeNavControls {
|
|||
|
||||
/** @internal */
|
||||
getExtension$(): Observable<ChromeNavControl[]>;
|
||||
|
||||
/** @internal */
|
||||
getHelpMenuLinks$(): Observable<ChromeHelpMenuLink[]>;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
const DOC_LINK_VERSION = meta.version;
|
||||
const ELASTIC_WEBSITE_URL = meta.elasticWebsiteUrl;
|
||||
const DOCS_WEBSITE_URL = meta.docsWebsiteUrl;
|
||||
const ELASTIC_GITHUB = meta.elasticGithubUrl;
|
||||
|
||||
const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
|
||||
const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`;
|
||||
|
@ -305,6 +306,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
},
|
||||
addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`,
|
||||
kibana: {
|
||||
askElastic: `${ELASTIC_WEBSITE_URL}products/kibana/ask-elastic?blade=kibanaaskelastic`,
|
||||
createGithubIssue: `${ELASTIC_GITHUB}kibana/issues/new/choose`,
|
||||
feedback: `${ELASTIC_WEBSITE_URL}products/kibana/feedback?blade=kibanafeedback`,
|
||||
guide: `${KIBANA_DOCS}index.html`,
|
||||
autocompleteSuggestions: `${KIBANA_DOCS}kibana-concepts-analysts.html#autocomplete-suggestions`,
|
||||
secureSavedObject: `${KIBANA_DOCS}xpack-security-secure-saved-objects.html`,
|
||||
|
|
|
@ -16,6 +16,7 @@ export const getDocLinksMeta = ({ kibanaBranch }: GetDocLinksMetaOptions): DocLi
|
|||
return {
|
||||
version: kibanaBranch === 'main' ? 'master' : kibanaBranch,
|
||||
elasticWebsiteUrl: 'https://www.elastic.co/',
|
||||
elasticGithubUrl: 'https://github.com/elastic/',
|
||||
docsWebsiteUrl: 'https://docs.elastic.co/',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export interface DocLinksMeta {
|
||||
version: string;
|
||||
elasticWebsiteUrl: string;
|
||||
elasticGithubUrl: string;
|
||||
docsWebsiteUrl: string;
|
||||
}
|
||||
|
||||
|
@ -284,6 +285,9 @@ export interface DocLinks {
|
|||
};
|
||||
readonly addData: string;
|
||||
readonly kibana: {
|
||||
readonly askElastic: string;
|
||||
readonly createGithubIssue: string;
|
||||
readonly feedback: string;
|
||||
readonly guide: string;
|
||||
readonly autocompleteSuggestions: string;
|
||||
readonly secureSavedObject: string;
|
||||
|
|
|
@ -17,7 +17,7 @@ pageLoadAssetSize:
|
|||
cloudExperiments: 59358
|
||||
cloudFullStory: 18493
|
||||
cloudGainsight: 18710
|
||||
cloudLinks: 17629
|
||||
cloudLinks: 55984
|
||||
cloudSecurityPosture: 19109
|
||||
console: 46091
|
||||
contentManagement: 16254
|
||||
|
|
|
@ -215,6 +215,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cloud_integrations.gain_sight.org_id (any)',
|
||||
'xpack.cloud.id (string)',
|
||||
'xpack.cloud.organization_url (string)',
|
||||
'xpack.cloud.billing_url (string)',
|
||||
'xpack.cloud.profile_url (string)',
|
||||
'xpack.discoverEnhanced.actions.exploreDataInChart.enabled (boolean)',
|
||||
'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean)',
|
||||
|
|
|
@ -39,6 +39,7 @@ const createStartMock = (): jest.Mocked<CloudStart> => ({
|
|||
cloudId: 'mock-cloud-id',
|
||||
isCloudEnabled: true,
|
||||
deploymentUrl: 'deployment-url',
|
||||
billingUrl: 'billing-url',
|
||||
profileUrl: 'profile-url',
|
||||
organizationUrl: 'organization-url',
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface CloudConfigType {
|
|||
base_url?: string;
|
||||
profile_url?: string;
|
||||
deployment_url?: string;
|
||||
billing_url?: string;
|
||||
organization_url?: string;
|
||||
trial_end_date?: string;
|
||||
is_elastic_staff_owned?: boolean;
|
||||
|
@ -30,6 +31,7 @@ export interface CloudConfigType {
|
|||
interface CloudUrls {
|
||||
deploymentUrl?: string;
|
||||
profileUrl?: string;
|
||||
billingUrl?: string;
|
||||
organizationUrl?: string;
|
||||
snapshotsUrl?: string;
|
||||
}
|
||||
|
@ -99,12 +101,13 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
);
|
||||
};
|
||||
|
||||
const { deploymentUrl, profileUrl, organizationUrl } = this.getCloudUrls();
|
||||
const { deploymentUrl, profileUrl, billingUrl, organizationUrl } = this.getCloudUrls();
|
||||
|
||||
return {
|
||||
CloudContextProvider,
|
||||
isCloudEnabled: this.isCloudEnabled,
|
||||
cloudId: this.config.id,
|
||||
billingUrl,
|
||||
deploymentUrl,
|
||||
profileUrl,
|
||||
organizationUrl,
|
||||
|
@ -116,6 +119,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
private getCloudUrls(): CloudUrls {
|
||||
const {
|
||||
profile_url: profileUrl,
|
||||
billing_url: billingUrl,
|
||||
organization_url: organizationUrl,
|
||||
deployment_url: deploymentUrl,
|
||||
base_url: baseUrl,
|
||||
|
@ -123,12 +127,14 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
|
||||
const fullCloudDeploymentUrl = getFullCloudUrl(baseUrl, deploymentUrl);
|
||||
const fullCloudProfileUrl = getFullCloudUrl(baseUrl, profileUrl);
|
||||
const fullCloudBillingUrl = getFullCloudUrl(baseUrl, billingUrl);
|
||||
const fullCloudOrganizationUrl = getFullCloudUrl(baseUrl, organizationUrl);
|
||||
const fullCloudSnapshotsUrl = `${fullCloudDeploymentUrl}/${CLOUD_SNAPSHOTS_PATH}`;
|
||||
|
||||
return {
|
||||
deploymentUrl: fullCloudDeploymentUrl,
|
||||
profileUrl: fullCloudProfileUrl,
|
||||
billingUrl: fullCloudBillingUrl,
|
||||
organizationUrl: fullCloudOrganizationUrl,
|
||||
snapshotsUrl: fullCloudSnapshotsUrl,
|
||||
};
|
||||
|
|
|
@ -28,6 +28,10 @@ export interface CloudStart {
|
|||
* The full URL to the user profile page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
profileUrl?: string;
|
||||
/**
|
||||
* The full URL to the billing page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
billingUrl?: string;
|
||||
/**
|
||||
* The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
|
|
|
@ -24,6 +24,7 @@ const configSchema = schema.object({
|
|||
cname: schema.maybe(schema.string()),
|
||||
deployment_url: schema.maybe(schema.string()),
|
||||
id: schema.maybe(schema.string()),
|
||||
billing_url: schema.maybe(schema.string()),
|
||||
organization_url: schema.maybe(schema.string()),
|
||||
profile_url: schema.maybe(schema.string()),
|
||||
trial_end_date: schema.maybe(schema.string()),
|
||||
|
@ -38,6 +39,7 @@ export const config: PluginConfigDescriptor<CloudConfigType> = {
|
|||
cname: true,
|
||||
deployment_url: true,
|
||||
id: true,
|
||||
billing_url: true,
|
||||
organization_url: true,
|
||||
profile_url: true,
|
||||
trial_end_date: true,
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ChromeHelpMenuLink } from '@kbn/core-chrome-browser';
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
|
||||
export const createHelpMenuLinks = ({
|
||||
docLinks,
|
||||
helpSupportUrl,
|
||||
}: {
|
||||
docLinks: DocLinksStart;
|
||||
helpSupportUrl: string;
|
||||
}) => {
|
||||
const helpMenuLinks: ChromeHelpMenuLink[] = [
|
||||
{
|
||||
title: i18n.translate('xpack.cloudLinks.helpMenuLinks.documentation', {
|
||||
defaultMessage: 'Documentation',
|
||||
}),
|
||||
href: docLinks.links.elasticStackGetStarted,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.cloudLinks.helpMenuLinks.support', {
|
||||
defaultMessage: 'Support',
|
||||
}),
|
||||
href: helpSupportUrl,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.cloudLinks.helpMenuLinks.giveFeedback', {
|
||||
defaultMessage: 'Give feedback',
|
||||
}),
|
||||
href: docLinks.links.kibana.feedback,
|
||||
},
|
||||
];
|
||||
|
||||
return helpMenuLinks;
|
||||
};
|
|
@ -18,6 +18,7 @@ describe('maybeAddCloudLinks', () => {
|
|||
security,
|
||||
chrome: coreMock.createStart().chrome,
|
||||
cloud: { ...cloudMock.createStart(), isCloudEnabled: false },
|
||||
docLinks: coreMock.createStart().docLinks,
|
||||
});
|
||||
// Since there's a promise, let's wait for the next tick
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
@ -29,11 +30,12 @@ describe('maybeAddCloudLinks', () => {
|
|||
security.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({ elastic_cloud_user: true })
|
||||
);
|
||||
const chrome = coreMock.createStart().chrome;
|
||||
const { chrome, docLinks } = coreMock.createStart();
|
||||
maybeAddCloudLinks({
|
||||
security,
|
||||
chrome,
|
||||
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
|
||||
docLinks,
|
||||
});
|
||||
// Since there's a promise, let's wait for the next tick
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
@ -55,15 +57,41 @@ describe('maybeAddCloudLinks', () => {
|
|||
Object {
|
||||
"href": "profile-url",
|
||||
"iconType": "user",
|
||||
"label": "Edit profile",
|
||||
"label": "Profile",
|
||||
"order": 100,
|
||||
"setAsProfile": true,
|
||||
},
|
||||
Object {
|
||||
"href": "billing-url",
|
||||
"iconType": "visGauge",
|
||||
"label": "Billing",
|
||||
"order": 200,
|
||||
},
|
||||
Object {
|
||||
"href": "organization-url",
|
||||
"iconType": "gear",
|
||||
"label": "Account & Billing",
|
||||
"order": 200,
|
||||
"label": "Organization",
|
||||
"order": 300,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
|
||||
expect(chrome.setHelpMenuLinks).toHaveBeenCalledTimes(1);
|
||||
expect(chrome.setHelpMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"href": "https://www.elastic.co/guide/en/index.html",
|
||||
"title": "Documentation",
|
||||
},
|
||||
Object {
|
||||
"href": "https://www.elastic.co/support",
|
||||
"title": "Support",
|
||||
},
|
||||
Object {
|
||||
"href": "https://www.elastic.co/products/kibana/feedback?blade=kibanafeedback",
|
||||
"title": "Give feedback",
|
||||
},
|
||||
],
|
||||
]
|
||||
|
@ -73,11 +101,12 @@ describe('maybeAddCloudLinks', () => {
|
|||
it('when cloud enabled and it fails to fetch the user, it sets the links', async () => {
|
||||
const security = securityMock.createStart();
|
||||
security.authc.getCurrentUser.mockRejectedValue(new Error('Something went terribly wrong'));
|
||||
const chrome = coreMock.createStart().chrome;
|
||||
const { chrome, docLinks } = coreMock.createStart();
|
||||
maybeAddCloudLinks({
|
||||
security,
|
||||
chrome,
|
||||
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
|
||||
docLinks,
|
||||
});
|
||||
// Since there's a promise, let's wait for the next tick
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
@ -99,15 +128,40 @@ describe('maybeAddCloudLinks', () => {
|
|||
Object {
|
||||
"href": "profile-url",
|
||||
"iconType": "user",
|
||||
"label": "Edit profile",
|
||||
"label": "Profile",
|
||||
"order": 100,
|
||||
"setAsProfile": true,
|
||||
},
|
||||
Object {
|
||||
"href": "billing-url",
|
||||
"iconType": "visGauge",
|
||||
"label": "Billing",
|
||||
"order": 200,
|
||||
},
|
||||
Object {
|
||||
"href": "organization-url",
|
||||
"iconType": "gear",
|
||||
"label": "Account & Billing",
|
||||
"order": 200,
|
||||
"label": "Organization",
|
||||
"order": 300,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(chrome.setHelpMenuLinks).toHaveBeenCalledTimes(1);
|
||||
expect(chrome.setHelpMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"href": "https://www.elastic.co/guide/en/index.html",
|
||||
"title": "Documentation",
|
||||
},
|
||||
Object {
|
||||
"href": "https://www.elastic.co/support",
|
||||
"title": "Support",
|
||||
},
|
||||
Object {
|
||||
"href": "https://www.elastic.co/products/kibana/feedback?blade=kibanafeedback",
|
||||
"title": "Give feedback",
|
||||
},
|
||||
],
|
||||
]
|
||||
|
@ -119,16 +173,18 @@ describe('maybeAddCloudLinks', () => {
|
|||
security.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({ elastic_cloud_user: false })
|
||||
);
|
||||
const chrome = coreMock.createStart().chrome;
|
||||
const { chrome, docLinks } = coreMock.createStart();
|
||||
maybeAddCloudLinks({
|
||||
security,
|
||||
chrome,
|
||||
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
|
||||
docLinks,
|
||||
});
|
||||
// Since there's a promise, let's wait for the next tick
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
expect(security.authc.getCurrentUser).toHaveBeenCalledTimes(1);
|
||||
expect(chrome.setCustomNavLink).not.toHaveBeenCalled();
|
||||
expect(security.navControlService.addUserMenuLinks).not.toHaveBeenCalled();
|
||||
expect(chrome.setHelpMenuLinks).not.toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,44 +5,63 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { catchError, defer, filter, map, of } from 'rxjs';
|
||||
import { catchError, defer, filter, map, of, combineLatest } from 'rxjs';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import type { ChromeStart } from '@kbn/core/public';
|
||||
import type { SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import { createUserMenuLinks } from './user_menu_links';
|
||||
import { createHelpMenuLinks } from './help_menu_links';
|
||||
|
||||
export interface MaybeAddCloudLinksDeps {
|
||||
security: SecurityPluginStart;
|
||||
chrome: ChromeStart;
|
||||
cloud: CloudStart;
|
||||
docLinks: DocLinksStart;
|
||||
}
|
||||
|
||||
export function maybeAddCloudLinks({ security, chrome, cloud }: MaybeAddCloudLinksDeps): void {
|
||||
export function maybeAddCloudLinks({
|
||||
security,
|
||||
chrome,
|
||||
cloud,
|
||||
docLinks,
|
||||
}: MaybeAddCloudLinksDeps): void {
|
||||
const userObservable = defer(() => security.authc.getCurrentUser()).pipe(
|
||||
// Check if user is a cloud user.
|
||||
map((user) => user.elastic_cloud_user),
|
||||
// If user is not defined due to an unexpected error, then fail *open*.
|
||||
catchError(() => of(true)),
|
||||
filter((isElasticCloudUser) => isElasticCloudUser === true),
|
||||
map(() => {
|
||||
if (cloud.deploymentUrl) {
|
||||
chrome.setCustomNavLink({
|
||||
title: i18n.translate('xpack.cloudLinks.deploymentLinkLabel', {
|
||||
defaultMessage: 'Manage this deployment',
|
||||
}),
|
||||
euiIconType: 'logoCloud',
|
||||
href: cloud.deploymentUrl,
|
||||
});
|
||||
}
|
||||
const userMenuLinks = createUserMenuLinks(cloud);
|
||||
security.navControlService.addUserMenuLinks(userMenuLinks);
|
||||
})
|
||||
);
|
||||
|
||||
const helpObservable = chrome.getHelpSupportUrl$();
|
||||
|
||||
if (cloud.isCloudEnabled) {
|
||||
defer(() => security.authc.getCurrentUser())
|
||||
.pipe(
|
||||
// Check if user is a cloud user.
|
||||
map((user) => user.elastic_cloud_user),
|
||||
// If user is not defined due to an unexpected error, then fail *open*.
|
||||
catchError(() => of(true)),
|
||||
filter((isElasticCloudUser) => isElasticCloudUser === true),
|
||||
map(() => {
|
||||
if (cloud.deploymentUrl) {
|
||||
chrome.setCustomNavLink({
|
||||
title: i18n.translate('xpack.cloudLinks.deploymentLinkLabel', {
|
||||
defaultMessage: 'Manage this deployment',
|
||||
}),
|
||||
euiIconType: 'logoCloud',
|
||||
href: cloud.deploymentUrl,
|
||||
});
|
||||
}
|
||||
const userMenuLinks = createUserMenuLinks(cloud);
|
||||
security.navControlService.addUserMenuLinks(userMenuLinks);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
combineLatest({ user: userObservable, helpSupportUrl: helpObservable }).subscribe(
|
||||
({ helpSupportUrl }) => {
|
||||
const helpMenuLinks = createHelpMenuLinks({
|
||||
docLinks,
|
||||
helpSupportUrl,
|
||||
});
|
||||
|
||||
chrome.setHelpMenuLinks(helpMenuLinks);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,13 +10,14 @@ import type { CloudStart } from '@kbn/cloud-plugin/public';
|
|||
import type { UserMenuLink } from '@kbn/security-plugin/public';
|
||||
|
||||
export const createUserMenuLinks = (cloud: CloudStart): UserMenuLink[] => {
|
||||
const { profileUrl, organizationUrl } = cloud;
|
||||
const { profileUrl, billingUrl, organizationUrl } = cloud;
|
||||
|
||||
const userMenuLinks = [] as UserMenuLink[];
|
||||
|
||||
if (profileUrl) {
|
||||
userMenuLinks.push({
|
||||
label: i18n.translate('xpack.cloudLinks.userMenuLinks.profileLinkText', {
|
||||
defaultMessage: 'Edit profile',
|
||||
defaultMessage: 'Profile',
|
||||
}),
|
||||
iconType: 'user',
|
||||
href: profileUrl,
|
||||
|
@ -25,14 +26,25 @@ export const createUserMenuLinks = (cloud: CloudStart): UserMenuLink[] => {
|
|||
});
|
||||
}
|
||||
|
||||
if (billingUrl) {
|
||||
userMenuLinks.push({
|
||||
label: i18n.translate('xpack.cloudLinks.userMenuLinks.billingLinkText', {
|
||||
defaultMessage: 'Billing',
|
||||
}),
|
||||
iconType: 'visGauge',
|
||||
href: billingUrl,
|
||||
order: 200,
|
||||
});
|
||||
}
|
||||
|
||||
if (organizationUrl) {
|
||||
userMenuLinks.push({
|
||||
label: i18n.translate('xpack.cloudLinks.userMenuLinks.accountLinkText', {
|
||||
defaultMessage: 'Account & Billing',
|
||||
label: i18n.translate('xpack.cloudLinks.userMenuLinks.organizationLinkText', {
|
||||
defaultMessage: 'Organization',
|
||||
}),
|
||||
iconType: 'gear',
|
||||
href: organizationUrl,
|
||||
order: 200,
|
||||
order: 300,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export class CloudLinksPlugin
|
|||
});
|
||||
}
|
||||
if (security) {
|
||||
maybeAddCloudLinks({ security, chrome: core.chrome, cloud });
|
||||
maybeAddCloudLinks({ security, chrome: core.chrome, cloud, docLinks: core.docLinks });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
"@kbn/i18n",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/guided-onboarding-plugin",
|
||||
"@kbn/core-chrome-browser",
|
||||
"@kbn/core-doc-links-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -39320,7 +39320,6 @@
|
|||
"xpack.cloudDataMigration.upgrade.text": "Effectuer la mise à niveau vers les versions plus récentes beaucoup plus facilement",
|
||||
"xpack.cloudLinks.deploymentLinkLabel": "Gérer ce déploiement",
|
||||
"xpack.cloudLinks.setupGuide": "Guides de configuration",
|
||||
"xpack.cloudLinks.userMenuLinks.accountLinkText": "Compte et facturation",
|
||||
"xpack.cloudLinks.userMenuLinks.profileLinkText": "Modifier le profil",
|
||||
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "Choisir le tableau de bord de destination",
|
||||
"xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "Ouvrir le tableau de bord dans un nouvel onglet",
|
||||
|
|
|
@ -39294,7 +39294,6 @@
|
|||
"xpack.cloudDataMigration.upgrade.text": "新しいバージョンへのアップグレードがより簡単に",
|
||||
"xpack.cloudLinks.deploymentLinkLabel": "このデプロイの管理",
|
||||
"xpack.cloudLinks.setupGuide": "セットアップガイド",
|
||||
"xpack.cloudLinks.userMenuLinks.accountLinkText": "会計・請求",
|
||||
"xpack.cloudLinks.userMenuLinks.profileLinkText": "プロフィールを編集",
|
||||
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "対象ダッシュボードを選択",
|
||||
"xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "新しいタブでダッシュボードを開く",
|
||||
|
|
|
@ -39288,7 +39288,6 @@
|
|||
"xpack.cloudDataMigration.upgrade.text": "更轻松地升级到较新版本",
|
||||
"xpack.cloudLinks.deploymentLinkLabel": "管理此部署",
|
||||
"xpack.cloudLinks.setupGuide": "设置指南",
|
||||
"xpack.cloudLinks.userMenuLinks.accountLinkText": "帐户和帐单",
|
||||
"xpack.cloudLinks.userMenuLinks.profileLinkText": "编辑配置文件",
|
||||
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "选择目标仪表板",
|
||||
"xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "在新选项卡中打开仪表板",
|
||||
|
|
|
@ -48,6 +48,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
'--xpack.cloud.base_url=https://cloud.elastic.co',
|
||||
'--xpack.cloud.deployment_url=/deployments/deploymentId',
|
||||
'--xpack.cloud.organization_url=/organization/organizationId',
|
||||
'--xpack.cloud.billing_url=/billing',
|
||||
'--xpack.cloud.profile_url=/user/userId',
|
||||
'--xpack.security.authc.selector.enabled=false',
|
||||
`--xpack.security.authc.providers=${JSON.stringify({
|
||||
|
|
|
@ -55,18 +55,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('Fills up the user menu items', () => {
|
||||
it('Shows the button Edit profile', async () => {
|
||||
await PageObjects.common.clickAndValidate('userMenuButton', 'userMenuLink__Edit profile');
|
||||
const cloudLink = await find.byLinkText('Edit profile');
|
||||
it('Shows the button Profile', async () => {
|
||||
await PageObjects.common.clickAndValidate('userMenuButton', 'userMenuLink__Profile');
|
||||
const cloudLink = await find.byLinkText('Profile');
|
||||
expect(cloudLink).to.not.be(null);
|
||||
});
|
||||
|
||||
it('Shows the button Account & Billing', async () => {
|
||||
await PageObjects.common.clickAndValidate(
|
||||
'userMenuButton',
|
||||
'userMenuLink__Account & Billing'
|
||||
);
|
||||
const cloudLink = await find.byLinkText('Account & Billing');
|
||||
it('Shows the button Billing', async () => {
|
||||
await PageObjects.common.clickAndValidate('userMenuButton', 'userMenuLink__Billing');
|
||||
const cloudLink = await find.byLinkText('Billing');
|
||||
expect(cloudLink).to.not.be(null);
|
||||
});
|
||||
|
||||
it('Shows the button Organization', async () => {
|
||||
await PageObjects.common.clickAndValidate('userMenuButton', 'userMenuLink__Organization');
|
||||
const cloudLink = await find.byLinkText('Organization');
|
||||
expect(cloudLink).to.not.be(null);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue