[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:
Ryan Keairns 2019-01-31 10:13:40 -06:00 committed by GitHub
parent 98a77ab9db
commit bbc68b0000
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 414 additions and 213 deletions

44
src/ui/public/chrome/api/nav.d.ts vendored Normal file
View 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;
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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
});

View file

@ -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;
}

View file

@ -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';

View file

@ -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()));
}
}

View file

@ -51,6 +51,10 @@ class RecentlyAccessed {
public get() {
return this.history.get();
}
public get$() {
return this.history.get$();
}
}
export const recentlyAccessed = new RecentlyAccessed();

View file

@ -11,4 +11,7 @@
-webkit-transition-duration: 0s !important;
transition-duration: 0s !important;
-webkit-transition-delay: 0s !important;
transition-delay: 0s !important;
}

View file

@ -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 () {

View file

@ -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() {

View file

@ -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}`);
}

View file

@ -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')
));
}
};
}

View file

@ -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');
}

View file

@ -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,