[7.x] Move Chrome UI to Core (#39300) (#39755)

This commit is contained in:
Josh Dover 2019-06-27 13:30:09 -05:00 committed by GitHub
parent 955d709d43
commit d6fb5ce920
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 623 additions and 506 deletions

View file

@ -4,6 +4,7 @@
## ChromeStart interface
ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser.
<b>Signature:</b>
@ -32,6 +33,7 @@ export interface ChromeStart
| [getIsCollapsed$()](./kibana-plugin-public.chromestart.getiscollapsed$.md) | Get an observable of the current collapsed state of the chrome. |
| [getIsVisible$()](./kibana-plugin-public.chromestart.getisvisible$.md) | Get an observable of the current visibility state of the chrome. |
| [removeApplicationClass(className)](./kibana-plugin-public.chromestart.removeapplicationclass.md) | Remove a className added with <code>addApplicationClass()</code>. If className is unknown it is ignored. |
| [setAppTitle(appTitle)](./kibana-plugin-public.chromestart.setapptitle.md) | Sets the current app's title |
| [setBadge(badge)](./kibana-plugin-public.chromestart.setbadge.md) | Override the current badge |
| [setBrand(brand)](./kibana-plugin-public.chromestart.setbrand.md) | Set the brand configuration. |
| [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs |
@ -39,3 +41,28 @@ export interface ChromeStart
| [setIsCollapsed(isCollapsed)](./kibana-plugin-public.chromestart.setiscollapsed.md) | Set the collapsed state of the chrome navigation. |
| [setIsVisible(isVisible)](./kibana-plugin-public.chromestart.setisvisible.md) | Set the temporary visibility for the chrome. This does nothing if the chrome is hidden by default and should be used to hide the chrome for things like full-screen modes with an exit button. |
## Remarks
While ChromeStart exposes many APIs, they should be used sparingly and the developer should understand how they affect other plugins and applications.
## Example 1
How to add a recently accessed item to the sidebar:
```ts
core.chrome.recentlyAccessed.add('/app/map/1234', 'Map 1234', '1234');
```
## Example 2
How to set the help dropdown extension:
```tsx
core.chrome.setHelpExtension(elem => {
ReactDOM.render(<MyHelpComponent />, elem);
return () => ReactDOM.unmountComponentAtNode(elem);
});
```

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeStart](./kibana-plugin-public.chromestart.md) &gt; [setAppTitle](./kibana-plugin-public.chromestart.setapptitle.md)
## ChromeStart.setAppTitle() method
Sets the current app's title
<b>Signature:</b>
```typescript
setAppTitle(appTitle: string): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| appTitle | <code>string</code> | |
<b>Returns:</b>
`void`

View file

@ -33,7 +33,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [ChromeNavLinks](./kibana-plugin-public.chromenavlinks.md) | [APIs](./kibana-plugin-public.chromenavlinks.md) for manipulating nav links. |
| [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. |
| [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | |
| [ChromeStart](./kibana-plugin-public.chromestart.md) | |
| [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. |
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the <code>Plugin</code> setup lifecycle |
| [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the <code>Plugin</code> start lifecycle |
| [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | |

View file

@ -0,0 +1 @@
@import './ui/index';

View file

@ -22,11 +22,12 @@ import {
ChromeBrand,
ChromeBreadcrumb,
ChromeService,
ChromeStart,
InternalChromeStart,
} from './chrome_service';
const createStartContractMock = () => {
const setupContract: jest.Mocked<ChromeStart> = {
const startContract: jest.Mocked<InternalChromeStart> = {
getComponent: jest.fn(),
navLinks: {
getNavLinks$: jest.fn(),
has: jest.fn(),
@ -48,6 +49,7 @@ const createStartContractMock = () => {
getLeft$: jest.fn(),
getRight$: jest.fn(),
},
setAppTitle: jest.fn(),
setBrand: jest.fn(),
getBrand$: jest.fn(),
setIsVisible: jest.fn(),
@ -64,14 +66,14 @@ const createStartContractMock = () => {
getHelpExtension$: jest.fn(),
setHelpExtension: jest.fn(),
};
setupContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand));
setupContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false));
setupContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false));
setupContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name']));
setupContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge));
setupContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb]));
setupContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined));
return setupContract;
startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand));
startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false));
startContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false));
startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name']));
startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge));
startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb]));
startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined));
return startContract;
};
type ChromeServiceContract = PublicMethodsOf<ChromeService>;

View file

@ -19,12 +19,15 @@
import * as Rx from 'rxjs';
import { toArray } from 'rxjs/operators';
import { shallow } from 'enzyme';
import React from 'react';
import { applicationServiceMock } from '../application/application_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
import { ChromeService } from './chrome_service';
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
const store = new Map();
(window as any).localStorage = {
@ -33,9 +36,10 @@ const store = new Map();
removeItem: (key: string) => store.delete(String(key)),
};
function defaultStartDeps(): any {
function defaultStartDeps() {
return {
application: applicationServiceMock.createStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
http: httpServiceMock.createStartContract(),
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
@ -77,6 +81,16 @@ Array [
expect(startDeps.notifications.toasts.addWarning).not.toBeCalled();
});
describe('getComponent', () => {
it('returns a renderable React component', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
// Have to do some fanagling to get the type system and enzyme to accept this.
// Don't capture the snapshot because it's 600+ lines long.
expect(shallow(React.createElement(() => start.getComponent()))).toBeDefined();
});
});
describe('brand', () => {
it('updates/emits the brand as it changes', async () => {
const service = new ChromeService({ browserSupportsCsp: true });

View file

@ -17,11 +17,12 @@
* under the License.
*/
import React from 'react';
import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import * as Url from 'url';
import { i18n } from '@kbn/i18n';
import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import { IconType } from '@elastic/eui';
import { InjectedMetadataStart } from '../injected_metadata';
@ -32,6 +33,8 @@ import { HttpStart } from '../http';
import { ChromeNavLinks, NavLinksService } from './nav_links';
import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed';
import { NavControlsService, ChromeNavControls } from './nav_controls';
import { LoadingIndicator, Header } from './ui';
import { DocLinksStart } from '../doc_links';
export { ChromeNavControls, ChromeRecentlyAccessed };
@ -71,6 +74,7 @@ interface ConstructorParams {
interface StartDeps {
application: ApplicationStart;
docLinks: DocLinksStart;
http: HttpStart;
injectedMetadata: InjectedMetadataStart;
notifications: NotificationsStart;
@ -90,12 +94,14 @@ export class ChromeService {
public async start({
application,
docLinks,
http,
injectedMetadata,
notifications,
}: StartDeps): Promise<ChromeStart> {
}: StartDeps): Promise<InternalChromeStart> {
const FORCE_HIDDEN = isEmbedParamInHash();
const appTitle$ = new BehaviorSubject<string>('Kibana');
const brand$ = new BehaviorSubject<ChromeBrand>({});
const isVisible$ = new BehaviorSubject(true);
const isCollapsed$ = new BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY));
@ -104,6 +110,10 @@ export class ChromeService {
const breadcrumbs$ = new BehaviorSubject<ChromeBreadcrumb[]>([]);
const badge$ = new BehaviorSubject<ChromeBadge | undefined>(undefined);
const navControls = this.navControls.start();
const navLinks = this.navLinks.start({ application, http });
const recentlyAccessed = await this.recentlyAccessed.start({ http });
if (!this.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
notifications.toasts.addWarning(
i18n.translate('core.chrome.legacyBrowserWarning', {
@ -113,9 +123,39 @@ export class ChromeService {
}
return {
navControls: this.navControls.start(),
navLinks: this.navLinks.start({ application, http }),
recentlyAccessed: await this.recentlyAccessed.start({ http }),
navControls,
navLinks,
recentlyAccessed,
getComponent: () => (
<React.Fragment>
<LoadingIndicator loadingCount$={http.getLoadingCount$()} />
<div className="header-global-wrapper hide-for-sharing" data-test-subj="headerGlobalNav">
<Header
appTitle$={appTitle$.pipe(takeUntil(this.stop$))}
badge$={badge$.pipe(takeUntil(this.stop$))}
basePath={http.basePath}
breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))}
kibanaDocLink={docLinks.links.kibana}
forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()}
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
homeHref={http.basePath.prepend('/app/kibana#/home')}
isVisible$={isVisible$.pipe(
map(visibility => (FORCE_HIDDEN ? false : visibility)),
takeUntil(this.stop$)
)}
kibanaVersion={injectedMetadata.getKibanaVersion()}
navLinks$={navLinks.getNavLinks$()}
recentlyAccessed$={recentlyAccessed.get$()}
navControlsLeft$={navControls.getLeft$()}
navControlsRight$={navControls.getRight$()}
/>
</div>
</React.Fragment>
),
setAppTitle: (appTitle: string) => appTitle$.next(appTitle),
getBrand$: () => brand$.pipe(takeUntil(this.stop$)),
@ -193,7 +233,32 @@ export class ChromeService {
}
}
/** @public */
/**
* ChromeStart allows plugins to customize the global chrome header UI and
* enrich the UX with additional information about the current location of the
* browser.
*
* @remarks
* While ChromeStart exposes many APIs, they should be used sparingly and the
* developer should understand how they affect other plugins and applications.
*
* @example
* How to add a recently accessed item to the sidebar:
* ```ts
* core.chrome.recentlyAccessed.add('/app/map/1234', 'Map 1234', '1234');
* ```
*
* @example
* How to set the help dropdown extension:
* ```tsx
* core.chrome.setHelpExtension(elem => {
* ReactDOM.render(<MyHelpComponent />, elem);
* return () => ReactDOM.unmountComponentAtNode(elem);
* });
* ```
*
* @public
*/
export interface ChromeStart {
/** {@inheritdoc ChromeNavLinks} */
navLinks: ChromeNavLinks;
@ -202,6 +267,15 @@ export interface ChromeStart {
/** {@inheritdoc ChromeRecentlyAccessed} */
recentlyAccessed: ChromeRecentlyAccessed;
/**
* Sets the current app's title
*
* @internalRemarks
* This should be handled by the application service once it is in charge
* of mounting applications.
*/
setAppTitle(appTitle: string): void;
/**
* Get an observable of the current brand information.
*/
@ -294,3 +368,12 @@ export interface ChromeStart {
*/
setHelpExtension(helpExtension?: ChromeHelpExtension): void;
}
/** @internal */
export interface InternalChromeStart extends ChromeStart {
/**
* Used only by MountingService to render the header UI
* @internal
*/
getComponent(): JSX.Element;
}

View file

@ -22,6 +22,7 @@ export {
ChromeBreadcrumb,
ChromeService,
ChromeStart,
InternalChromeStart,
ChromeBrand,
ChromeHelpExtension,
} from './chrome_service';

View file

@ -18,7 +18,7 @@
*/
import { sortBy } from 'lodash';
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
/** @public */
@ -48,6 +48,10 @@ export interface ChromeNavControls {
registerLeft(navControl: ChromeNavControl): void;
/** Register a nav control to be presented on the right side of the chrome header. */
registerRight(navControl: ChromeNavControl): void;
/** @internal */
getLeft$(): Observable<ChromeNavControl[]>;
/** @internal */
getRight$(): Observable<ChromeNavControl[]>;
}
/** @internal */

View file

@ -0,0 +1,2 @@
@import './header/index';
@import './loading_indicator';

View file

@ -1,3 +1,7 @@
$kbnLoadingIndicatorBackgroundSize: $euiSizeXXL * 10;
$kbnLoadingIndicatorColor1: tint($euiColorAccent, 15%);
$kbnLoadingIndicatorColor2: tint($euiColorAccent, 60%);
/**
* 1. Position this loader on top of the content.
* 2. Make sure indicator isn't wider than the screen.

View file

@ -41,8 +41,6 @@ import {
// @ts-ignore
EuiImage,
// @ts-ignore
EuiListGroupItem,
// @ts-ignore
EuiNavDrawer,
// @ts-ignore
EuiNavDrawerGroup,
@ -52,9 +50,6 @@ import {
import { i18n } from '@kbn/i18n';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import chrome from 'ui/chrome';
import { HelpExtension } from 'ui/chrome';
import { relativeToAbsolute } from 'ui/url/relative_to_absolute';
import { HeaderBadge } from './header_badge';
import { HeaderBreadcrumbs } from './header_breadcrumbs';
@ -67,24 +62,43 @@ import {
ChromeNavLink,
ChromeRecentlyAccessedHistoryItem,
ChromeNavControl,
} from '../../../../../../../core/public';
} from '../..';
import { HttpStart } from '../../../http';
import { ChromeHelpExtension } from '../../chrome_service';
// Providing a buffer between the limit and the cut off index
// protects from truncating just the last couple (6) characters
const TRUNCATE_LIMIT: number = 64;
const TRUNCATE_AT: number = 58;
/**
*
* @param {string} url - a relative or root relative url. If a relative path is given then the
* absolute url returned will depend on the current page where this function is called from. For example
* if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get
* back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that
* starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart".
* @return {string} the relative url transformed into an absolute url
*/
function relativeToAbsolute(url: string) {
// convert all link urls to absolute urls
const a = document.createElement('a');
a.setAttribute('href', url);
return a.href;
}
function extendRecentlyAccessedHistoryItem(
navLinks: ChromeNavLink[],
recentlyAccessed: ChromeRecentlyAccessedHistoryItem
recentlyAccessed: ChromeRecentlyAccessedHistoryItem,
basePath: HttpStart['basePath']
) {
const href = relativeToAbsolute(chrome.addBasePath(recentlyAccessed.link));
const href = relativeToAbsolute(basePath.prepend(recentlyAccessed.link));
const navLink = navLinks.find(nl => href.startsWith(nl.subUrlBase || nl.baseUrl));
let titleAndAriaLabel = recentlyAccessed.label;
if (navLink) {
const objectTypeForAriaAppendix = navLink.title;
titleAndAriaLabel = i18n.translate('common.ui.recentLinks.linkItem.screenReaderLabel', {
titleAndAriaLabel = i18n.translate('core.ui.recentLinks.linkItem.screenReaderLabel', {
defaultMessage: '{recentlyAccessedItemLinklabel}, type: {pageType}',
values: {
recentlyAccessedItemLinklabel: recentlyAccessed.label,
@ -131,22 +145,28 @@ function truncateRecentItemLabel(label: string): string {
return label;
}
export type HeaderProps = Pick<Props, Exclude<keyof Props, 'intl'>>;
interface Props {
appTitle?: string;
kibanaVersion: string;
appTitle$: Rx.Observable<string>;
badge$: Rx.Observable<ChromeBadge | undefined>;
breadcrumbs$: Rx.Observable<ChromeBreadcrumb[]>;
homeHref: string;
isVisible$: Rx.Observable<boolean>;
kibanaDocLink: string;
navLinks$: Rx.Observable<ChromeNavLink[]>;
recentlyAccessed$: Rx.Observable<ChromeRecentlyAccessedHistoryItem[]>;
forceAppSwitcherNavigation$: Rx.Observable<boolean>;
helpExtension$: Rx.Observable<HelpExtension>;
helpExtension$: Rx.Observable<ChromeHelpExtension>;
navControlsLeft$: Rx.Observable<ReadonlyArray<ChromeNavControl>>;
navControlsRight$: Rx.Observable<ReadonlyArray<ChromeNavControl>>;
intl: InjectedIntl;
basePath: HttpStart['basePath'];
}
interface State {
appTitle: string;
isVisible: boolean;
navLinks: ReadonlyArray<ReturnType<typeof extendNavLink>>;
recentlyAccessed: ReadonlyArray<ReturnType<typeof extendRecentlyAccessedHistoryItem>>;
@ -163,6 +183,7 @@ class HeaderUI extends Component<Props, State> {
super(props);
this.state = {
appTitle: 'Kibana',
isVisible: true,
navLinks: [],
recentlyAccessed: [],
@ -174,27 +195,29 @@ class HeaderUI extends Component<Props, State> {
public componentDidMount() {
this.subscription = Rx.combineLatest(
this.props.appTitle$,
this.props.isVisible$,
this.props.forceAppSwitcherNavigation$,
this.props.navLinks$,
this.props.recentlyAccessed$,
this.props.navControlsLeft$,
this.props.navControlsRight$
// Types for combineLatest only handle up to 6 inferred types so we combine these two separately.
Rx.combineLatest(this.props.navControlsLeft$, this.props.navControlsRight$)
).subscribe({
next: ([
appTitle,
isVisible,
forceNavigation,
navLinks,
recentlyAccessed,
navControlsLeft,
navControlsRight,
[navControlsLeft, navControlsRight],
]) => {
this.setState({
appTitle,
isVisible,
forceNavigation,
navLinks: navLinks.map(navLink => extendNavLink(navLink)),
recentlyAccessed: recentlyAccessed.map(ra =>
extendRecentlyAccessedHistoryItem(navLinks, ra)
extendRecentlyAccessedHistoryItem(navLinks, ra, this.props.basePath)
),
navControlsLeft,
navControlsRight,
@ -218,7 +241,7 @@ class HeaderUI extends Component<Props, State> {
onClick={this.onNavClick}
href={homeHref}
aria-label={intl.formatMessage({
id: 'common.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel',
id: 'core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel',
defaultMessage: 'Go to home page',
})}
/>
@ -237,8 +260,23 @@ class HeaderUI extends Component<Props, State> {
}
public render() {
const { appTitle, badge$, breadcrumbs$, helpExtension$, intl } = this.props;
const { isVisible, navLinks, recentlyAccessed, navControlsLeft, navControlsRight } = this.state;
const {
badge$,
basePath,
breadcrumbs$,
helpExtension$,
intl,
kibanaDocLink,
kibanaVersion,
} = this.props;
const {
appTitle,
isVisible,
navControlsLeft,
navControlsRight,
navLinks,
recentlyAccessed,
} = this.state;
if (!isVisible) {
return null;
@ -259,7 +297,7 @@ class HeaderUI extends Component<Props, State> {
size="s"
alt=""
aria-hidden={true}
url={chrome.addBasePath(`/${navLink.icon}`)}
url={basePath.prepend(`/${navLink.icon}`)}
/>
) : (
undefined
@ -270,14 +308,14 @@ class HeaderUI extends Component<Props, State> {
const recentLinksArray = [
{
label: intl.formatMessage({
id: 'common.ui.chrome.sideGlobalNav.viewRecentItemsLabel',
id: 'core.ui.chrome.sideGlobalNav.viewRecentItemsLabel',
defaultMessage: 'Recently viewed',
}),
iconType: 'clock',
isDisabled: recentlyAccessed.length > 0 ? false : true,
flyoutMenu: {
title: intl.formatMessage({
id: 'common.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle',
id: 'core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle',
defaultMessage: 'Recent items',
}),
listItems: recentlyAccessed.map(item => ({
@ -310,7 +348,7 @@ class HeaderUI extends Component<Props, State> {
<EuiHeaderSection side="right">
<EuiHeaderSectionItem>
<HeaderHelpMenu helpExtension$={helpExtension$} />
<HeaderHelpMenu {...{ helpExtension$, kibanaDocLink, kibanaVersion }} />
</EuiHeaderSectionItem>
<HeaderNavControls side="right" navControls={navControlsRight} />

View file

@ -21,6 +21,8 @@ import { EuiBetaBadge } from '@elastic/eui';
import React, { Component } from 'react';
import * as Rx from 'rxjs';
import { ChromeBadge } from '../../chrome_service';
interface Props {
badge$: Rx.Observable<ChromeBadge | undefined>;
}
@ -29,8 +31,6 @@ interface State {
badge: ChromeBadge | undefined;
}
import { ChromeBadge } from '../../../../../../../core/public';
export class HeaderBadge extends Component<Props, State> {
private subscription?: Rx.Subscription;

View file

@ -20,7 +20,8 @@
import { mount } from 'enzyme';
import React from 'react';
import * as Rx from 'rxjs';
import { ChromeBreadcrumb } from '../../../../../../../core/public';
import { ChromeBreadcrumb } from '../../chrome_service';
import { HeaderBreadcrumbs } from './header_breadcrumbs';
describe('HeaderBreadcrumbs', () => {

View file

@ -25,8 +25,7 @@ import {
// @ts-ignore
EuiHeaderBreadcrumbs,
} from '@elastic/eui';
import { ChromeBreadcrumb } from '../../../../../../../core/public';
import { ChromeBreadcrumb } from '../../chrome_service';
interface Props {
appTitle?: string;

View file

@ -38,22 +38,21 @@ import {
EuiText,
} from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { HelpExtension } from 'ui/chrome';
import { metadata } from '../../../../metadata';
import { documentationLinks } from '../../../../documentation_links';
import { HeaderExtension } from './header_extension';
import { ChromeHelpExtension } from '../../chrome_service';
interface Props {
helpExtension$: Rx.Observable<HelpExtension>;
helpExtension$: Rx.Observable<ChromeHelpExtension>;
intl: InjectedIntl;
kibanaVersion: string;
useDefaultContent?: boolean;
documentationLink?: string;
kibanaDocLink: string;
}
interface State {
isOpen: boolean;
helpExtension?: HelpExtension;
helpExtension?: ChromeHelpExtension;
}
class HeaderHelpMenuUI extends Component<Props, State> {
@ -86,7 +85,7 @@ class HeaderHelpMenuUI extends Component<Props, State> {
}
public render() {
const { intl, useDefaultContent, documentationLink } = this.props;
const { intl, kibanaVersion, useDefaultContent, kibanaDocLink } = this.props;
const { helpExtension } = this.state;
const defaultContent = useDefaultContent ? (
@ -94,7 +93,7 @@ class HeaderHelpMenuUI extends Component<Props, State> {
<EuiText size="s">
<p>
<FormattedMessage
id="common.ui.chrome.headerGlobalNav.helpMenuHelpDescription"
id="core.ui.chrome.headerGlobalNav.helpMenuHelpDescription"
defaultMessage="Get updates, information, and answers in our documentation."
/>
</p>
@ -102,9 +101,9 @@ class HeaderHelpMenuUI extends Component<Props, State> {
<EuiSpacer />
<EuiButton iconType="popout" href={documentationLink} target="_blank">
<EuiButton iconType="popout" href={kibanaDocLink} target="_blank">
<FormattedMessage
id="common.ui.chrome.headerGlobalNav.helpMenuGoToDocumentation"
id="core.ui.chrome.headerGlobalNav.helpMenuGoToDocumentation"
defaultMessage="Go to documentation"
/>
</EuiButton>
@ -116,7 +115,7 @@ class HeaderHelpMenuUI extends Component<Props, State> {
aria-expanded={this.state.isOpen}
aria-haspopup="true"
aria-label={intl.formatMessage({
id: 'common.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel',
id: 'core.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel',
defaultMessage: 'Help menu',
})}
onClick={this.onMenuButtonClick}
@ -140,15 +139,15 @@ class HeaderHelpMenuUI extends Component<Props, State> {
<EuiFlexGroup responsive={false}>
<EuiFlexItem>
<FormattedMessage
id="common.ui.chrome.headerGlobalNav.helpMenuTitle"
id="core.ui.chrome.headerGlobalNav.helpMenuTitle"
defaultMessage="Help"
/>
</EuiFlexItem>
<EuiFlexItem grow={false} className="chrHeaderHelpMenu__version">
<FormattedMessage
id="common.ui.chrome.headerGlobalNav.helpMenuVersion"
id="core.ui.chrome.headerGlobalNav.helpMenuVersion"
defaultMessage="v {version}"
values={{ version: metadata.version }}
values={{ version: kibanaVersion }}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -179,6 +178,5 @@ class HeaderHelpMenuUI extends Component<Props, State> {
export const HeaderHelpMenu = injectI18n(HeaderHelpMenuUI);
HeaderHelpMenu.defaultProps = {
documentationLink: documentationLinks.kibana,
useDefaultContent: true,
};

View file

@ -25,7 +25,7 @@ import {
} from '@elastic/eui';
import { HeaderExtension } from './header_extension';
import { ChromeNavControl } from '../../../../../../../core/public';
import { ChromeNavControl } from '../../nav_controls';
interface Props {
navControls: ReadonlyArray<ChromeNavControl>;

View file

@ -17,9 +17,4 @@
* under the License.
*/
import { uiRegistry } from './_registry';
export const chromeNavControlsRegistry = uiRegistry({
name: 'chromeNavControls',
order: ['order']
});
export { Header, HeaderProps } from './header';

View file

@ -17,16 +17,5 @@
* under the License.
*/
import './header_global_nav';
export enum NavControlSide {
Left = 'left',
Right = 'right',
}
export interface NavControl {
name: string;
order: number;
side: NavControlSide;
render: (targetDomElement: HTMLDivElement) => (() => void);
}
export { LoadingIndicator } from './loading_indicator';
export { Header } from './header';

View file

@ -19,38 +19,19 @@
import React from 'react';
import { shallow } from 'enzyme';
import chrome from 'ui/chrome';
import { BehaviorSubject } from 'rxjs';
import { LoadingIndicator } from './loading_indicator';
jest.mock('ui/chrome', () => {
return {
loadingCount: {
subscribe: jest.fn(() => {
return () => {};
})
}
};
});
beforeEach(() => {
chrome.loadingCount.subscribe.mockClear();
});
describe('kbnLoadingIndicator', function () {
it('is hidden by default', function () {
const wrapper = shallow(<LoadingIndicator />);
describe('kbnLoadingIndicator', () => {
it('is hidden by default', () => {
const wrapper = shallow(<LoadingIndicator loadingCount$={new BehaviorSubject(0)} />);
expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator-hidden');
expect(wrapper).toMatchSnapshot();
});
it('is visible when loadingCount is > 0', () => {
chrome.loadingCount.subscribe.mockImplementation((fn) => {
fn(1);
return () => {};
});
const wrapper = shallow(<LoadingIndicator />);
const wrapper = shallow(<LoadingIndicator loadingCount$={new BehaviorSubject(1)} />);
expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator');
expect(wrapper).toMatchSnapshot();
});

View file

@ -17,20 +17,25 @@
* under the License.
*/
import 'ngreact';
import React from 'react';
import classNames from 'classnames';
import { Subscription } from 'rxjs';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
import { HttpStart } from '../../http';
export interface LoadingIndicatorProps {
loadingCount$: ReturnType<HttpStart['getLoadingCount$']>;
}
export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { visible: boolean }> {
private loadingCountSubscription?: Subscription;
export class LoadingIndicator extends React.Component {
state = {
visible: false,
};
componentDidMount() {
this._unsub = chrome.loadingCount.subscribe(count => {
this.loadingCountSubscription = this.props.loadingCount$.subscribe(count => {
this.setState({
visible: count > 0,
});
@ -38,8 +43,10 @@ export class LoadingIndicator extends React.Component {
}
componentWillUnmount() {
this._unsub();
this._unsub = null;
if (this.loadingCountSubscription) {
this.loadingCountSubscription.unsubscribe();
this.loadingCountSubscription = undefined;
}
}
render() {
@ -56,7 +63,3 @@ export class LoadingIndicator extends React.Component {
);
}
}
uiModules
.get('app/kibana', ['react'])
.directive('kbnLoadingIndicator', reactDirective => reactDirective(LoadingIndicator));

View file

@ -29,6 +29,7 @@ import { overlayServiceMock } from './overlays/overlay_service.mock';
import { pluginsServiceMock } from './plugins/plugins_service.mock';
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
import { docLinksServiceMock } from './doc_links/doc_links_service.mock';
import { renderingServiceMock } from './rendering/rendering_service.mock';
export const MockLegacyPlatformService = legacyPlatformServiceMock.create();
export const LegacyPlatformServiceConstructor = jest
@ -113,3 +114,9 @@ export const DocLinksServiceConstructor = jest.fn().mockImplementation(() => Moc
jest.doMock('./doc_links', () => ({
DocLinksService: DocLinksServiceConstructor,
}));
export const MockRenderingService = renderingServiceMock.create();
export const RenderingServiceConstructor = jest.fn().mockImplementation(() => MockRenderingService);
jest.doMock('./rendering', () => ({
RenderingService: RenderingServiceConstructor,
}));

View file

@ -39,6 +39,8 @@ import {
UiSettingsServiceConstructor,
MockApplicationService,
MockDocLinksService,
MockRenderingService,
RenderingServiceConstructor,
} from './core_system.test.mocks';
import { CoreSystem } from './core_system';
@ -80,6 +82,7 @@ describe('constructor', () => {
expect(UiSettingsServiceConstructor).toHaveBeenCalledTimes(1);
expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1);
expect(OverlayServiceConstructor).toHaveBeenCalledTimes(1);
expect(RenderingServiceConstructor).toHaveBeenCalledTimes(1);
});
it('passes injectedMetadata param to InjectedMetadataService', () => {
@ -199,11 +202,13 @@ describe('#start()', () => {
await core.start();
}
it('clears the children of the rootDomElement and appends container for legacyPlatform and notifications', async () => {
it('clears the children of the rootDomElement and appends container for rendering service with #kibana-body, notifications, overlays', async () => {
const root = document.createElement('div');
root.innerHTML = '<p>foo bar</p>';
await startCore(root);
expect(root.innerHTML).toBe('<div></div><div></div><div></div>');
expect(root.innerHTML).toMatchInlineSnapshot(
`"<div id=\\"kibana-body\\"></div><div></div><div></div>"`
);
});
it('calls application#start()', async () => {
@ -255,6 +260,15 @@ describe('#start()', () => {
await startCore();
expect(MockOverlayService.start).toHaveBeenCalledTimes(1);
});
it('calls rendering#start()', async () => {
await startCore();
expect(MockRenderingService.start).toHaveBeenCalledTimes(1);
expect(MockRenderingService.start).toHaveBeenCalledWith({
chrome: expect.any(Object),
targetDomElement: expect.any(HTMLElement),
});
});
});
describe('#stop()', () => {
@ -319,25 +333,44 @@ describe('#stop()', () => {
});
});
describe('LegacyPlatform targetDomElement', () => {
it('only mounts the element when start, after setting up the legacyPlatformService', async () => {
describe('RenderingService targetDomElement', () => {
it('only mounts the element when start, after setting up the renderingService', async () => {
const rootDomElement = document.createElement('div');
const core = createCoreSystem({
rootDomElement,
});
let targetDomElementParentInStart: HTMLElement | null;
MockLegacyPlatformService.start.mockImplementation(async ({ targetDomElement }) => {
MockRenderingService.start.mockImplementation(({ targetDomElement }) => {
targetDomElementParentInStart = targetDomElement.parentElement;
return { legacyTargetDomElement: document.createElement('div') };
});
// setting up the core system should mount the targetDomElement as a child of the rootDomElement
// Starting the core system should pass the targetDomElement as a child of the rootDomElement
await core.setup();
await core.start();
expect(targetDomElementParentInStart!).toBe(rootDomElement);
});
});
describe('LegacyPlatformService targetDomElement', () => {
it('only mounts the element when start, after setting up the legacyPlatformService', async () => {
const core = createCoreSystem();
let targetDomElementInStart: HTMLElement | null;
MockLegacyPlatformService.start.mockImplementation(({ targetDomElement }) => {
targetDomElementInStart = targetDomElement;
});
await core.setup();
await core.start();
// Starting the core system should pass the legacyTargetDomElement to the LegacyPlatformService
const renderingLegacyTargetDomElement =
MockRenderingService.start.mock.results[0].value.legacyTargetDomElement;
expect(targetDomElementInStart!).toBe(renderingLegacyTargetDomElement);
});
});
describe('Notifications targetDomElement', () => {
it('only mounts the element when started, after setting up the notificationsService', async () => {
const rootDomElement = document.createElement('div');
@ -353,7 +386,7 @@ describe('Notifications targetDomElement', () => {
}
);
// setting up and starting the core system should mount the targetDomElement as a child of the rootDomElement
// Starting the core system should pass the targetDomElement as a child of the rootDomElement
await core.setup();
await core.start();
expect(targetDomElementParentInStart!).toBe(rootDomElement);

View file

@ -33,6 +33,7 @@ import { UiSettingsService } from './ui_settings';
import { ApplicationService } from './application';
import { mapToObject } from '../utils/';
import { DocLinksService } from './doc_links';
import { RenderingService } from './rendering';
interface Params {
rootDomElement: HTMLElement;
@ -67,6 +68,7 @@ export class CoreSystem {
private readonly plugins: PluginsService;
private readonly application: ApplicationService;
private readonly docLinks: DocLinksService;
private readonly rendering: RenderingService;
private readonly rootDomElement: HTMLElement;
private fatalErrorsSetup: FatalErrorsSetup | null = null;
@ -100,6 +102,7 @@ export class CoreSystem {
this.application = new ApplicationService();
this.chrome = new ChromeService({ browserSupportsCsp });
this.docLinks = new DocLinksService();
this.rendering = new RenderingService();
const core: CoreContext = {};
this.plugins = new PluginsService(core);
@ -157,15 +160,16 @@ export class CoreSystem {
const i18n = await this.i18n.start();
const application = await this.application.start({ injectedMetadata });
const coreUiTargetDomElement = document.createElement('div');
coreUiTargetDomElement.id = 'kibana-body';
const notificationsTargetDomElement = document.createElement('div');
const overlayTargetDomElement = document.createElement('div');
const legacyPlatformTargetDomElement = document.createElement('div');
// ensure the rootDomElement is empty
this.rootDomElement.textContent = '';
this.rootDomElement.classList.add('coreSystemRootDomElement');
this.rootDomElement.appendChild(coreUiTargetDomElement);
this.rootDomElement.appendChild(notificationsTargetDomElement);
this.rootDomElement.appendChild(legacyPlatformTargetDomElement);
this.rootDomElement.appendChild(overlayTargetDomElement);
const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement });
@ -176,6 +180,7 @@ export class CoreSystem {
});
const chrome = await this.chrome.start({
application,
docLinks,
http,
injectedMetadata,
notifications,
@ -195,10 +200,14 @@ export class CoreSystem {
};
const plugins = await this.plugins.start(core);
const rendering = this.rendering.start({
chrome,
targetDomElement: coreUiTargetDomElement,
});
await this.legacyPlatform.start({
core,
plugins: mapToObject(plugins.contracts),
targetDomElement: legacyPlatformTargetDomElement,
targetDomElement: rendering.legacyTargetDomElement,
});
} catch (error) {
if (this.fatalErrorsSetup) {

View file

@ -19,6 +19,7 @@
import { HttpService } from './http_service';
import { HttpSetup } from './types';
import { BehaviorSubject } from 'rxjs';
type ServiceSetupMockType = jest.Mocked<HttpSetup> & {
basePath: jest.Mocked<HttpSetup['basePath']>;
@ -39,7 +40,7 @@ const createServiceMock = (): ServiceSetupMockType => ({
remove: jest.fn(),
},
addLoadingCount: jest.fn(),
getLoadingCount$: jest.fn(),
getLoadingCount$: jest.fn().mockReturnValue(new BehaviorSubject(0)),
stop: jest.fn(),
intercept: jest.fn(),
removeAllInterceptors: jest.fn(),

View file

@ -0,0 +1,10 @@
// Functions need to be first, since we use them in our variables and mixin definitions
@import '@elastic/eui/src/global_styling/functions/index';
// Variables come next, and are used in some mixins
@import '@elastic/eui/src/global_styling/variables/index';
// Mixins provide generic code expansion through helpers
@import '@elastic/eui/src/global_styling/mixins/index';
@import './chrome/index';

View file

@ -22,20 +22,21 @@ import {
NotificationsStart,
} from './notifications_service';
import { toastsServiceMock } from './toasts/toasts_service.mock';
import { ToastsApi } from './toasts/toasts_api';
type DeeplyMocked<T> = { [P in keyof T]: jest.Mocked<T[P]> };
const createSetupContractMock = () => {
const setupContract: jest.Mocked<NotificationsSetup> = {
const setupContract: DeeplyMocked<NotificationsSetup> = {
// we have to suppress type errors until decide how to mock es6 class
toasts: (toastsServiceMock.createSetupContract() as unknown) as ToastsApi,
toasts: toastsServiceMock.createSetupContract(),
};
return setupContract;
};
const createStartContractMock = () => {
const startContract: jest.Mocked<NotificationsStart> = {
const startContract: DeeplyMocked<NotificationsStart> = {
// we have to suppress type errors until decide how to mock es6 class
toasts: (toastsServiceMock.createStartContract() as unknown) as ToastsApi,
toasts: toastsServiceMock.createStartContract(),
};
return startContract;
};

View file

@ -17,6 +17,8 @@
* under the License.
*/
import { omit } from 'lodash';
import { DiscoveredPlugin, PluginName } from '../../server';
import { CoreContext } from '../core_system';
import { PluginWrapper } from './plugin';
@ -100,7 +102,7 @@ export function createPluginStartContext<
},
docLinks: deps.docLinks,
http: deps.http,
chrome: deps.chrome,
chrome: omit(deps.chrome, 'getComponent'),
i18n: deps.i18n,
notifications: deps.notifications,
overlays: deps.overlays,

View file

@ -96,6 +96,7 @@ beforeEach(() => {
application: {
capabilities: mockStartDeps.application.capabilities,
},
chrome: omit(mockStartDeps.chrome, 'getComponent'),
};
// Reset these for each test.

View file

@ -85,6 +85,10 @@ export interface ChromeNavControl {
// @public
export interface ChromeNavControls {
// @internal (undocumented)
getLeft$(): Observable<ChromeNavControl[]>;
// @internal (undocumented)
getRight$(): Observable<ChromeNavControl[]>;
registerLeft(navControl: ChromeNavControl): void;
registerRight(navControl: ChromeNavControl): void;
}
@ -139,7 +143,7 @@ export interface ChromeRecentlyAccessedHistoryItem {
link: string;
}
// @public (undocumented)
// @public
export interface ChromeStart {
addApplicationClass(className: string): void;
getApplicationClasses$(): Observable<string[]>;
@ -153,6 +157,7 @@ export interface ChromeStart {
navLinks: ChromeNavLinks;
recentlyAccessed: ChromeRecentlyAccessed;
removeApplicationClass(className: string): void;
setAppTitle(appTitle: string): void;
setBadge(badge?: ChromeBadge): void;
setBrand(brand: ChromeBrand): void;
setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void;

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { RenderingService, RenderingStart } from './rendering_service';

View file

@ -17,29 +17,25 @@
* under the License.
*/
import { chromeNavControlsRegistry } from '../../registry/chrome_nav_controls';
import { uiModules } from '../../modules';
import { RenderingStart, RenderingService } from './rendering_service';
export function kbnAppendChromeNavControls() {
const createStartContractMock = () => {
const setupContract: jest.Mocked<RenderingStart> = {
legacyTargetDomElement: document.createElement('div'),
};
return setupContract;
};
uiModules
.get('kibana')
.directive('kbnChromeAppendNavControls', function (Private) {
return {
template: function ($element) {
const parts = [$element.html()];
const controls = Private(chromeNavControlsRegistry);
type RenderingServiceContract = PublicMethodsOf<RenderingService>;
const createMock = () => {
const mocked: jest.Mocked<RenderingServiceContract> = {
start: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
for (const control of controls.inOrder) {
parts.unshift(
`<!-- nav control ${control.name} -->`,
control.template
);
}
return parts.join('\n');
}
};
});
}
export const renderingServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -0,0 +1,66 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { chromeServiceMock } from '../chrome/chrome_service.mock';
import { RenderingService } from './rendering_service';
describe('RenderingService#start', () => {
const getService = () => {
const rendering = new RenderingService();
const chrome = chromeServiceMock.createStartContract();
chrome.getComponent.mockReturnValue(<div>Hello chrome!</div>);
const targetDomElement = document.createElement('div');
const start = rendering.start({ chrome, targetDomElement });
return { start, targetDomElement };
};
it('renders into provided DOM element', () => {
const { targetDomElement } = getService();
expect(targetDomElement).toMatchInlineSnapshot(`
<div>
<div
class="content"
data-test-subj="kibanaChrome"
>
<div>
Hello chrome!
</div>
<div />
</div>
</div>
`);
});
it('returns a div for the legacy service to render into', () => {
const {
start: { legacyTargetDomElement },
targetDomElement,
} = getService();
legacyTargetDomElement.innerHTML = '<span id="legacy">Hello legacy!</span>';
expect(targetDomElement.querySelector('#legacy')).toMatchInlineSnapshot(`
<span
id="legacy"
>
Hello legacy!
</span>
`);
});
});

View file

@ -0,0 +1,66 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { InternalChromeStart } from '../chrome';
interface StartDeps {
chrome: InternalChromeStart;
targetDomElement: HTMLDivElement;
}
/**
* Renders all Core UI in a single React tree.
*
* @internalRemarks Currently this only renders Chrome UI. Notifications and
* Overlays UI should be moved here as well.
*
* @returns a DOM element for the legacy platform to render into.
*
* @internal
*/
export class RenderingService {
start({ chrome, targetDomElement }: StartDeps) {
const chromeUi = chrome.getComponent();
const legacyRef = React.createRef<HTMLDivElement>();
ReactDOM.render(
<I18nProvider>
<div className="content" data-test-subj="kibanaChrome">
{chromeUi}
<div ref={legacyRef} />
</div>
</I18nProvider>,
targetDomElement
);
return {
legacyTargetDomElement: legacyRef.current!,
};
}
}
/** @internal */
export interface RenderingStart {
legacyTargetDomElement: HTMLDivElement;
}

View file

@ -1,91 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import ngMock from 'ng_mock';
import $ from 'jquery';
import expect from '@kbn/expect';
import { chromeNavControlsRegistry } from '../../registry/chrome_nav_controls';
import { uiRegistry } from '../../registry/_registry';
describe('chrome nav controls', function () {
let compile;
let stubRegistry;
beforeEach(ngMock.module('kibana', function (PrivateProvider) {
stubRegistry = uiRegistry({
order: ['order']
});
PrivateProvider.swap(chromeNavControlsRegistry, stubRegistry);
}));
beforeEach(ngMock.inject(function ($compile, $rootScope) {
compile = function () {
const $el = $('<div kbn-chrome-append-nav-controls>');
$rootScope.$apply();
$compile($el)($rootScope);
return $el;
};
}));
it('injects templates from the ui/registry/chrome_nav_controls registry', function () {
stubRegistry.register(function () {
return {
name: 'control',
order: 100,
template: `<span id="testTemplateEl"></span>`
};
});
const $el = compile();
expect($el.find('#testTemplateEl')).to.have.length(1);
});
it('renders controls in reverse order, assuming that each control will float:right', function () {
stubRegistry.register(function () {
return {
name: 'control2',
order: 2,
template: `<span id="2", class="testControl"></span>`
};
});
stubRegistry.register(function () {
return {
name: 'control1',
order: 1,
template: `<span id="1", class="testControl"></span>`
};
});
stubRegistry.register(function () {
return {
name: 'control3',
order: 3,
template: `<span id="3", class="testControl"></span>`
};
});
const $el = compile();
expect(
$el.find('.testControl')
.toArray()
.map(el => el.id)
).to.eql(['3', '2', '1']);
});
});

View file

@ -2,7 +2,3 @@ $kbnGlobalNavClosedWidth: 53px;
$kbnGlobalNavOpenWidth: 180px;
$kbnGlobalNavLogoHeight: 70px;
$kbnGlobalNavAppIconHeight: $euiSizeXXL + $euiSizeXS;
$kbnLoadingIndicatorBackgroundSize: $euiSizeXXL * 10;
$kbnLoadingIndicatorColor1: tint($euiColorAccent, 15%);
$kbnLoadingIndicatorColor2: tint($euiColorAccent, 60%);

View file

@ -47,6 +47,7 @@ import { initSavedObjectClient } from './api/saved_object_client';
import { initChromeBasePathApi } from './api/base_path';
import { initChromeInjectedVarsApi } from './api/injected_vars';
import { initHelpExtensionApi } from './api/help_extension';
import { npStart } from '../new_platform';
export const chrome = {};
const internals = _.defaults(
@ -80,6 +81,8 @@ initChromeControlsApi(chrome);
templateApi(chrome, internals);
initChromeThemeApi(chrome);
npStart.core.chrome.setAppTitle(chrome.getAppTitle());
const waitForBootstrap = new Promise(resolve => {
chrome.bootstrap = function (targetDomElement) {
// import chrome nav controls and hacks now so that they are executed after
@ -92,8 +95,10 @@ const waitForBootstrap = new Promise(resolve => {
document.body.setAttribute('id', `${internals.app.id}-app`);
chrome.setupAngular();
targetDomElement.setAttribute('id', 'kibana-body');
// targetDomElement.setAttribute('id', 'kibana-body');
targetDomElement.setAttribute('kbn-chrome', 'true');
targetDomElement.setAttribute('ng-class', '{ \'hidden-chrome\': !chrome.getVisible() }');
targetDomElement.className = 'app-wrapper';
angular.bootstrap(targetDomElement, ['kibana']);
resolve(targetDomElement);
};

View file

@ -1,4 +1 @@
@import './kbn_chrome';
@import './loading_indicator';
@import './header_global_nav/index';

View file

@ -1 +0,0 @@
@import './header_global_nav';

View file

@ -1,66 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { uiModules } from '../../../modules';
import { Header } from './components/header';
import { wrapInI18nContext } from 'ui/i18n';
import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
import { npStart } from '../../../new_platform';
import { NavControlSide } from '.';
const module = uiModules.get('kibana');
module.directive('headerGlobalNav', (reactDirective, Private) => {
const newPlatform = npStart.core;
// Continue to support legacy nav controls not registered with the NP.
// NOTE: in future change this needs to be moved out of this directive.
const navControls = Private(chromeHeaderNavControlsRegistry);
(navControls.bySide[NavControlSide.Left] || [])
.forEach(navControl => newPlatform.chrome.navControls.registerLeft({
order: navControl.order,
mount: navControl.render,
}));
(navControls.bySide[NavControlSide.Right] || [])
.forEach(navControl => newPlatform.chrome.navControls.registerRight({
order: navControl.order,
mount: navControl.render,
}));
return reactDirective(wrapInI18nContext(Header), [
// scope accepted by directive, passed in as React props
'appTitle',
],
{},
// angular injected React props
{
isVisible$: newPlatform.chrome.getIsVisible$(),
badge$: newPlatform.chrome.getBadge$(),
breadcrumbs$: newPlatform.chrome.getBreadcrumbs$(),
helpExtension$: newPlatform.chrome.getHelpExtension$(),
navLinks$: newPlatform.chrome.navLinks.getNavLinks$(),
forceAppSwitcherNavigation$: newPlatform.chrome.navLinks.getForceAppSwitcherNavigation$(),
homeHref: newPlatform.http.basePath.prepend('/app/kibana#/home'),
uiCapabilities: newPlatform.application.capabilities,
recentlyAccessed$: newPlatform.chrome.recentlyAccessed.get$(),
navControlsLeft$: newPlatform.chrome.navControls.getLeft$(),
navControlsRight$: newPlatform.chrome.navControls.getRight$(),
});
});

View file

@ -17,13 +17,8 @@
* under the License.
*/
import './header_global_nav';
import { kbnChromeProvider } from './kbn_chrome';
import { kbnAppendChromeNavControls } from './append_nav_controls';
import './loading_indicator';
export function directivesProvider(chrome, internals) {
kbnChromeProvider(chrome, internals);
kbnAppendChromeNavControls(chrome, internals);
}

View file

@ -1,25 +1,13 @@
<div class="content" chrome-context data-test-subj="kibanaChrome">
<kbn-loading-indicator></kbn-loading-indicator>
<div class="app-wrapper-panel">
<kbn-notifications
list="notifList"
></kbn-notifications>
<header-global-nav
class="header-global-wrapper hide-for-sharing"
app-title="chrome.getAppTitle()"
data-test-subj="headerGlobalNav"
></header-global-nav>
<div id="globalBannerList"></div>
<div class="app-wrapper" ng-class="{ 'hidden-chrome': !chrome.getVisible() }">
<div class="app-wrapper-panel">
<kbn-notifications
list="notifList"
></kbn-notifications>
<div id="globalBannerList"></div>
<div
class="application"
ng-class="'tab-' + getFirstPathSegment() + ' ' + chrome.getApplicationClasses()"
ng-view
></div>
</div>
</div>
<div
class="application"
ng-class="'tab-' + getFirstPathSegment() + ' ' + chrome.getApplicationClasses()"
ng-view
></div>
</div>

View file

@ -31,6 +31,8 @@ import {
} from '../../notify';
import { I18nContext } from '../../i18n';
import { npStart } from '../../new_platform';
import { chromeHeaderNavControlsRegistry, NavControlSide } from '../../registry/chrome_header_nav_controls';
export function kbnChromeProvider(chrome, internals) {
@ -55,7 +57,7 @@ export function kbnChromeProvider(chrome, internals) {
},
controllerAs: 'chrome',
controller($scope, $location) {
controller($scope, $location, Private) {
// Notifications
$scope.notifList = notify._notifs;
@ -63,6 +65,19 @@ export function kbnChromeProvider(chrome, internals) {
return $location.path().split('/')[1];
};
// Continue to support legacy nav controls not registered with the NP.
const navControls = Private(chromeHeaderNavControlsRegistry);
(navControls.bySide[NavControlSide.Left] || [])
.forEach(navControl => npStart.core.chrome.navControls.registerLeft({
order: navControl.order,
mount: navControl.render,
}));
(navControls.bySide[NavControlSide.Right] || [])
.forEach(navControl => npStart.core.chrome.navControls.registerRight({
order: navControl.order,
mount: navControl.render,
}));
// Non-scope based code (e.g., React)
// Banners

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { NavControl } from '../chrome/directives/header_global_nav';
import { IndexedArray } from '../indexed_array';
import { uiRegistry, UIRegistry } from './_registry';
@ -25,6 +24,18 @@ interface ChromeHeaderNavControlsRegistryAccessors {
bySide: { [typeName: string]: IndexedArray<NavControl> };
}
export enum NavControlSide {
Left = 'left',
Right = 'right',
}
export interface NavControl {
name: string;
order: number;
side: NavControlSide;
render: (targetDomElement: HTMLDivElement) => (() => void);
}
export type ChromeHeaderNavControlsRegistry = UIRegistry<NavControl> &
ChromeHeaderNavControlsRegistryAccessors;

View file

@ -35,7 +35,16 @@ export const UI_EXPORT_DEFAULTS = {
'moment-timezone$': resolve(ROOT, 'webpackShims/moment-timezone')
},
styleSheetPaths: [],
styleSheetPaths:
['light', 'dark'].map(theme => ({
theme,
localPath: resolve(ROOT, 'src/core/public/index.scss'),
publicPath: `core.${theme}.css`,
urlImports: {
urlBase: 'built_assets/css/',
publicDir: resolve(ROOT, 'src/core/public'),
}
})),
appExtensions: {
fieldFormatEditors: [

View file

@ -1,18 +0,0 @@
<div ng-controller="securityNavController" ng-show="!!user">
<global-nav-link
kbn-route="route"
icon="'plugins/security/images/person.svg'"
eui-icon-type="'user'"
tooltip-content="accountTooltip(user.full_name || user.username)"
label="user.full_name || user.username"
data-test-subj="loggedInUser"
></global-nav-link>
<global-nav-link
kbn-route="'/logout'"
icon="'plugins/security/images/logout.svg'"
eui-icon-type="'exit'"
tooltip-content="formatTooltip(logoutLabel)"
label="logoutLabel"
></global-nav-link>
</div>

View file

@ -5,54 +5,18 @@
*/
import { I18nContext } from 'ui/i18n';
import { i18n } from '@kbn/i18n';
import React from 'react';
import ReactDOM from 'react-dom';
import { constant } from 'lodash';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
import { chromeNavControlsRegistry } from 'ui/registry/chrome_nav_controls';
import template from 'plugins/security/views/nav_control/nav_control.html';
import 'plugins/security/services/shield_user';
import '../account/account';
import { Path } from 'plugins/xpack_main/services/path';
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
import { chromeHeaderNavControlsRegistry, NavControlSide } from 'ui/registry/chrome_header_nav_controls';
import { SecurityNavControl } from './nav_control_component';
import { NavControlSide } from 'ui/chrome/directives/header_global_nav';
chromeNavControlsRegistry.register(constant({
name: 'security',
order: 1000,
template
}));
const module = uiModules.get('security', ['kibana']);
module.controller('securityNavController', ($scope, ShieldUser, globalNavState, kbnBaseUrl, Private) => {
const xpackInfo = Private(XPackInfoProvider);
const showSecurityLinks = xpackInfo.get('features.security.showLinks');
if (Path.isUnauthenticated() || !showSecurityLinks) return;
$scope.user = ShieldUser.getCurrent();
$scope.route = `${kbnBaseUrl}#/account`;
$scope.accountTooltip = (tooltip) => {
// If the sidebar is open and there's no disabled message,
// then we don't need to show the tooltip.
if (globalNavState.isOpen()) {
return;
}
return tooltip;
};
$scope.logoutLabel = i18n.translate('xpack.security.navControl.logoutLabel', {
defaultMessage: 'Logout'
});
});
chromeHeaderNavControlsRegistry.register((ShieldUser, kbnBaseUrl, Private) => ({
name: 'security',

View file

@ -4,7 +4,7 @@ exports[`NavControlPopover renders without crashing 1`] = `
<EuiPopover
anchorPosition="downRight"
button={
<SpacesGlobalNavButton
<SpacesHeaderNavButton
linkIcon={
<SpaceAvatar
announceSpaceName={true}

View file

@ -1,21 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component } from 'react';
import { ButtonProps } from '../types';
export class SpacesGlobalNavButton extends Component<ButtonProps> {
public render() {
return (
<div className="kbnGlobalNavLink">
<button className="kbnGlobalNavLink__anchor" onClick={this.props.toggleSpaceSelector}>
<span className="kbnGlobalNavLink__icon"> {this.props.linkIcon} </span>
<span className="kbnGlobalNavLink__title"> {this.props.linkTitle} </span>
</button>
</div>
);
}
}

View file

@ -1,3 +0,0 @@
<div ng-controller="spacesNavController">
<div id="spacesNavReactRoot" />
</div>

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { constant } from 'lodash';
import { SpacesManager } from 'plugins/spaces/lib/spaces_manager';
// @ts-ignore
import template from 'plugins/spaces/views/nav_control/nav_control.html';
@ -12,27 +11,18 @@ import { NavControlPopover } from 'plugins/spaces/views/nav_control/nav_control_
// @ts-ignore
import { Path } from 'plugins/xpack_main/services/path';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import ReactDOM from 'react-dom';
import { NavControlSide } from 'ui/chrome/directives/header_global_nav';
import { I18nContext } from 'ui/i18n';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
import {
chromeHeaderNavControlsRegistry,
NavControlSide,
} from 'ui/registry/chrome_header_nav_controls';
// @ts-ignore
import { chromeNavControlsRegistry } from 'ui/registry/chrome_nav_controls';
import { Space } from '../../../common/model/space';
import { SpacesGlobalNavButton } from './components/spaces_global_nav_button';
import { SpacesHeaderNavButton } from './components/spaces_header_nav_button';
chromeNavControlsRegistry.register(
constant({
name: 'spaces',
order: 90,
template,
})
);
const module = uiModules.get('spaces_nav', ['kibana']);
export interface SpacesNavState {
@ -42,40 +32,6 @@ export interface SpacesNavState {
let spacesManager: SpacesManager;
module.controller('spacesNavController', ($scope: any, chrome: any, activeSpace: any) => {
const domNode = document.getElementById(`spacesNavReactRoot`);
const spaceSelectorURL = chrome.getInjected('spaceSelectorURL');
spacesManager = new SpacesManager(spaceSelectorURL);
let mounted = false;
$scope.$parent.$watch('isVisible', function isVisibleWatcher(isVisible: boolean) {
if (isVisible && !mounted && !Path.isUnauthenticated()) {
render(
<I18nContext>
<NavControlPopover
spacesManager={spacesManager}
activeSpace={activeSpace}
anchorPosition={'rightCenter'}
buttonClass={SpacesGlobalNavButton}
/>
</I18nContext>,
domNode
);
mounted = true;
}
});
// unmount react on controller destroy
$scope.$on('$destroy', () => {
if (domNode) {
unmountComponentAtNode(domNode);
}
mounted = false;
});
});
module.service('spacesNavState', (activeSpace: any) => {
return {
getActiveSpace: () => {

View file

@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme';
import React from 'react';
import { SpaceAvatar } from '../../components';
import { spacesManagerMock } from '../../lib/mocks';
import { SpacesGlobalNavButton } from './components/spaces_global_nav_button';
import { SpacesHeaderNavButton } from './components/spaces_header_nav_button';
import { NavControlPopover } from './nav_control_popover';
describe('NavControlPopover', () => {
@ -25,7 +25,7 @@ describe('NavControlPopover', () => {
activeSpace={activeSpace}
spacesManager={spacesManager}
anchorPosition={'downRight'}
buttonClass={SpacesGlobalNavButton}
buttonClass={SpacesHeaderNavButton}
/>
);
expect(wrapper).toMatchSnapshot();
@ -56,7 +56,7 @@ describe('NavControlPopover', () => {
activeSpace={activeSpace}
spacesManager={spacesManager}
anchorPosition={'rightCenter'}
buttonClass={SpacesGlobalNavButton}
buttonClass={SpacesHeaderNavButton}
/>
);

View file

@ -235,14 +235,6 @@
"common.ui.chrome.bigUrlWarningNotificationMessage": "{advancedSettingsLink} の {storeInSessionStorageParam} オプションを有効にするか、画面上のビジュアルをシンプルにしてください。",
"common.ui.chrome.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定",
"common.ui.chrome.bigUrlWarningNotificationTitle": "URL が大きく、Kibana の動作が停止する可能性があります",
"common.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "ホームページに移動",
"common.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "ヘルプメニュー",
"common.ui.chrome.headerGlobalNav.helpMenuGoToDocumentation": "ドキュメンテーションに移動",
"common.ui.chrome.headerGlobalNav.helpMenuHelpDescription": "ドキュメンテーションでアップデートや情報、答えが得られます。",
"common.ui.chrome.headerGlobalNav.helpMenuTitle": "ヘルプ",
"common.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}",
"common.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近のアイテム",
"common.ui.chrome.sideGlobalNav.viewRecentItemsLabel": "最近閲覧",
"common.ui.courier.fetch.failedToClearRequestErrorMessage": "返答から未完全または重複のリクエストを消去できませんでした。",
"common.ui.courier.fetch.requestTimedOutNotificationMessage": "リクエストがタイムアウトしたため、データが不完全な可能性があります",
"common.ui.courier.fetch.requestWasAbortedTwiceErrorMessage": "リクエストが 2 度中断されましたか?",
@ -504,7 +496,6 @@
"common.ui.paginateSelectableList.sortByButtonLabeDescendingScreenReaderOnly": "降順",
"common.ui.paginateSelectableList.sortByButtonLabel": "名前",
"common.ui.paginateSelectableList.sortByButtonLabelScreenReaderOnly": "並べ替え基準",
"common.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel}、タイプ: {pageType}",
"common.ui.savedObjectFinder.addNewItemButtonLabel": "新規 {item} を追加",
"common.ui.savedObjectFinder.manageItemsButtonLabel": "{items} の管理",
"common.ui.savedObjectFinder.noMatchesFoundDescription": "一致する {items} が見つかりません。",
@ -632,6 +623,15 @@
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした",
"common.ui.welcomeErrorMessage": "Kibana が正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。",
"common.ui.welcomeMessage": "Kibana を読み込み中",
"core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "ホームページに移動",
"core.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "ヘルプメニュー",
"core.ui.chrome.headerGlobalNav.helpMenuGoToDocumentation": "ドキュメンテーションに移動",
"core.ui.chrome.headerGlobalNav.helpMenuHelpDescription": "ドキュメンテーションでアップデートや情報、答えが得られます。",
"core.ui.chrome.headerGlobalNav.helpMenuTitle": "ヘルプ",
"core.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}",
"core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近のアイテム",
"core.ui.chrome.sideGlobalNav.viewRecentItemsLabel": "最近閲覧",
"core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel}、タイプ: {pageType}",
"data.filter.applyFilters.popupHeader": "適用するフィルターの選択",
"data.filter.applyFiltersPopup.cancelButtonLabel": "キャンセル",
"data.filter.applyFiltersPopup.saveButtonLabel": "適用",
@ -8789,7 +8789,6 @@
"xpack.security.management.users.userNameColumnName": "ユーザー名",
"xpack.security.management.users.usersTitle": "ユーザー",
"xpack.security.management.usersTitle": "ユーザー",
"xpack.security.navControl.logoutLabel": "ログアウト",
"xpack.security.navControlComponent.accountMenuAriaLabel": "アカウントメニュー",
"xpack.security.navControlComponent.editProfileLinkText": "プロフィールを編集",
"xpack.security.navControlComponent.logoutLinkText": "ログアウト",

View file

@ -234,14 +234,6 @@
"common.ui.chrome.bigUrlWarningNotificationMessage": "在“{advancedSettingsLink}”启用“{storeInSessionStorageParam}”选项,或简化屏幕视觉效果。",
"common.ui.chrome.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置",
"common.ui.chrome.bigUrlWarningNotificationTitle": "URL 过大Kibana 可能无法工作",
"common.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "前往主页",
"common.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "帮助菜单",
"common.ui.chrome.headerGlobalNav.helpMenuGoToDocumentation": "前往文档",
"common.ui.chrome.headerGlobalNav.helpMenuHelpDescription": "在我们的文档中获取更新、信息以及答案。",
"common.ui.chrome.headerGlobalNav.helpMenuTitle": "帮助",
"common.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}",
"common.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近项",
"common.ui.chrome.sideGlobalNav.viewRecentItemsLabel": "最近查看",
"common.ui.courier.fetch.failedToClearRequestErrorMessage": "无法从响应中清除不完整或重复的请求。",
"common.ui.courier.fetch.requestTimedOutNotificationMessage": "由于您的请求超时,因此数据可能不完整",
"common.ui.courier.fetch.requestWasAbortedTwiceErrorMessage": "请求已中止两次?",
@ -503,7 +495,6 @@
"common.ui.paginateSelectableList.sortByButtonLabeDescendingScreenReaderOnly": "降序",
"common.ui.paginateSelectableList.sortByButtonLabel": "名称",
"common.ui.paginateSelectableList.sortByButtonLabelScreenReaderOnly": "排序依据",
"common.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel},类型:{pageType}",
"common.ui.savedObjectFinder.addNewItemButtonLabel": "添加新的 {item}",
"common.ui.savedObjectFinder.manageItemsButtonLabel": "管理 {items}",
"common.ui.savedObjectFinder.noMatchesFoundDescription": "未找到任何匹配的 {items}。",
@ -631,6 +622,15 @@
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "无法获取边界",
"common.ui.welcomeErrorMessage": "Kibana 未正确加载。检查服务器输出以了解详情。",
"common.ui.welcomeMessage": "正在加载 Kibana",
"core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "前往主页",
"core.ui.chrome.headerGlobalNav.helpMenuButtonAriaLabel": "帮助菜单",
"core.ui.chrome.headerGlobalNav.helpMenuGoToDocumentation": "前往文档",
"core.ui.chrome.headerGlobalNav.helpMenuHelpDescription": "在我们的文档中获取更新、信息以及答案。",
"core.ui.chrome.headerGlobalNav.helpMenuTitle": "帮助",
"core.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}",
"core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近项",
"core.ui.chrome.sideGlobalNav.viewRecentItemsLabel": "最近查看",
"core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel},类型:{pageType}",
"data.filter.applyFilters.popupHeader": "选择要应用的筛选",
"data.filter.applyFiltersPopup.cancelButtonLabel": "取消",
"data.filter.applyFiltersPopup.saveButtonLabel": "应用",
@ -8791,7 +8791,6 @@
"xpack.security.management.users.userNameColumnName": "用户名",
"xpack.security.management.users.usersTitle": "用户",
"xpack.security.management.usersTitle": "用户",
"xpack.security.navControl.logoutLabel": "注销",
"xpack.security.navControlComponent.accountMenuAriaLabel": "帐户菜单",
"xpack.security.navControlComponent.editProfileLinkText": "编辑配置文件",
"xpack.security.navControlComponent.logoutLinkText": "注销",