mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[7.0] Adds new K7 side navigation (#28940)
* initial commit of K7 side nav * [ui/persistedLog/recentlyAccessed] support observing history items * [globalNav] use real recentlyAccessed and navLink data * fixes header logo and mobile layout * handle hidden links and no recents, use size vars * handle hidden links and no recents, use size vars * adds prop to wrap flyout link text * fix menu scroll for IE * [chrome/headerGlobalNav] fix typescript issues * [chrome/headerGlobalNav] only ignore types that are actually missing * update EUI to 6.6.0 * Revert "update EUI to 6.6.0" This reverts commit 048cbf9157f8fc7410d4cf8e017b210c4548104e. * [chrome] remove HeaderAppMenu component * [ftr/services/appsMemnu] update for new style * [ftr/discover] tweak size of discover tests and size-dependent assertions * [chrome] disable offsets in embed mode * [ftr/discover] try sticking with original screen width * [ftr/discover] try to force screensize before checking axis labels * [ftr/services/appMenu] ensure menu closed after clicking app * uptime app icon, mobile header size removed * [ftr/discover] make assertion resilient to minor changes in vis width * [chrome] add types for nav apis * [chrome/recentlyAccessed] display app icons when possible * [ftr/discover] make a little wider so scroll doesn't interfer with vis width * [ftr/services/globalNav] fix test subject
This commit is contained in:
parent
98a77ab9db
commit
bbc68b0000
16 changed files with 414 additions and 213 deletions
44
src/ui/public/chrome/api/nav.d.ts
vendored
Normal file
44
src/ui/public/chrome/api/nav.d.ts
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { IconType } from '@elastic/eui';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url';
|
||||
|
||||
export interface NavLink {
|
||||
title: string;
|
||||
url: string;
|
||||
subUrlBase: string;
|
||||
id: string;
|
||||
euiIconType: IconType;
|
||||
active: boolean;
|
||||
lastSubUrl?: string;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface ChromeNavLinks {
|
||||
getNavLinks$(): Rx.Observable<NavLink>;
|
||||
getNavLinks(): NavLink[];
|
||||
navLinkExists(id: string): boolean;
|
||||
getNavLinkById(id: string): NavLink;
|
||||
showOnlyById(id: string): void;
|
||||
untrackNavLinksForDeletedSavedObjects(deletedIds: string[]): void;
|
||||
trackSubUrlForApp(linkId: string, parsedKibanaUrl: KibanaParsedUrl): void;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
@import '@elastic/eui/src/components/header/variables';
|
||||
|
||||
.header-global-wrapper {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
|
@ -6,23 +8,19 @@
|
|||
}
|
||||
|
||||
.header-global-wrapper + .app-wrapper:not(.hidden-chrome) {
|
||||
top: 48px;
|
||||
left: 0;
|
||||
top: $euiHeaderChildSize;
|
||||
left: $euiHeaderChildSize;
|
||||
|
||||
// HOTFIX: Temporary fix for flyouts not inside portals
|
||||
// SASSTODO: Find an actual solution
|
||||
.euiFlyout {
|
||||
top: 48px;
|
||||
top: $euiHeaderChildSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile header is smaller
|
||||
@include euiBreakpoint('xs','s') {
|
||||
.header-global-wrapper + .app-wrapper {
|
||||
top: 36px;
|
||||
|
||||
.euiFlyout {
|
||||
top: 36px;
|
||||
}
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
.header-global-wrapper + .app-wrapper:not(.hidden-chrome) {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import {
|
||||
|
@ -30,15 +31,35 @@ import {
|
|||
EuiHeaderSection,
|
||||
// @ts-ignore
|
||||
EuiHeaderSectionItem,
|
||||
// @ts-ignore
|
||||
EuiHeaderSectionItemButton,
|
||||
// @ts-ignore
|
||||
EuiHideFor,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiListGroup,
|
||||
// @ts-ignore
|
||||
EuiListGroupItem,
|
||||
// @ts-ignore
|
||||
EuiNavDrawer,
|
||||
// @ts-ignore
|
||||
EuiNavDrawerFlyout,
|
||||
// @ts-ignore
|
||||
EuiNavDrawerMenu,
|
||||
EuiOutsideClickDetector,
|
||||
// @ts-ignore
|
||||
EuiShowFor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { HeaderAppMenu } from './header_app_menu';
|
||||
import { HeaderBreadcrumbs } from './header_breadcrumbs';
|
||||
import { HeaderNavControls } from './header_nav_controls';
|
||||
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import chrome, { NavLink } from 'ui/chrome';
|
||||
import { RecentlyAccessedHistoryItem } from 'ui/persisted_log';
|
||||
import { ChromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
|
||||
import { NavControlSide, NavLink } from '../';
|
||||
import { relativeToAbsolute } from 'ui/url/relative_to_absolute';
|
||||
import { NavControlSide } from '../';
|
||||
import { Breadcrumb } from '../../../../../../core/public/chrome';
|
||||
|
||||
interface Props {
|
||||
|
@ -47,17 +68,89 @@ interface Props {
|
|||
homeHref: string;
|
||||
isVisible: boolean;
|
||||
navLinks$: Rx.Observable<NavLink[]>;
|
||||
recentlyAccessed$: Rx.Observable<RecentlyAccessedHistoryItem[]>;
|
||||
navControls: ChromeHeaderNavControlsRegistry;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
class HeaderUI extends Component<Props> {
|
||||
function extendRecentlyAccessedHistoryItem(
|
||||
navLinks: NavLink[],
|
||||
recentlyAccessed: RecentlyAccessedHistoryItem
|
||||
) {
|
||||
const href = relativeToAbsolute(chrome.addBasePath(recentlyAccessed.link));
|
||||
const navLink = navLinks.find(nl => href.startsWith(nl.subUrlBase));
|
||||
|
||||
return {
|
||||
...recentlyAccessed,
|
||||
href,
|
||||
euiIconType: navLink ? navLink.euiIconType : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
interface State {
|
||||
isCollapsed: boolean;
|
||||
flyoutIsCollapsed: boolean;
|
||||
flyoutIsAnimating: boolean;
|
||||
navFlyoutTitle: string;
|
||||
navFlyoutContent: [];
|
||||
mobileIsHidden: boolean;
|
||||
showScrollbar: boolean;
|
||||
outsideClickDisabled: boolean;
|
||||
isManagingFocus: boolean;
|
||||
navLinks: NavLink[];
|
||||
recentlyAccessed: Array<ReturnType<typeof extendRecentlyAccessedHistoryItem>>;
|
||||
}
|
||||
|
||||
class HeaderUI extends Component<Props, State> {
|
||||
private subscription?: Rx.Subscription;
|
||||
private timeoutID?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isCollapsed: true,
|
||||
flyoutIsCollapsed: true,
|
||||
flyoutIsAnimating: false,
|
||||
navFlyoutTitle: '',
|
||||
navFlyoutContent: [],
|
||||
mobileIsHidden: true,
|
||||
showScrollbar: false,
|
||||
outsideClickDisabled: true,
|
||||
isManagingFocus: false,
|
||||
navLinks: [],
|
||||
recentlyAccessed: [],
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.subscription = Rx.combineLatest(
|
||||
this.props.navLinks$,
|
||||
this.props.recentlyAccessed$
|
||||
).subscribe({
|
||||
next: ([navLinks, recentlyAccessed]) => {
|
||||
this.setState({
|
||||
navLinks,
|
||||
recentlyAccessed: recentlyAccessed.map(ra =>
|
||||
extendRecentlyAccessedHistoryItem(navLinks, ra)
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public renderLogo() {
|
||||
const { homeHref, intl } = this.props;
|
||||
return (
|
||||
<EuiHeaderLogo
|
||||
iconType="logoKibana"
|
||||
data-test-subj="logo"
|
||||
iconType="logoKibana"
|
||||
href={homeHref}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'common.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel',
|
||||
|
@ -67,8 +160,17 @@ class HeaderUI extends Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderMenuTrigger() {
|
||||
return (
|
||||
<EuiHeaderSectionItemButton aria-label="Toggle side navigation" onClick={this.toggleOpen}>
|
||||
<EuiIcon type="apps" size="m" />
|
||||
</EuiHeaderSectionItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appTitle, breadcrumbs$, isVisible, navControls, navLinks$ } = this.props;
|
||||
const { appTitle, breadcrumbs$, isVisible, navControls } = this.props;
|
||||
const { navLinks, recentlyAccessed } = this.state;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
|
@ -78,25 +180,212 @@ class HeaderUI extends Component<Props> {
|
|||
const rightNavControls = navControls.bySide[NavControlSide.Right];
|
||||
|
||||
return (
|
||||
<EuiHeader>
|
||||
<EuiHeaderSection grow={false}>
|
||||
<EuiHeaderSectionItem border="right">{this.renderLogo()}</EuiHeaderSectionItem>
|
||||
<Fragment>
|
||||
<EuiHeader>
|
||||
<EuiHeaderSection grow={false}>
|
||||
<EuiShowFor sizes={['xs', 's']}>
|
||||
<EuiHeaderSectionItem border="right">{this.renderMenuTrigger()}</EuiHeaderSectionItem>
|
||||
</EuiShowFor>
|
||||
|
||||
<HeaderNavControls navControls={leftNavControls} />
|
||||
</EuiHeaderSection>
|
||||
<EuiHeaderSectionItem border="right">{this.renderLogo()}</EuiHeaderSectionItem>
|
||||
|
||||
<HeaderBreadcrumbs appTitle={appTitle} breadcrumbs$={breadcrumbs$} />
|
||||
<HeaderNavControls navControls={leftNavControls} />
|
||||
</EuiHeaderSection>
|
||||
|
||||
<EuiHeaderSection side="right">
|
||||
<HeaderNavControls navControls={rightNavControls} />
|
||||
<HeaderBreadcrumbs appTitle={appTitle} breadcrumbs$={breadcrumbs$} />
|
||||
|
||||
<EuiHeaderSectionItem>
|
||||
<HeaderAppMenu navLinks$={navLinks$} />
|
||||
</EuiHeaderSectionItem>
|
||||
</EuiHeaderSection>
|
||||
</EuiHeader>
|
||||
<EuiHeaderSection side="right">
|
||||
<HeaderNavControls navControls={rightNavControls} />
|
||||
</EuiHeaderSection>
|
||||
</EuiHeader>
|
||||
<EuiOutsideClickDetector
|
||||
onOutsideClick={() => this.collapseDrawer()}
|
||||
isDisabled={this.state.outsideClickDisabled}
|
||||
>
|
||||
<EuiNavDrawer
|
||||
isCollapsed={this.state.isCollapsed}
|
||||
flyoutIsCollapsed={this.state.flyoutIsCollapsed}
|
||||
flyoutIsAnimating={this.state.flyoutIsAnimating}
|
||||
onMouseOver={this.expandDrawer}
|
||||
onFocus={this.expandDrawer}
|
||||
onBlur={this.focusOut}
|
||||
onMouseLeave={this.collapseDrawer}
|
||||
mobileIsHidden={this.state.mobileIsHidden}
|
||||
showScrollbar={this.state.showScrollbar}
|
||||
data-test-subj={classNames(
|
||||
'navDrawer',
|
||||
this.state.isCollapsed ? 'collapsed' : 'expanded'
|
||||
)}
|
||||
>
|
||||
<EuiNavDrawerMenu id="navDrawerMenu">
|
||||
<EuiListGroup>
|
||||
<EuiListGroupItem
|
||||
label="Recently viewed"
|
||||
iconType="clock"
|
||||
size="s"
|
||||
style={{ color: 'inherit' }}
|
||||
aria-label="Recently viewed items"
|
||||
onClick={() => this.expandFlyout()}
|
||||
isDisabled={recentlyAccessed.length > 0 ? false : true}
|
||||
extraAction={{
|
||||
color: 'subdued',
|
||||
iconType: 'arrowRight',
|
||||
iconSize: 's',
|
||||
'aria-label': 'Expand to view recent apps and objects',
|
||||
onClick: () => this.expandFlyout(),
|
||||
alwaysShow: true,
|
||||
}}
|
||||
/>
|
||||
</EuiListGroup>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiListGroup data-test-subj="appsMenu">
|
||||
{navLinks.map(navLink =>
|
||||
navLink.hidden ? null : (
|
||||
<EuiListGroupItem
|
||||
key={navLink.id}
|
||||
label={navLink.title}
|
||||
href={
|
||||
navLink.lastSubUrl && !navLink.active ? navLink.lastSubUrl : navLink.url
|
||||
}
|
||||
iconType={navLink.euiIconType}
|
||||
size="s"
|
||||
style={{ color: 'inherit' }}
|
||||
aria-label={navLink.title}
|
||||
isActive={navLink.active}
|
||||
data-test-subj="appLink"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</EuiListGroup>
|
||||
</EuiNavDrawerMenu>
|
||||
<EuiNavDrawerFlyout
|
||||
id="navDrawerFlyout"
|
||||
title="Recent items"
|
||||
isCollapsed={this.state.flyoutIsCollapsed}
|
||||
listItems={recentlyAccessed.map(item => ({
|
||||
label: item.label,
|
||||
href: item.href,
|
||||
iconType: item.euiIconType,
|
||||
size: 's',
|
||||
style: { color: 'inherit' },
|
||||
'aria-label': item.label,
|
||||
}))}
|
||||
onMouseLeave={this.collapseFlyout}
|
||||
wrapText={true}
|
||||
/>
|
||||
</EuiNavDrawer>
|
||||
</EuiOutsideClickDetector>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private toggleOpen = () => {
|
||||
this.setState({
|
||||
mobileIsHidden: !this.state.mobileIsHidden,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
outsideClickDisabled: this.state.mobileIsHidden ? true : false,
|
||||
});
|
||||
}, this.getTimeoutMs(350));
|
||||
};
|
||||
|
||||
private expandDrawer = () => {
|
||||
this.setState({ isCollapsed: false });
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
showScrollbar: true,
|
||||
});
|
||||
}, this.getTimeoutMs(350));
|
||||
|
||||
// This prevents the drawer from collapsing when tabbing through children
|
||||
// by clearing the timeout thus cancelling the onBlur event (see focusOut).
|
||||
// This means isManagingFocus remains true as long as a child element
|
||||
// has focus. This is the case since React bubbles up onFocus and onBlur
|
||||
// events from the child elements.
|
||||
|
||||
if (this.timeoutID) {
|
||||
clearTimeout(this.timeoutID);
|
||||
}
|
||||
|
||||
if (!this.state.isManagingFocus) {
|
||||
this.setState({
|
||||
isManagingFocus: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private collapseDrawer = () => {
|
||||
this.setState({
|
||||
flyoutIsAnimating: false,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
isCollapsed: true,
|
||||
flyoutIsCollapsed: true,
|
||||
mobileIsHidden: true,
|
||||
showScrollbar: false,
|
||||
outsideClickDisabled: true,
|
||||
});
|
||||
}, this.getTimeoutMs(350));
|
||||
|
||||
// Scrolls the menu and flyout back to top when the nav drawer collapses
|
||||
setTimeout(() => {
|
||||
const menuEl = document.getElementById('navDrawerMenu');
|
||||
if (menuEl) {
|
||||
menuEl.scrollTop = 0;
|
||||
}
|
||||
|
||||
const flyoutEl = document.getElementById('navDrawerFlyout');
|
||||
if (flyoutEl) {
|
||||
flyoutEl.scrollTop = 0;
|
||||
}
|
||||
}, this.getTimeoutMs(300));
|
||||
};
|
||||
|
||||
private focusOut = () => {
|
||||
// This collapses the drawer when no children have focus (i.e. tabbed out).
|
||||
// In other words, if focus does not bubble up from a child element, then
|
||||
// the drawer will collapse. See the corresponding block in expandDrawer
|
||||
// (called by onFocus) which cancels this operation via clearTimeout.
|
||||
this.timeoutID = setTimeout(() => {
|
||||
if (this.state.isManagingFocus) {
|
||||
this.setState({
|
||||
isManagingFocus: false,
|
||||
});
|
||||
|
||||
this.collapseDrawer();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
private expandFlyout = () => {
|
||||
this.setState(() => ({
|
||||
flyoutIsCollapsed: !this.state.flyoutIsCollapsed,
|
||||
}));
|
||||
|
||||
this.setState({
|
||||
flyoutIsAnimating: true,
|
||||
});
|
||||
};
|
||||
|
||||
private collapseFlyout = () => {
|
||||
this.setState({ flyoutIsAnimating: true });
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
flyoutIsCollapsed: true,
|
||||
});
|
||||
}, this.getTimeoutMs(250));
|
||||
};
|
||||
|
||||
private getTimeoutMs = (defaultTimeout: number) => {
|
||||
const uiSettings = chrome.getUiSettingsClient();
|
||||
return uiSettings.get('accessibility:disableAnimations') ? 0 : defaultTimeout;
|
||||
};
|
||||
}
|
||||
|
||||
export const Header = injectI18n(HeaderUI);
|
||||
|
|
|
@ -1,138 +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 React, { Component } from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import {
|
||||
// TODO: add type annotations
|
||||
// @ts-ignore
|
||||
EuiHeaderSectionItemButton,
|
||||
// @ts-ignore
|
||||
EuiIcon,
|
||||
// @ts-ignore
|
||||
EuiKeyPadMenu,
|
||||
// @ts-ignore
|
||||
EuiKeyPadMenuItem,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { NavLink } from '../';
|
||||
|
||||
interface Props {
|
||||
navLinks$: Rx.Observable<NavLink[]>;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean;
|
||||
navLinks: NavLink[];
|
||||
}
|
||||
|
||||
class HeaderAppMenuUI extends Component<Props, State> {
|
||||
private subscription?: Rx.Subscription;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
navLinks: [],
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.subscription = this.props.navLinks$.subscribe({
|
||||
next: navLinks => {
|
||||
this.setState({ navLinks });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { intl } = this.props;
|
||||
const { navLinks } = this.state;
|
||||
|
||||
const button = (
|
||||
<EuiHeaderSectionItemButton
|
||||
aria-controls="keyPadMenu"
|
||||
aria-expanded={this.state.isOpen}
|
||||
aria-haspopup="true"
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'common.ui.chrome.headerGlobalNav.appMenuButtonAriaLabel',
|
||||
defaultMessage: 'Apps menu',
|
||||
})}
|
||||
onClick={this.onMenuButtonClick}
|
||||
>
|
||||
<EuiIcon type="apps" size="m" />
|
||||
</EuiHeaderSectionItemButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="headerAppMenu"
|
||||
button={button}
|
||||
isOpen={this.state.isOpen}
|
||||
anchorPosition="downRight"
|
||||
// @ts-ignore
|
||||
repositionOnScroll
|
||||
closePopover={this.closeMenu}
|
||||
data-test-subj="appsMenuButton"
|
||||
>
|
||||
<EuiKeyPadMenu id="keyPadMenu" style={{ width: 288 }} data-test-subj="appsMenu">
|
||||
{navLinks.map(this.renderNavLink)}
|
||||
</EuiKeyPadMenu>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
private onMenuButtonClick = () => {
|
||||
this.setState({
|
||||
isOpen: !this.state.isOpen,
|
||||
});
|
||||
};
|
||||
|
||||
private closeMenu = () => {
|
||||
this.setState({
|
||||
isOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
private renderNavLink = (navLink: NavLink) => (
|
||||
<EuiKeyPadMenuItem
|
||||
label={navLink.title}
|
||||
href={navLink.active || !navLink.lastSubUrl ? navLink.url : navLink.lastSubUrl}
|
||||
key={navLink.id}
|
||||
onClick={this.closeMenu}
|
||||
data-test-subj="appLink"
|
||||
>
|
||||
<EuiIcon type={navLink.euiIconType} size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export const HeaderAppMenu = injectI18n(HeaderAppMenuUI);
|
|
@ -26,6 +26,7 @@ import { injectI18nProvider } from '@kbn/i18n/react';
|
|||
const module = uiModules.get('kibana');
|
||||
|
||||
module.directive('headerGlobalNav', (reactDirective, chrome, Private) => {
|
||||
const { recentlyAccessed } = require('ui/persisted_log');
|
||||
const navControls = Private(chromeHeaderNavControlsRegistry);
|
||||
const homeHref = chrome.addBasePath('/app/kibana#/home');
|
||||
|
||||
|
@ -39,6 +40,7 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private) => {
|
|||
{
|
||||
breadcrumbs$: chrome.breadcrumbs.get$(),
|
||||
navLinks$: chrome.getNavLinks$(),
|
||||
recentlyAccessed$: recentlyAccessed.get$(),
|
||||
navControls,
|
||||
homeHref
|
||||
});
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { IconType } from '@elastic/eui';
|
||||
import './header_global_nav';
|
||||
|
||||
export enum NavControlSide {
|
||||
|
@ -31,12 +30,3 @@ export interface NavControl {
|
|||
side: NavControlSide;
|
||||
render: (targetDomElement: HTMLDivElement) => (() => void) | void;
|
||||
}
|
||||
|
||||
export interface NavLink {
|
||||
title: string;
|
||||
url: string;
|
||||
id: string;
|
||||
euiIconType: IconType;
|
||||
lastSubUrl?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
|
6
src/ui/public/chrome/index.d.ts
vendored
6
src/ui/public/chrome/index.d.ts
vendored
|
@ -19,13 +19,13 @@
|
|||
|
||||
import { Brand } from '../../../core/public/chrome';
|
||||
import { BreadcrumbsApi } from './api/breadcrumbs';
|
||||
export { Breadcrumb } from './api/breadcrumbs';
|
||||
import { ChromeNavLinks } from './api/nav';
|
||||
|
||||
interface IInjector {
|
||||
get<T>(injectable: string): T;
|
||||
}
|
||||
|
||||
declare interface Chrome {
|
||||
declare interface Chrome extends ChromeNavLinks {
|
||||
breadcrumbs: BreadcrumbsApi;
|
||||
addBasePath<T = string>(path: T): T;
|
||||
dangerouslyGetActiveInjector(): Promise<IInjector>;
|
||||
|
@ -46,3 +46,5 @@ declare interface Chrome {
|
|||
declare const chrome: Chrome;
|
||||
|
||||
export default chrome;
|
||||
export { Breadcrumb } from './api/breadcrumbs';
|
||||
export { NavLink } from './api/nav';
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Storage } from 'ui/storage';
|
||||
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
|
@ -40,6 +42,8 @@ export class PersistedLog<T = any> {
|
|||
public storage: Storage;
|
||||
public items: T[];
|
||||
|
||||
private update$ = new Rx.BehaviorSubject(undefined);
|
||||
|
||||
constructor(name: string, options: PersistedLogOptions<T> = {}, storage = localStorage) {
|
||||
this.name = name;
|
||||
this.maxLength =
|
||||
|
@ -76,10 +80,15 @@ export class PersistedLog<T = any> {
|
|||
|
||||
// persist the stack
|
||||
this.storage.set(this.name, this.items);
|
||||
this.update$.next(undefined);
|
||||
return this.items;
|
||||
}
|
||||
|
||||
public get() {
|
||||
return _.cloneDeep(this.items);
|
||||
}
|
||||
|
||||
public get$() {
|
||||
return this.update$.pipe(map(() => this.get()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,10 @@ class RecentlyAccessed {
|
|||
public get() {
|
||||
return this.history.get();
|
||||
}
|
||||
|
||||
public get$() {
|
||||
return this.history.get$();
|
||||
}
|
||||
}
|
||||
|
||||
export const recentlyAccessed = new RecentlyAccessed();
|
||||
|
|
|
@ -11,4 +11,7 @@
|
|||
|
||||
-webkit-transition-duration: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
|
||||
-webkit-transition-delay: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import moment from 'moment';
|
||||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
const log = getService('log');
|
||||
|
@ -153,8 +154,13 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.visualize.waitForVisualization();
|
||||
await PageObjects.discover.brushHistogram(0, 1);
|
||||
await PageObjects.visualize.waitForVisualization();
|
||||
const actualTimeString = await PageObjects.header.getPrettyDuration();
|
||||
expect(actualTimeString).to.be('September 19th 2015, 23:59:02.606 to September 20th 2015, 02:56:40.744');
|
||||
const newFromTime = await PageObjects.header.getFromTime();
|
||||
const newToTime = await PageObjects.header.getToTime();
|
||||
|
||||
const newDurationHours = moment.duration(moment(newToTime) - moment(newFromTime)).asHours();
|
||||
if (newDurationHours < 1 || newDurationHours >= 5) {
|
||||
throw new Error(`expected new duration of ${newDurationHours} hours to be between 1 and 5 hours`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should show correct initial chart interval of Auto', async function () {
|
||||
|
|
|
@ -25,7 +25,7 @@ export default function ({ getService, loadTestFile }) {
|
|||
this.tags('ciGroup6');
|
||||
|
||||
before(function () {
|
||||
return browser.setWindowSize(1200, 800);
|
||||
return browser.setWindowSize(1250, 800);
|
||||
});
|
||||
|
||||
after(function unloadMakelogs() {
|
||||
|
|
|
@ -22,11 +22,6 @@ export function HomePageProvider({ getService }) {
|
|||
const retry = getService('retry');
|
||||
|
||||
class HomePage {
|
||||
|
||||
async clickKibanaIcon() {
|
||||
await testSubjects.click('kibanaLogo');
|
||||
}
|
||||
|
||||
async clickSynopsis(title) {
|
||||
await testSubjects.click(`homeSynopsisLink${title}`);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export function AppsMenuProvider({ getService }) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
const flyout = getService('flyout');
|
||||
const globalNav = getService('globalNav');
|
||||
|
||||
return new class AppsMenu {
|
||||
async readLinks() {
|
||||
|
@ -42,38 +42,31 @@ export function AppsMenuProvider({ getService }) {
|
|||
}
|
||||
|
||||
async clickLink(appTitle) {
|
||||
log.debug(`click "${appTitle}" tab`);
|
||||
await this._ensureMenuOpen();
|
||||
const container = await testSubjects.find('appsMenu');
|
||||
const link = await container.findByPartialLinkText(appTitle);
|
||||
await link.click();
|
||||
try {
|
||||
log.debug(`click "${appTitle}" tab`);
|
||||
await this._ensureMenuOpen();
|
||||
const container = await testSubjects.find('appsMenu');
|
||||
const link = await container.findByPartialLinkText(appTitle);
|
||||
await link.click();
|
||||
} finally {
|
||||
await this._ensureMenuClosed();
|
||||
}
|
||||
}
|
||||
|
||||
async _ensureMenuOpen() {
|
||||
// some apps render flyouts that cover the global nav menu, so we make sure all flyouts are
|
||||
// closed before trying to use the appsMenu
|
||||
await flyout.ensureAllClosed();
|
||||
|
||||
if (!await testSubjects.exists('appsMenu')) {
|
||||
await testSubjects.click('appsMenuButton');
|
||||
await retry.waitFor('apps menu displayed', async () => (
|
||||
await testSubjects.exists('appsMenu')
|
||||
if (!await testSubjects.exists('navDrawer&expanded')) {
|
||||
await testSubjects.moveMouseTo('navDrawer');
|
||||
await retry.waitFor('apps drawer open', async () => (
|
||||
await testSubjects.exists('navDrawer&expanded')
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
async _ensureMenuClosed() {
|
||||
const [appsMenuButtonExists, appsMenuExists] = await Promise.all([
|
||||
testSubjects.exists('appsMenuButton'),
|
||||
testSubjects.exists('appsMenu')
|
||||
]);
|
||||
|
||||
if (appsMenuButtonExists && appsMenuExists) {
|
||||
await testSubjects.click('appsMenuButton');
|
||||
await retry.waitFor('user menu closed', async () => (
|
||||
!await testSubjects.exists('appsMenu')
|
||||
));
|
||||
}
|
||||
await globalNav.moveMouseToLogo();
|
||||
await retry.waitFor('apps drawer closed', async () => (
|
||||
await testSubjects.exists('navDrawer&collapsed')
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ export function GlobalNavProvider({ getService }) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
|
||||
return new class GlobalNav {
|
||||
async moveMouseToLogo() {
|
||||
await testSubjects.moveMouseTo('headerGlobalNav logo');
|
||||
}
|
||||
|
||||
async clickLogo() {
|
||||
return await testSubjects.click('headerGlobalNav logo');
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export const uptime = (kibana: any) =>
|
|||
description: 'The description text that will be shown to users in Kibana',
|
||||
}),
|
||||
icon: 'plugins/uptime/icons/heartbeat_white.svg',
|
||||
euiIconType: 'heartbeatApp',
|
||||
euiIconType: 'uptimeApp',
|
||||
title: 'Uptime',
|
||||
main: 'plugins/uptime/app',
|
||||
order: 8900,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue