Grouped Kibana nav (#53545)

Adds concept of `category` to nav links, grouping them by this in the side nav
This commit is contained in:
Michail Yasonik 2020-01-21 12:48:07 -05:00 committed by GitHub
parent 01fe8afb98
commit da54657b91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 6419 additions and 444 deletions

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppBase](./kibana-plugin-public.appbase.md) &gt; [category](./kibana-plugin-public.appbase.category.md)
## AppBase.category property
The category definition of the product See [AppCategory](./kibana-plugin-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference
<b>Signature:</b>
```typescript
category?: AppCategory;
```

View file

@ -16,6 +16,7 @@ export interface AppBase
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-public.appbase.capabilities.md) | <code>Partial&lt;Capabilities&gt;</code> | Custom capabilities defined by the app. |
| [category](./kibana-plugin-public.appbase.category.md) | <code>AppCategory</code> | The category definition of the product See [AppCategory](./kibana-plugin-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference |
| [chromeless](./kibana-plugin-public.appbase.chromeless.md) | <code>boolean</code> | Hide the UI chrome when the application is mounted. Defaults to <code>false</code>. Takes precedence over chrome service visibility settings. |
| [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) | <code>string</code> | A EUI iconType that will be used for the app's icon. This icon takes precendence over the <code>icon</code> property. |
| [icon](./kibana-plugin-public.appbase.icon.md) | <code>string</code> | A URL to an image file used as an icon. Used as a fallback if <code>euiIconType</code> is not provided. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppCategory](./kibana-plugin-public.appcategory.md) &gt; [ariaLabel](./kibana-plugin-public.appcategory.arialabel.md)
## AppCategory.ariaLabel property
If the visual label isn't appropriate for screen readers, can override it here
<b>Signature:</b>
```typescript
ariaLabel?: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppCategory](./kibana-plugin-public.appcategory.md) &gt; [euiIconType](./kibana-plugin-public.appcategory.euiicontype.md)
## AppCategory.euiIconType property
Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined
<b>Signature:</b>
```typescript
euiIconType?: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppCategory](./kibana-plugin-public.appcategory.md) &gt; [label](./kibana-plugin-public.appcategory.label.md)
## AppCategory.label property
Label used for cateogry name. Also used as aria-label if one isn't set.
<b>Signature:</b>
```typescript
label: string;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppCategory](./kibana-plugin-public.appcategory.md)
## AppCategory interface
A category definition for nav links to know where to sort them in the left hand nav
<b>Signature:</b>
```typescript
export interface AppCategory
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [ariaLabel](./kibana-plugin-public.appcategory.arialabel.md) | <code>string</code> | If the visual label isn't appropriate for screen readers, can override it here |
| [euiIconType](./kibana-plugin-public.appcategory.euiicontype.md) | <code>string</code> | Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined |
| [label](./kibana-plugin-public.appcategory.label.md) | <code>string</code> | Label used for cateogry name. Also used as aria-label if one isn't set. |
| [order](./kibana-plugin-public.appcategory.order.md) | <code>number</code> | The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [AppCategory](./kibana-plugin-public.appcategory.md) &gt; [order](./kibana-plugin-public.appcategory.order.md)
## AppCategory.order property
The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000)
<b>Signature:</b>
```typescript
order?: number;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) &gt; [category](./kibana-plugin-public.chromenavlink.category.md)
## ChromeNavLink.category property
The category the app lives in
<b>Signature:</b>
```typescript
readonly category?: AppCategory;
```

View file

@ -17,6 +17,7 @@ export interface ChromeNavLink
| --- | --- | --- |
| [active](./kibana-plugin-public.chromenavlink.active.md) | <code>boolean</code> | Indicates whether or not this app is currently on the screen. |
| [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) | <code>string</code> | The base route used to open the root of an application. |
| [category](./kibana-plugin-public.chromenavlink.category.md) | <code>AppCategory</code> | The category the app lives in |
| [disabled](./kibana-plugin-public.chromenavlink.disabled.md) | <code>boolean</code> | Disables a link from being clickable. |
| [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) | <code>string</code> | A EUI iconType that will be used for the app's icon. This icon takes precendence over the <code>icon</code> property. |
| [hidden](./kibana-plugin-public.chromenavlink.hidden.md) | <code>boolean</code> | Hides a link from the navigation. |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) &gt; [category](./kibana-plugin-public.legacynavlink.category.md)
## LegacyNavLink.category property
<b>Signature:</b>
```typescript
category?: AppCategory;
```

View file

@ -15,6 +15,7 @@ export interface LegacyNavLink
| Property | Type | Description |
| --- | --- | --- |
| [category](./kibana-plugin-public.legacynavlink.category.md) | <code>AppCategory</code> | |
| [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md) | <code>string</code> | |
| [icon](./kibana-plugin-public.legacynavlink.icon.md) | <code>string</code> | |
| [id](./kibana-plugin-public.legacynavlink.id.md) | <code>string</code> | |

View file

@ -32,6 +32,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| --- | --- |
| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. |
| [AppBase](./kibana-plugin-public.appbase.md) | |
| [AppCategory](./kibana-plugin-public.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav |
| [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.<!-- -->See |
| [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.<!-- -->See |
| [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | |

View file

@ -17,15 +17,13 @@
* under the License.
*/
import { readFile } from 'fs';
import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import { unique } from './core/helper';
import { Translation } from './translation';
const asyncReadFile = promisify(readFile);
const TRANSLATION_FILE_EXTENSION = '.json';
/**
@ -69,7 +67,8 @@ function getLocaleFromFileName(fullFileName: string) {
* @returns
*/
async function loadFile(pathToFile: string): Promise<Translation> {
return JSON.parse(await asyncReadFile(pathToFile, 'utf8'));
// doing this at the moment because fs is mocked in a lot of places where this would otherwise fail
return JSON.parse(await promisify(fs.readFile)(pathToFile, 'utf8'));
}
/**

View file

@ -31,6 +31,7 @@ import { PluginOpaqueId } from '../plugins';
import { IUiSettingsClient } from '../ui_settings';
import { RecursiveReadonly } from '../../utils';
import { SavedObjectsStart } from '../saved_objects';
import { AppCategory } from '../../types';
/** @public */
export interface AppBase {
@ -44,6 +45,13 @@ export interface AppBase {
*/
title: string;
/**
* The category definition of the product
* See {@link AppCategory}
* See DEFAULT_APP_CATEGORIES for more reference
*/
category?: AppCategory;
/**
* The initial status of the application.
* Defaulting to `accessible`

View file

@ -29,6 +29,7 @@ import { notificationServiceMock } from '../notifications/notifications_service.
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
import { ChromeService } from './chrome_service';
import { App } from '../application';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
class FakeApp implements App {
public title = `${this.id} App`;
@ -51,6 +52,7 @@ function defaultStartDeps(availableApps?: App[]) {
http: httpServiceMock.createStartContract(),
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
};
if (availableApps) {

View file

@ -38,7 +38,7 @@ import { LoadingIndicator, HeaderWrapper as Header } from './ui';
import { DocLinksStart } from '../doc_links';
import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu';
import { KIBANA_ASK_ELASTIC_LINK } from './constants';
import { IUiSettingsClient } from '../ui_settings';
export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle };
const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed';
@ -85,6 +85,7 @@ interface StartDeps {
http: HttpStart;
injectedMetadata: InjectedMetadataStart;
notifications: NotificationsStart;
uiSettings: IUiSettingsClient;
}
/** @internal */
@ -139,6 +140,7 @@ export class ChromeService {
http,
injectedMetadata,
notifications,
uiSettings,
}: StartDeps): Promise<InternalChromeStart> {
this.initVisibility(application);
@ -173,7 +175,6 @@ export class ChromeService {
getHeaderComponent: () => (
<React.Fragment>
<LoadingIndicator loadingCount$={http.getLoadingCount$()} />
<Header
application={application}
appTitle$={appTitle$.pipe(takeUntil(this.stop$))}
@ -192,6 +193,7 @@ export class ChromeService {
recentlyAccessed$={recentlyAccessed.get$()}
navControlsLeft$={navControls.getLeft$()}
navControlsRight$={navControls.getRight$()}
navSetting$={uiSettings.get$('pageNavigation')}
/>
</React.Fragment>
),

View file

@ -18,6 +18,7 @@
*/
import { pick } from '../../../utils';
import { AppCategory } from '../../';
/**
* @public
@ -33,6 +34,11 @@ export interface ChromeNavLink {
*/
readonly title: string;
/**
* The category the app lives in
*/
readonly category?: AppCategory;
/**
* The base route used to open the root of an application.
*/

File diff suppressed because it is too large Load diff

View file

@ -17,141 +17,40 @@
* under the License.
*/
import Url from 'url';
import React, { Component, createRef } from 'react';
import * as Rx from 'rxjs';
import {
// TODO: add type annotations
EuiHeader,
EuiHeaderLogo,
EuiHeaderSection,
EuiHeaderSectionItem,
EuiHeaderSectionItemButton,
EuiHorizontalRule,
EuiIcon,
EuiImage,
// @ts-ignore
EuiNavDrawer,
// @ts-ignore
EuiNavDrawerGroup,
// @ts-ignore
EuiShowFor,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { HeaderBadge } from './header_badge';
import { HeaderBreadcrumbs } from './header_breadcrumbs';
import { HeaderHelpMenu } from './header_help_menu';
import { HeaderNavControls } from './header_nav_controls';
import React, { Component, createRef } from 'react';
import * as Rx from 'rxjs';
import {
ChromeBadge,
ChromeBreadcrumb,
ChromeNavControl,
ChromeNavLink,
ChromeRecentlyAccessedHistoryItem,
ChromeNavControl,
} from '../..';
import { InternalApplicationStart } from '../../../application/types';
import { HttpStart } from '../../../http';
import { ChromeHelpExtension } from '../../chrome_service';
import { InternalApplicationStart } from '../../../application/types';
import { HeaderBadge } from './header_badge';
import { NavSetting, OnIsLockedUpdate } from './';
import { HeaderBreadcrumbs } from './header_breadcrumbs';
import { HeaderHelpMenu } from './header_help_menu';
import { HeaderNavControls } from './header_nav_controls';
import { euiNavLink } from './nav_link';
import { HeaderLogo } from './header_logo';
import { NavDrawer } from './nav_drawer';
// 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,
basePath: HttpStart['basePath']
) {
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('core.ui.recentLinks.linkItem.screenReaderLabel', {
defaultMessage: '{recentlyAccessedItemLinklabel}, type: {pageType}',
values: {
recentlyAccessedItemLinklabel: recentlyAccessed.label,
pageType: objectTypeForAriaAppendix,
},
});
}
return {
...recentlyAccessed,
href,
euiIconType: navLink ? navLink.euiIconType : undefined,
title: titleAndAriaLabel,
};
}
function extendNavLink(navLink: ChromeNavLink) {
if (navLink.legacy) {
return {
...navLink,
href: navLink.url && !navLink.active ? navLink.url : navLink.baseUrl,
};
}
return {
...navLink,
href: navLink.baseUrl,
};
}
function isModifiedEvent(event: MouseEvent) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void {
let current = element;
while (current) {
if (current.tagName === 'A') {
return current as HTMLAnchorElement;
}
if (!current.parentElement || current.parentElement === document.body) {
return undefined;
}
current = current.parentElement;
}
}
function truncateRecentItemLabel(label: string): string {
if (label.length > TRUNCATE_LIMIT) {
label = `${label.substring(0, TRUNCATE_AT)}`;
}
return label;
}
export type HeaderProps = Pick<Props, Exclude<keyof Props, 'intl'>>;
interface Props {
export interface HeaderProps {
kibanaVersion: string;
application: InternalApplicationStart;
appTitle$: Rx.Observable<string>;
@ -168,28 +67,29 @@ interface Props {
legacyMode: boolean;
navControlsLeft$: Rx.Observable<readonly ChromeNavControl[]>;
navControlsRight$: Rx.Observable<readonly ChromeNavControl[]>;
intl: InjectedIntl;
basePath: HttpStart['basePath'];
isLocked?: boolean;
onIsLockedUpdate?: (isLocked: boolean) => void;
navSetting$: Rx.Observable<NavSetting>;
onIsLockedUpdate?: OnIsLockedUpdate;
}
interface State {
appTitle: string;
currentAppId?: string;
isVisible: boolean;
navLinks: ReadonlyArray<ReturnType<typeof extendNavLink>>;
recentlyAccessed: ReadonlyArray<ReturnType<typeof extendRecentlyAccessedHistoryItem>>;
navLinks: ChromeNavLink[];
recentlyAccessed: ChromeRecentlyAccessedHistoryItem[];
forceNavigation: boolean;
navControlsLeft: readonly ChromeNavControl[];
navControlsRight: readonly ChromeNavControl[];
navSetting: NavSetting;
currentAppId: string | undefined;
}
class HeaderUI extends Component<Props, State> {
export class Header extends Component<HeaderProps, State> {
private subscription?: Rx.Subscription;
private navDrawerRef = createRef<EuiNavDrawer>();
constructor(props: Props) {
constructor(props: HeaderProps) {
super(props);
this.state = {
@ -200,6 +100,8 @@ class HeaderUI extends Component<Props, State> {
forceNavigation: false,
navControlsLeft: [],
navControlsRight: [],
navSetting: 'grouped',
currentAppId: '',
};
}
@ -214,7 +116,8 @@ class HeaderUI extends Component<Props, State> {
Rx.combineLatest(
this.props.navControlsLeft$,
this.props.navControlsRight$,
this.props.application.currentAppId$
this.props.application.currentAppId$,
this.props.navSetting$
)
).subscribe({
next: ([
@ -223,18 +126,17 @@ class HeaderUI extends Component<Props, State> {
forceNavigation,
navLinks,
recentlyAccessed,
[navControlsLeft, navControlsRight, currentAppId],
[navControlsLeft, navControlsRight, currentAppId, navSetting],
]) => {
this.setState({
appTitle,
isVisible,
forceNavigation,
navLinks: navLinks.map(extendNavLink),
recentlyAccessed: recentlyAccessed.map(ra =>
extendRecentlyAccessedHistoryItem(navLinks, ra, this.props.basePath)
),
navLinks: navLinks.filter(navLink => !navLink.hidden),
recentlyAccessed,
navControlsLeft,
navControlsRight,
navSetting,
currentAppId,
});
},
@ -247,26 +149,12 @@ class HeaderUI extends Component<Props, State> {
}
}
public renderLogo() {
const { homeHref, intl } = this.props;
return (
<EuiHeaderLogo
data-test-subj="logo"
iconType="logoKibana"
onClick={this.onNavClick}
href={homeHref}
aria-label={intl.formatMessage({
id: 'core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel',
defaultMessage: 'Go to home page',
})}
/>
);
}
public renderMenuTrigger() {
return (
<EuiHeaderSectionItemButton
aria-label="Toggle side navigation"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.toggleSideNavAriaLabel', {
defaultMessage: 'Toggle side navigation',
})}
onClick={() => this.navDrawerRef.current.toggleOpen()}
>
<EuiIcon type="apps" size="m" />
@ -275,98 +163,29 @@ class HeaderUI extends Component<Props, State> {
}
public render() {
const { appTitle, isVisible, navControlsLeft, navControlsRight } = this.state;
const {
application,
badge$,
basePath,
breadcrumbs$,
helpExtension$,
helpSupportUrl$,
intl,
isLocked,
kibanaDocLink,
kibanaVersion,
onIsLockedUpdate,
legacyMode,
} = this.props;
const {
appTitle,
currentAppId,
isVisible,
navControlsLeft,
navControlsRight,
navLinks,
recentlyAccessed,
} = this.state;
const navLinks = this.state.navLinks.map(link =>
euiNavLink(
link,
this.props.legacyMode,
this.state.currentAppId,
this.props.basePath,
this.props.application.navigateToApp
)
);
if (!isVisible) {
return null;
}
const navLinksArray = navLinks
.filter(navLink => !navLink.hidden)
.map(navLink => ({
key: navLink.id,
label: navLink.tooltip ?? navLink.title,
// Use href and onClick to support "open in new tab" and SPA navigation in the same link
href: navLink.href,
onClick: (event: MouseEvent) => {
if (
!legacyMode && // ignore when in legacy mode
!navLink.legacy && // ignore links to legacy apps
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
application.navigateToApp(navLink.id);
}
},
// Legacy apps use `active` property, NP apps should match the current app
isActive: navLink.active || currentAppId === navLink.id,
isDisabled: navLink.disabled,
iconType: navLink.euiIconType,
icon:
!navLink.euiIconType && navLink.icon ? (
<EuiImage
size="s"
alt=""
aria-hidden={true}
url={basePath.prepend(`/${navLink.icon}`)}
/>
) : (
undefined
),
'data-test-subj': 'navDrawerAppsMenuLink',
}));
const recentLinksArray = [
{
label: intl.formatMessage({
id: 'core.ui.chrome.sideGlobalNav.viewRecentItemsLabel',
defaultMessage: 'Recently viewed',
}),
iconType: 'clock',
isDisabled: recentlyAccessed.length > 0 ? false : true,
flyoutMenu: {
title: intl.formatMessage({
id: 'core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle',
defaultMessage: 'Recent items',
}),
listItems: recentlyAccessed.map(item => ({
label: truncateRecentItemLabel(item.label),
title: item.title,
'aria-label': item.title,
href: item.href,
iconType: item.euiIconType,
})),
},
},
];
return (
<header>
<EuiHeader>
@ -375,7 +194,13 @@ class HeaderUI extends Component<Props, State> {
<EuiHeaderSectionItem border="right">{this.renderMenuTrigger()}</EuiHeaderSectionItem>
</EuiShowFor>
<EuiHeaderSectionItem border="right">{this.renderLogo()}</EuiHeaderSectionItem>
<EuiHeaderSectionItem border="right">
<HeaderLogo
href={this.props.homeHref}
forceNavigation={this.state.forceNavigation}
navLinks={navLinks}
/>
</EuiHeaderSectionItem>
<HeaderNavControls side="left" navControls={navControlsLeft} />
</EuiHeaderSection>
@ -399,75 +224,17 @@ class HeaderUI extends Component<Props, State> {
<HeaderNavControls side="right" navControls={navControlsRight} />
</EuiHeaderSection>
</EuiHeader>
<EuiNavDrawer
<NavDrawer
navSetting={this.state.navSetting}
isLocked={this.props.isLocked}
onIsLockedUpdate={this.props.onIsLockedUpdate}
navLinks={navLinks}
chromeNavLinks={this.state.navLinks}
recentlyAccessedItems={this.state.recentlyAccessed}
basePath={this.props.basePath}
ref={this.navDrawerRef}
data-test-subj="navDrawer"
isLocked={isLocked}
onIsLockedUpdate={onIsLockedUpdate}
aria-label={i18n.translate('core.ui.primaryNav.screenReaderLabel', {
defaultMessage: 'Primary',
})}
>
<EuiNavDrawerGroup
listItems={recentLinksArray}
aria-label={i18n.translate('core.ui.recentLinks.screenReaderLabel', {
defaultMessage: 'Recently viewed links, navigation',
})}
/>
<EuiHorizontalRule margin="none" />
<EuiNavDrawerGroup
data-test-subj="navDrawerAppsMenu"
listItems={navLinksArray}
aria-label={i18n.translate('core.ui.primaryNavList.screenReaderLabel', {
defaultMessage: 'Primary navigation links',
})}
/>
</EuiNavDrawer>
/>
</header>
);
}
private onNavClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
const anchor = findClosestAnchor((event as any).nativeEvent.target);
if (!anchor) {
return;
}
const navLink = this.state.navLinks.find(item => item.href === anchor.href);
if (navLink && navLink.disabled) {
event.preventDefault();
return;
}
if (
!this.state.forceNavigation ||
event.isDefaultPrevented() ||
event.altKey ||
event.metaKey ||
event.ctrlKey
) {
return;
}
const toParsed = Url.parse(anchor.href);
const fromParsed = Url.parse(document.location.href);
const sameProto = toParsed.protocol === fromParsed.protocol;
const sameHost = toParsed.host === fromParsed.host;
const samePath = toParsed.path === fromParsed.path;
if (sameProto && sameHost && samePath) {
if (toParsed.hash) {
document.location.reload();
}
// event.preventDefault() keeps the browser from seeing the new url as an update
// and even setting window.location does not mimic that behavior, so instead
// we use stopPropagation() to prevent angular from seeing the click and
// starting a digest cycle/attempting to handle it in the router.
event.stopPropagation();
}
};
}
export const Header = injectI18n(HeaderUI);

View file

@ -0,0 +1,104 @@
/*
* 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 Url from 'url';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiHeaderLogo } from '@elastic/eui';
import { NavLink } from './nav_link';
function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void {
let current = element;
while (current) {
if (current.tagName === 'A') {
return current as HTMLAnchorElement;
}
if (!current.parentElement || current.parentElement === document.body) {
return undefined;
}
current = current.parentElement;
}
}
function onClick(
event: React.MouseEvent<HTMLAnchorElement>,
forceNavigation: boolean,
navLinks: NavLink[]
) {
const anchor = findClosestAnchor((event as any).nativeEvent.target);
if (!anchor) {
return;
}
const navLink = navLinks.find(item => item.href === anchor.href);
if (navLink && navLink.isDisabled) {
event.preventDefault();
return;
}
if (
!forceNavigation ||
event.isDefaultPrevented() ||
event.altKey ||
event.metaKey ||
event.ctrlKey
) {
return;
}
const toParsed = Url.parse(anchor.href);
const fromParsed = Url.parse(document.location.href);
const sameProto = toParsed.protocol === fromParsed.protocol;
const sameHost = toParsed.host === fromParsed.host;
const samePath = toParsed.path === fromParsed.path;
if (sameProto && sameHost && samePath) {
if (toParsed.hash) {
document.location.reload();
}
// event.preventDefault() keeps the browser from seeing the new url as an update
// and even setting window.location does not mimic that behavior, so instead
// we use stopPropagation() to prevent angular from seeing the click and
// starting a digest cycle/attempting to handle it in the router.
event.stopPropagation();
}
}
interface Props {
href: string;
navLinks: NavLink[];
forceNavigation: boolean;
}
export function HeaderLogo({ href, forceNavigation, navLinks }: Props) {
return (
<EuiHeaderLogo
data-test-subj="logo"
iconType="logoKibana"
onClick={e => onClick(e, forceNavigation, navLinks)}
href={href}
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', {
defaultMessage: 'Go to home page',
})}
/>
);
}

View file

@ -26,3 +26,5 @@ export {
ChromeHelpExtensionMenuDocumentationLink,
ChromeHelpExtensionMenuGitHubLink,
} from './header_help_menu';
export type NavSetting = 'grouped' | 'individual';
export type OnIsLockedUpdate = (isLocked: boolean) => void;

View file

@ -0,0 +1,103 @@
/*
* 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 { cloneDeep } from 'lodash';
import { mount } from 'enzyme';
import React from 'react';
import { NavSetting } from './';
import { ChromeNavLink } from '../../../';
import { AppCategory } from 'src/core/types';
import { DEFAULT_APP_CATEGORIES } from '../../../../utils';
import { NavDrawer } from './nav_drawer';
import { euiNavLink } from './nav_link';
const { analyze, management, observability, security } = DEFAULT_APP_CATEGORIES;
const mockIBasePath = {
get: () => '/app',
prepend: () => '/app',
remove: () => '/app',
};
const getMockProps = (chromeNavLinks: ChromeNavLink[], navSetting: NavSetting = 'grouped') => ({
navSetting,
navLinks: chromeNavLinks.map(link =>
euiNavLink(link, true, undefined, mockIBasePath, () => Promise.resolve())
),
chromeNavLinks,
recentlyAccessedItems: [],
basePath: mockIBasePath,
});
const makeLink = (id: string, order: number, category?: AppCategory) => ({
id,
category,
order,
title: id,
baseUrl: `http://localhost:5601/app/${id}`,
legacy: true,
});
const getMockChromeNavLink = () =>
cloneDeep([
makeLink('discover', 100, analyze),
makeLink('siem', 500, security),
makeLink('metrics', 600, observability),
makeLink('monitoring', 800, management),
makeLink('visualize', 200, analyze),
makeLink('dashboard', 300, analyze),
makeLink('canvas', 400, { label: 'customCategory' }),
makeLink('logs', 700, observability),
]);
describe('NavDrawer', () => {
describe('Advanced setting set to individual', () => {
it('renders individual items', () => {
const component = mount(
<NavDrawer {...getMockProps(getMockChromeNavLink(), 'individual')} />
);
expect(component).toMatchSnapshot();
});
});
describe('Advanced setting set to grouped', () => {
it('renders individual items if there are less than 7', () => {
const links = getMockChromeNavLink().slice(0, 5);
const component = mount(<NavDrawer {...getMockProps(links)} />);
expect(component).toMatchSnapshot();
});
it('renders individual items if there is only 1 category', () => {
// management doesn't count as a category
const navLinks = [
makeLink('discover', 100, analyze),
makeLink('siem', 500, analyze),
makeLink('metrics', 600, analyze),
makeLink('monitoring', 800, analyze),
makeLink('visualize', 200, analyze),
makeLink('dashboard', 300, management),
makeLink('canvas', 400, management),
makeLink('logs', 700, management),
];
const component = mount(<NavDrawer {...getMockProps(navLinks)} />);
expect(component).toMatchSnapshot();
});
it('renders grouped items', () => {
const component = mount(<NavDrawer {...getMockProps(getMockChromeNavLink())} />);
expect(component).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,170 @@
/*
* 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 { groupBy, sortBy } from 'lodash';
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui';
import { NavSetting, OnIsLockedUpdate } from './';
import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..';
import { AppCategory } from '../../../../types';
import { HttpStart } from '../../../http';
import { NavLink } from './nav_link';
import { RecentLinks } from './recent_links';
function getAllCategories(allCategorizedLinks: Record<string, NavLink[]>) {
const allCategories = {} as Record<string, AppCategory | undefined>;
for (const [key, value] of Object.entries(allCategorizedLinks)) {
allCategories[key] = value[0].category;
}
return allCategories;
}
function getOrderedCategories(
mainCategories: Record<string, NavLink[]>,
categoryDictionary: ReturnType<typeof getAllCategories>
) {
return sortBy(
Object.keys(mainCategories),
categoryName => categoryDictionary[categoryName]?.order
);
}
export interface Props {
navSetting: NavSetting;
isLocked?: boolean;
onIsLockedUpdate?: OnIsLockedUpdate;
navLinks: NavLink[];
chromeNavLinks: ChromeNavLink[];
recentlyAccessedItems: ChromeRecentlyAccessedHistoryItem[];
basePath: HttpStart['basePath'];
}
function navDrawerRenderer(
{
navSetting,
isLocked,
onIsLockedUpdate,
navLinks,
chromeNavLinks,
recentlyAccessedItems,
basePath,
}: Props,
ref: React.Ref<HTMLElement>
) {
const disableGroupedNavSetting = navSetting === 'individual';
const groupedNavLinks = groupBy(navLinks, link => link?.category?.label);
const { undefined: unknowns, ...allCategorizedLinks } = groupedNavLinks;
const { Management: management, ...mainCategories } = allCategorizedLinks;
const categoryDictionary = getAllCategories(allCategorizedLinks);
const orderedCategories = getOrderedCategories(mainCategories, categoryDictionary);
const showUngroupedNav =
disableGroupedNavSetting || navLinks.length < 7 || Object.keys(mainCategories).length === 1;
return (
<EuiNavDrawer
ref={ref}
data-test-subj="navDrawer"
isLocked={isLocked}
onIsLockedUpdate={onIsLockedUpdate}
aria-label={i18n.translate('core.ui.primaryNav.screenReaderLabel', {
defaultMessage: 'Primary',
})}
>
{RecentLinks({
recentlyAccessedItems,
navLinks: chromeNavLinks,
basePath,
})}
<EuiHorizontalRule margin="none" />
{showUngroupedNav ? (
<EuiNavDrawerGroup
data-test-subj="navDrawerAppsMenu"
listItems={navLinks}
aria-label={i18n.translate('core.ui.primaryNavList.screenReaderLabel', {
defaultMessage: 'Primary navigation links',
})}
/>
) : (
<>
<EuiNavDrawerGroup
data-test-subj="navDrawerAppsMenu"
aria-label={i18n.translate('core.ui.primaryNavList.screenReaderLabel', {
defaultMessage: 'Primary navigation links',
})}
listItems={[
...orderedCategories.map(categoryName => {
const category = categoryDictionary[categoryName]!;
const links = mainCategories[categoryName];
if (links.length === 1) {
return {
...links[0],
label: category.label,
iconType: category.euiIconType || links[0].iconType,
};
}
return {
'data-test-subj': 'navDrawerCategory',
iconType: category.euiIconType,
label: category.label,
flyoutMenu: {
title: category.label,
listItems: sortBy(links, 'order').map(link => {
link['data-test-subj'] = 'navDrawerFlyoutLink';
return link;
}),
},
};
}),
...sortBy(unknowns, 'order'),
]}
/>
<EuiHorizontalRule margin="none" />
<EuiNavDrawerGroup
data-test-subj="navDrawerManagementMenu"
aria-label={i18n.translate('core.ui.managementNavList.screenReaderLabel', {
defaultMessage: 'Management navigation links',
})}
listItems={[
{
label: categoryDictionary.Management!.label,
iconType: categoryDictionary.Management!.euiIconType,
'data-test-subj': 'navDrawerCategory',
flyoutMenu: {
title: categoryDictionary.Management!.label,
listItems: sortBy(management, 'order').map(link => {
link['data-test-subj'] = 'navDrawerFlyoutLink';
return link;
}),
},
},
]}
/>
</>
)}
</EuiNavDrawer>
);
}
export const NavDrawer = React.forwardRef(navDrawerRenderer);

View file

@ -0,0 +1,87 @@
/*
* 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 { EuiImage } from '@elastic/eui';
import { ChromeNavLink, CoreStart } from '../../../';
import { HttpStart } from '../../../http';
function isModifiedEvent(event: MouseEvent) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
function LinkIcon({ url }: { url: string }) {
return <EuiImage size="s" alt="" aria-hidden={true} url={url} />;
}
export type NavLink = ReturnType<typeof euiNavLink>;
export function euiNavLink(
navLink: ChromeNavLink,
legacyMode: boolean,
currentAppId: string | undefined,
basePath: HttpStart['basePath'],
navigateToApp: CoreStart['application']['navigateToApp']
) {
const {
legacy,
url,
active,
baseUrl,
id,
title,
disabled,
euiIconType,
icon,
category,
order,
tooltip,
} = navLink;
let href = navLink.baseUrl;
if (legacy) {
href = url && !active ? url : baseUrl;
}
return {
category,
key: id,
label: tooltip ?? title,
href, // Use href and onClick to support "open in new tab" and SPA navigation in the same link
onClick(event: MouseEvent) {
if (
!legacyMode && // ignore when in legacy mode
!legacy && // ignore links to legacy apps
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
navigateToApp(navLink.id);
}
},
// Legacy apps use `active` property, NP apps should match the current app
isActive: active || currentAppId === id,
isDisabled: disabled,
iconType: euiIconType,
icon: !euiIconType && icon ? <LinkIcon url={basePath.prepend(`/${icon}`)} /> : undefined,
order,
'data-test-subj': 'navDrawerAppsMenuLink',
};
}

View file

@ -0,0 +1,113 @@
/*
* 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 { i18n } from '@kbn/i18n';
// @ts-ignore
import { EuiNavDrawerGroup } from '@elastic/eui';
import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..';
import { HttpStart } from '../../../http';
// 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;
export function truncateRecentItemLabel(label: string): string {
if (label.length > TRUNCATE_LIMIT) {
label = `${label.substring(0, TRUNCATE_AT)}`;
}
return label;
}
/**
* @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) {
const a = document.createElement('a');
a.setAttribute('href', url);
return a.href;
}
function prepareForEUI(
recentlyAccessed: ChromeRecentlyAccessedHistoryItem[],
navLinks: ChromeNavLink[],
basePath: HttpStart['basePath']
) {
return recentlyAccessed.map(({ link, label }) => {
const href = relativeToAbsolute(basePath.prepend(link));
const navLink = navLinks.find(nl => href.startsWith(nl.baseUrl ?? nl.subUrlBase));
let titleAndAriaLabel = label;
if (navLink) {
titleAndAriaLabel = i18n.translate('core.ui.recentLinks.linkItem.screenReaderLabel', {
defaultMessage: '{recentlyAccessedItemLinklabel}, type: {pageType}',
values: {
recentlyAccessedItemLinklabel: label,
pageType: navLink.title,
},
});
}
return {
href,
label: truncateRecentItemLabel(label),
title: titleAndAriaLabel,
'aria-label': titleAndAriaLabel,
iconType: navLink?.euiIconType,
};
});
}
interface Props {
recentlyAccessedItems: ChromeRecentlyAccessedHistoryItem[];
navLinks: ChromeNavLink[];
basePath: HttpStart['basePath'];
}
export function RecentLinks({ recentlyAccessedItems, navLinks, basePath }: Props) {
return (
<EuiNavDrawerGroup
listItems={[
{
label: i18n.translate('core.ui.chrome.sideGlobalNav.viewRecentItemsLabel', {
defaultMessage: 'Recently viewed',
}),
iconType: 'clock',
isDisabled: recentlyAccessedItems.length === 0,
flyoutMenu: {
title: i18n.translate('core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle', {
defaultMessage: 'Recent items',
}),
listItems: prepareForEUI(recentlyAccessedItems, navLinks, basePath),
},
},
]}
aria-label={i18n.translate('core.ui.recentLinks.screenReaderLabel', {
defaultMessage: 'Recently viewed links, navigation',
})}
/>
);
}

View file

@ -239,6 +239,7 @@ export class CoreSystem {
http,
injectedMetadata,
notifications,
uiSettings,
});
application.registerMountContext(this.coreContext.coreId, 'core', () => ({

View file

@ -77,7 +77,8 @@ import {
} from './context';
export { CoreContext, CoreSystem } from './core_system';
export { RecursiveReadonly } from '../utils';
export { RecursiveReadonly, DEFAULT_APP_CATEGORIES } from '../utils';
export { AppCategory } from '../types';
export {
ApplicationSetup,

View file

@ -26,10 +26,12 @@ import {
UserProvidedValues,
} from '../../server/types';
import { deepFreeze } from '../../utils/';
import { AppCategory } from '../';
/** @public */
export interface LegacyNavLink {
id: string;
category?: AppCategory;
title: string;
order: number;
url: string;
@ -52,6 +54,7 @@ export interface InjectedMetadataParams {
buildNumber: number;
branch: string;
basePath: string;
category?: AppCategory;
csp: {
warnLegacyBrowsers: boolean;
};
@ -75,6 +78,7 @@ export interface InjectedMetadataParams {
basePath: string;
serverName: string;
devMode: boolean;
category?: AppCategory;
uiSettings: {
defaults: Record<string, UiSettingsParams>;
user?: Record<string, UserProvidedValues>;

View file

@ -74,6 +74,7 @@ export class LegacyPlatformService {
appUrl: navLink.url,
subUrlBase: navLink.subUrlBase,
linkToLastSubUrl: navLink.linkToLastSubUrl,
category: navLink.category,
})
);

View file

@ -26,6 +26,7 @@ export interface App extends AppBase {
// @public (undocumented)
export interface AppBase {
capabilities?: Partial<Capabilities>;
category?: AppCategory;
chromeless?: boolean;
euiIconType?: string;
icon?: string;
@ -40,6 +41,14 @@ export interface AppBase {
updater$?: Observable<AppUpdater>;
}
// @public
export interface AppCategory {
ariaLabel?: string;
euiIconType?: string;
label: string;
order?: number;
}
// @public
export type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction;
@ -251,6 +260,7 @@ export interface ChromeNavLink {
// @deprecated
readonly active?: boolean;
readonly baseUrl: string;
readonly category?: AppCategory;
// @deprecated
readonly disabled?: boolean;
readonly euiIconType?: string;
@ -410,6 +420,26 @@ export class CoreSystem {
stop(): void;
}
// @internal (undocumented)
export const DEFAULT_APP_CATEGORIES: Readonly<{
analyze: {
label: string;
order: number;
};
observability: {
label: string;
order: number;
};
security: {
label: string;
order: number;
};
management: {
label: string;
euiIconType: string;
};
}>;
// @public (undocumented)
export interface DocLinksStart {
// (undocumented)
@ -746,6 +776,8 @@ export interface LegacyCoreStart extends CoreStart {
// @public (undocumented)
export interface LegacyNavLink {
// (undocumented)
category?: AppCategory;
// (undocumented)
euiIconType?: string;
// (undocumented)

View file

@ -65,6 +65,7 @@ function getUiAppsNavLinks({ uiAppSpecs = [] }: LegacyUiExports, pluginSpecs: Le
return {
id,
category: spec.category,
title: spec.title,
order: typeof spec.order === 'number' ? spec.order : 0,
icon: spec.icon,
@ -79,6 +80,7 @@ function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]
return (uiExports.navLinkSpecs || [])
.map<LegacyNavLink>(spec => ({
id: spec.id,
category: spec.category,
title: spec.title,
order: typeof spec.order === 'number' ? spec.order : 0,
url: spec.url,

View file

@ -139,7 +139,7 @@ export type LegacyNavLinkSpec = Record<string, unknown> & ChromeNavLink;
*/
export type LegacyAppSpec = Pick<
ChromeNavLink,
'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden'
'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden' | 'category'
> & { pluginId?: string; id?: string; listed?: boolean };
/**

View file

@ -0,0 +1,52 @@
/*
* 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.
*/
/** @public */
/**
* A category definition for nav links to know where to sort them in the left hand nav
* @public
*/
export interface AppCategory {
/**
* Label used for cateogry name.
* Also used as aria-label if one isn't set.
*/
label: string;
/**
* If the visual label isn't appropriate for screen readers,
* can override it here
*/
ariaLabel?: string;
/**
* The order that categories will be sorted in
* Prefer large steps between categories to allow for further editing
* (Default categories are in steps of 1000)
*/
order?: number;
/**
* Define an icon to be used for the category
* If the category is only 1 item, and no icon is defined, will default to the product icon
* Defaults to initials if no icon is defined
*/
euiIconType?: string;
}

View file

@ -23,3 +23,4 @@
*/
export * from './core_service';
export * from './capabilities';
export * from './app_category';

View file

@ -0,0 +1,48 @@
/*
* 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 { i18n } from '@kbn/i18n';
/** @internal */
export const DEFAULT_APP_CATEGORIES = Object.freeze({
analyze: {
label: i18n.translate('core.ui.analyzeNavList.label', {
defaultMessage: 'Analyze',
}),
order: 1000,
},
observability: {
label: i18n.translate('core.ui.observabilityNavList.label', {
defaultMessage: 'Observability',
}),
order: 2000,
},
security: {
label: i18n.translate('core.ui.securityNavList.label', {
defaultMessage: 'Security',
}),
order: 3000,
},
management: {
label: i18n.translate('core.ui.managementNavList.label', {
defaultMessage: 'Management',
}),
euiIconType: 'managementApp',
},
});

View file

@ -28,3 +28,4 @@ export * from './pick';
export * from './promise';
export * from './url';
export * from './unset';
export * from './default_app_categories';

View file

@ -34,6 +34,7 @@ import { getUiSettingDefaults } from './ui_setting_defaults';
import { registerCspCollector } from './server/lib/csp_usage_collector';
import { injectVars } from './inject_vars';
import { i18n } from '@kbn/i18n';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
const mkdirAsync = promisify(Fs.mkdir);
@ -83,6 +84,7 @@ export default function(kibana) {
order: -1003,
url: `${kbnBaseUrl}#/discover`,
euiIconType: 'discoverApp',
category: DEFAULT_APP_CATEGORIES.analyze,
},
{
id: 'kibana:visualize',
@ -92,6 +94,7 @@ export default function(kibana) {
order: -1002,
url: `${kbnBaseUrl}#/visualize`,
euiIconType: 'visualizeApp',
category: DEFAULT_APP_CATEGORIES.analyze,
},
{
id: 'kibana:dashboard',
@ -107,6 +110,7 @@ export default function(kibana) {
// to determine what url to use for the app link.
subUrlBase: `${kbnBaseUrl}#/dashboard`,
euiIconType: 'dashboardApp',
category: DEFAULT_APP_CATEGORIES.analyze,
},
{
id: 'kibana:dev_tools',
@ -116,16 +120,18 @@ export default function(kibana) {
order: 9001,
url: '/app/kibana#/dev_tools',
euiIconType: 'devToolsApp',
category: DEFAULT_APP_CATEGORIES.management,
},
{
id: 'kibana:management',
id: 'kibana:stack_management',
title: i18n.translate('kbn.managementTitle', {
defaultMessage: 'Management',
defaultMessage: 'Stack Management',
}),
order: 9003,
url: `${kbnBaseUrl}#/management`,
euiIconType: 'managementApp',
linkToLastSubUrl: false,
category: DEFAULT_APP_CATEGORIES.management,
},
],

View file

@ -39,7 +39,7 @@ const DiscoverFetchError = ({ fetchError }: Props) => {
if (fetchError.lang === 'painless') {
const { chrome } = getServices();
const mangagementUrlObj = chrome.navLinks.get('kibana:management');
const mangagementUrlObj = chrome.navLinks.get('kibana:stack_management');
const managementUrl = mangagementUrlObj ? mangagementUrlObj.url : '';
const url = `${managementUrl}/kibana/index_patterns`;

View file

@ -129,8 +129,8 @@ describe('home', () => {
test('should not render directory entry when showOnHomePage is false', async () => {
const directoryEntry = {
id: 'management',
title: 'Management',
id: 'stack-management',
title: 'Stack Management',
description: 'Your center console for managing the Elastic Stack.',
icon: 'managementApp',
path: 'management_landing_page',

View file

@ -74,7 +74,7 @@ export function updateLandingPage(version) {
<h1>
<FormattedMessage
id="kbn.management.landing.header"
defaultMessage="Kibana {version} management"
defaultMessage="Welcome to Stack Management {version}"
values={{ version }}
/>
</h1>
@ -93,7 +93,7 @@ export function updateLandingPage(version) {
<p>
<FormattedMessage
id="kbn.management.landing.text"
defaultMessage="A full list of tools can be found in the left menu"
defaultMessage="A complete list of apps is in the menu on the left."
/>
</p>
</EuiText>
@ -173,11 +173,11 @@ uiModules.get('apps/management').directive('kbnManagementLanding', function(kbnV
FeatureCatalogueRegistryProvider.register(() => {
return {
id: 'management',
title: i18n.translate('kbn.management.managementLabel', {
defaultMessage: 'Management',
id: 'stack-management',
title: i18n.translate('kbn.stackManagement.managementLabel', {
defaultMessage: 'Stack Management',
}),
description: i18n.translate('kbn.management.managementDescription', {
description: i18n.translate('kbn.stackManagement.managementDescription', {
defaultMessage: 'Your center console for managing the Elastic Stack.',
}),
icon: 'managementApp',

View file

@ -1170,5 +1170,24 @@ export function getUiSettingDefaults() {
category: ['accessibility'],
requiresPageReload: true,
},
pageNavigation: {
name: i18n.translate('kbn.advancedSettings.pageNavigationName', {
defaultMessage: 'Side nav style',
}),
value: 'grouped',
description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', {
defaultMessage: 'Change the style of navigation',
}),
type: 'select',
options: ['grouped', 'individual'],
optionLabels: {
grouped: i18n.translate('kbn.advancedSettings.pageNavigationGrouped', {
defaultMessage: 'Grouped',
}),
individual: i18n.translate('kbn.advancedSettings.pageNavigationIndividual', {
defaultMessage: 'Individual',
}),
},
},
};
}

View file

@ -23,7 +23,7 @@ import { Legacy } from '../../../../kibana';
// eslint-disable-next-line import/no-default-export
export default function ManagementPlugin(kibana: any) {
const config: Legacy.PluginSpecOptions = {
id: 'management',
id: 'stack-management',
publicDir: resolve(__dirname, 'public'),
config: (Joi: any) => {
return Joi.object({

View file

@ -80,4 +80,4 @@ export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings';
* The type name used within the Monitoring index to publish management stats.
* @type {string}
*/
export const KIBANA_MANAGEMENT_STATS_TYPE = 'management';
export const KIBANA_STACK_MANAGEMENT_STATS_TYPE = 'stack_management';

View file

@ -19,7 +19,7 @@
import { Server } from 'hapi';
import { size } from 'lodash';
import { KIBANA_MANAGEMENT_STATS_TYPE } from '../../../common/constants';
import { KIBANA_STACK_MANAGEMENT_STATS_TYPE } from '../../../common/constants';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
import { SavedObjectsClient } from '../../../../../../core/server';
@ -54,7 +54,7 @@ export function registerManagementUsageCollector(
server: any
) {
const collector = usageCollection.makeUsageCollector({
type: KIBANA_MANAGEMENT_STATS_TYPE,
type: KIBANA_STACK_MANAGEMENT_STATS_TYPE,
isReady: () => true,
fetch: createCollectorFetch(server),
});

View file

@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n';
import { Legacy } from 'kibana';
import { LegacyPluginApi, LegacyPluginInitializer } from 'src/legacy/plugin_discovery/types';
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { DEFAULT_APP_CATEGORIES } from '../../../core/utils';
import { plugin } from './server';
import { CustomCoreSetup } from './server/plugin';
@ -60,6 +61,7 @@ const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPl
icon: 'plugins/timelion/icon.svg',
euiIconType: 'timelionApp',
main: 'plugins/timelion/app',
category: DEFAULT_APP_CATEGORIES.analyze,
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
hacks: [resolve(__dirname, 'public/legacy')],

View file

@ -24,6 +24,7 @@ import { Capabilities } from '../../core/server';
import { SavedObjectsSchemaDefinition } from '../../core/server/saved_objects/schema';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { SavedObjectsManagementDefinition } from '../../core/server/saved_objects/management';
import { AppCategory } from '../../core/types';
/**
* Usage
@ -53,6 +54,7 @@ export interface LegacyPluginOptions {
uiExports: Partial<{
app: Partial<{
title: string;
category?: AppCategory;
description: string;
main: string;
icon: string;

View file

@ -20,8 +20,8 @@
import { i18n } from '@kbn/i18n';
export const MANAGEMENT_BREADCRUMB = Object.freeze({
text: i18n.translate('common.ui.management.breadcrumb', {
defaultMessage: 'Management',
text: i18n.translate('common.ui.stackManagement.breadcrumb', {
defaultMessage: 'Stack Management',
}),
href: '#/management',
});

View file

@ -32,6 +32,7 @@ export class UiApp {
hidden,
linkToLastSubUrl,
listed,
category,
url = `/app/${id}`,
} = spec;
@ -46,6 +47,7 @@ export class UiApp {
this._icon = icon;
this._euiIconType = euiIconType;
this._linkToLastSubUrl = linkToLastSubUrl;
this._category = category;
this._hidden = hidden;
this._listed = listed;
this._url = url;
@ -68,6 +70,7 @@ export class UiApp {
euiIconType: this._euiIconType,
url: this._url,
linkToLastSubUrl: this._linkToLastSubUrl,
category: this._category,
});
}
}
@ -115,6 +118,7 @@ export class UiApp {
main: this._main,
navLink: this._navLink,
linkToLastSubUrl: this._linkToLastSubUrl,
category: this._category,
};
}
}

View file

@ -34,6 +34,7 @@ function applySpecDefaults(spec, type, pluginSpec) {
linkToLastSubUrl = true,
listed = !hidden,
url = `/app/${id}`,
category,
} = spec;
if (spec.injectVars) {
@ -61,6 +62,7 @@ function applySpecDefaults(spec, type, pluginSpec) {
linkToLastSubUrl,
listed,
url,
category,
};
}

View file

@ -45,6 +45,7 @@ describe('UiNavLink', () => {
euiIconType: spec.euiIconType,
hidden: spec.hidden,
disabled: spec.disabled,
category: undefined,
// defaults
linkToLastSubUrl: true,

View file

@ -31,6 +31,7 @@ export class UiNavLink {
hidden = false,
disabled = false,
tooltip = '',
category,
} = spec;
this._id = id;
@ -44,6 +45,7 @@ export class UiNavLink {
this._hidden = hidden;
this._disabled = disabled;
this._tooltip = tooltip;
this._category = category;
}
getOrder() {
@ -63,6 +65,7 @@ export class UiNavLink {
hidden: this._hidden,
disabled: this._disabled,
tooltip: this._tooltip,
category: this._category,
};
}
}

View file

@ -161,14 +161,15 @@ export class ManagementSidebarNav extends React.Component<
}
public render() {
const HEADER_ID = 'management-nav-header';
const HEADER_ID = 'stack-management-nav-header';
return (
<>
<EuiScreenReaderOnly>
<h2 id={HEADER_ID}>
{i18n.translate('management.nav.label', {
defaultMessage: 'Management',
// todo
defaultMessage: 'Stack Management',
})}
</h2>
</EuiScreenReaderOnly>

View file

@ -27,7 +27,8 @@ export class LegacyManagementAdapter {
'management',
{
display: i18n.translate('management.displayName', {
defaultMessage: 'Management',
// todo
defaultMessage: 'Stack Management',
}),
},
capabilities
@ -35,6 +36,7 @@ export class LegacyManagementAdapter {
this.main.register('data', {
display: i18n.translate('management.connectDataDisplayName', {
// todo
defaultMessage: 'Connect Data',
}),
order: 0,

View file

@ -64,7 +64,8 @@ export class ManagementApp {
coreStart.chrome.setBreadcrumbs([
{
text: i18n.translate('management.breadcrumb', {
defaultMessage: 'Management',
// todo
defaultMessage: 'Stack Management',
}),
href: '#/management',
},

View file

@ -34,6 +34,7 @@ export default function({ getService, getPageObjects }) {
await esArchiver.load('dashboard/current/kibana');
await kibanaServer.uiSettings.replace({
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
pageNavigation: 'individual',
});
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.preserveCrossAppState();
@ -83,7 +84,7 @@ export default function({ getService, getPageObjects }) {
describe('is false', () => {
before(async () => {
await PageObjects.header.clickManagement();
await PageObjects.header.clickStackManagement();
await PageObjects.settings.clickKibanaSettings();
await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs');
});
@ -98,7 +99,7 @@ export default function({ getService, getPageObjects }) {
});
after(async () => {
await PageObjects.header.clickManagement();
await PageObjects.header.clickStackManagement();
await PageObjects.settings.clickKibanaSettings();
await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs');
await PageObjects.header.clickDashboard();

View file

@ -27,7 +27,7 @@ export default function({ getService, getPageObjects }) {
describe('index pattern filter', function describeIndexTests() {
before(async function() {
// delete .kibana index and then wait for Kibana to re-create it
await kibanaServer.uiSettings.replace({});
await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' });
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaIndexPatterns();
});

View file

@ -23,7 +23,6 @@ export default function({ getService, getPageObjects }) {
const log = getService('log');
const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings']);
// Flaky: https://github.com/elastic/kibana/issues/19743
describe('visualize lab mode', () => {
it('disabling does not break loading saved searches', async () => {
await PageObjects.common.navigateToUrl('discover', '');
@ -36,7 +35,7 @@ export default function({ getService, getPageObjects }) {
log.info('found saved search before toggling enableLabs mode');
// Navigate to advanced setting and disable lab mode
await PageObjects.header.clickManagement();
await PageObjects.header.clickStackManagement();
await PageObjects.settings.clickKibanaSettings();
await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs');
@ -50,7 +49,7 @@ export default function({ getService, getPageObjects }) {
after(async () => {
await PageObjects.discover.closeLoadSaveSearchPanel();
await PageObjects.header.clickManagement();
await PageObjects.header.clickStackManagement();
await PageObjects.settings.clickKibanaSettings();
await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs');
});

View file

@ -59,8 +59,8 @@ export function HeaderPageProvider({ getService, getPageObjects }) {
await this.awaitGlobalLoadingIndicatorHidden();
}
async clickManagement() {
await appsMenu.clickLink('Management');
async clickStackManagement() {
await appsMenu.clickLink('Stack Management');
await this.awaitGlobalLoadingIndicatorHidden();
}

View file

@ -19,8 +19,10 @@
import { map as mapAsync } from 'bluebird';
import expect from '@kbn/expect';
import { NavSetting } from '../../../src/core/public/chrome/ui/header/';
import { FtrProviderContext } from '../ftr_provider_context';
export function SettingsPageProvider({ getService, getPageObjects }) {
export function SettingsPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
const browser = getService('browser');
@ -34,7 +36,8 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
async clickNavigation() {
find.clickDisplayedByCssSelector('.app-link:nth-child(5) a');
}
async clickLinkText(text) {
async clickLinkText(text: string) {
await find.clickByDisplayedLinkText(text);
}
async clickKibanaSettings() {
@ -55,6 +58,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
// check for the index pattern info flyout that covers the
// create index pattern button on smaller screens
// @ts-ignore
await retry.waitFor('index pattern info flyout', async () => {
if (await testSubjects.exists('CreateIndexPatternPrompt')) {
await testSubjects.click('CreateIndexPatternPrompt > euiFlyoutCloseButton');
@ -62,18 +66,18 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
});
}
async getAdvancedSettings(propertyName) {
async getAdvancedSettings(propertyName: string) {
log.debug('in getAdvancedSettings');
const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`);
return await setting.getAttribute('value');
}
async expectDisabledAdvancedSetting(propertyName) {
async expectDisabledAdvancedSetting(propertyName: string) {
const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`);
expect(setting.getAttribute('disabled')).to.eql('');
}
async getAdvancedSettingCheckbox(propertyName) {
async getAdvancedSettingCheckbox(propertyName: string) {
log.debug('in getAdvancedSettingCheckbox');
return await testSubjects.getAttribute(
`advancedSetting-editField-${propertyName}`,
@ -81,12 +85,12 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
);
}
async clearAdvancedSettings(propertyName) {
async clearAdvancedSettings(propertyName: string) {
await testSubjects.click(`advancedSetting-resetField-${propertyName}`);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async setAdvancedSettingsSelect(propertyName, propertyValue) {
async setAdvancedSettingsSelect(propertyName: string, propertyValue: string) {
await find.clickByCssSelector(
`[data-test-subj="advancedSetting-editField-${propertyName}"] option[value="${propertyValue}"]`
);
@ -95,7 +99,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
}
async setAdvancedSettingsInput(propertyName, propertyValue) {
async setAdvancedSettingsInput(propertyName: string, propertyValue: string) {
const input = await testSubjects.find(`advancedSetting-editField-${propertyName}`);
await input.clearValue();
await input.type(propertyValue);
@ -103,7 +107,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
}
async toggleAdvancedSettingCheckbox(propertyName) {
async toggleAdvancedSettingCheckbox(propertyName: string) {
testSubjects.click(`advancedSetting-editField-${propertyName}`);
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`);
@ -126,7 +130,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
return await testSubjects.find('createIndexPatternTimeFieldSelect');
}
async selectTimeFieldOption(selection) {
async selectTimeFieldOption(selection: string) {
// open dropdown
await this.clickTimeFieldNameField();
// close dropdown, keep focus
@ -141,7 +145,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
});
}
async getTimeFieldOption(selection) {
async getTimeFieldOption(selection: string) {
return await find.displayedByCssSelector('option[value="' + selection + '"]');
}
@ -174,9 +178,9 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
return await find.allByCssSelector('table.euiTable thead tr th');
}
async sortBy(columnName) {
async sortBy(columnName: string) {
const chartTypes = await find.allByCssSelector('table.euiTable thead tr th button');
async function getChartType(chart) {
async function getChartType(chart: Record<string, any>) {
const chartString = await chart.getVisibleText();
if (chartString === columnName) {
await chart.click();
@ -187,7 +191,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
return Promise.all(getChartTypesPromises);
}
async getTableRow(rowNumber, colNumber) {
async getTableRow(rowNumber: number, colNumber: number) {
// passing in zero-based index, but adding 1 for css 1-based indexes
return await find.byCssSelector(
'table.euiTable tbody tr:nth-child(' +
@ -234,13 +238,13 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
});
}
async setFieldTypeFilter(type) {
async setFieldTypeFilter(type: string) {
await find.clickByCssSelector(
'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[label="' + type + '"]'
);
}
async setScriptedFieldLanguageFilter(language) {
async setScriptedFieldLanguageFilter(language: string) {
await find.clickByCssSelector(
'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[label="' +
language +
@ -248,13 +252,13 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
);
}
async filterField(name) {
async filterField(name: string) {
const input = await testSubjects.find('indexPatternFieldFilter');
await input.clearValue();
await input.type(name);
}
async openControlsByName(name) {
async openControlsByName(name: string) {
await this.filterField(name);
const tableFields = await (
await find.byCssSelector(
@ -312,7 +316,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
}
async createIndexPattern(
indexPatternName,
indexPatternName: string,
timefield = '@timestamp',
isStandardIndexPattern = true
) {
@ -364,7 +368,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
async getIndexPatternIdFromUrl() {
const currentUrl = await browser.getCurrentUrl();
const indexPatternId = currentUrl.match(/.*\/(.*)/)[1];
const indexPatternId = currentUrl.match(/.*\/(.*)/)![1];
log.debug('index pattern ID: ', indexPatternId);
@ -423,12 +427,19 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await testSubjects.click('tab-sourceFilters');
}
async editScriptedField(name) {
async editScriptedField(name: string) {
await this.filterField(name);
await find.clickByCssSelector('.euiTableRowCell--hasActions button:first-child');
}
async addScriptedField(name, language, type, format, popularity, script) {
async addScriptedField(
name: string,
language: string,
type: string,
format: Record<string, any>,
popularity: string,
script: string
) {
await this.clickAddScriptedField();
await this.setScriptedFieldName(name);
if (language) await this.setScriptedFieldLanguage(language);
@ -469,42 +480,42 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
}
async setScriptedFieldName(name) {
async setScriptedFieldName(name: string) {
log.debug('set scripted field name = ' + name);
const field = await testSubjects.find('editorFieldName');
await field.clearValue();
await field.type(name);
}
async setScriptedFieldLanguage(language) {
async setScriptedFieldLanguage(language: string) {
log.debug('set scripted field language = ' + language);
await find.clickByCssSelector(
'select[data-test-subj="editorFieldLang"] > option[value="' + language + '"]'
);
}
async setScriptedFieldType(type) {
async setScriptedFieldType(type: string) {
log.debug('set scripted field type = ' + type);
await find.clickByCssSelector(
'select[data-test-subj="editorFieldType"] > option[value="' + type + '"]'
);
}
async setFieldFormat(format) {
async setFieldFormat(format: string) {
log.debug('set scripted field format = ' + format);
await find.clickByCssSelector(
'select[data-test-subj="editorSelectedFormatId"] > option[value="' + format + '"]'
);
}
async setScriptedFieldUrlType(type) {
async setScriptedFieldUrlType(type: string) {
log.debug('set scripted field Url type = ' + type);
await find.clickByCssSelector(
'select[data-test-subj="urlEditorType"] > option[value="' + type + '"]'
);
}
async setScriptedFieldUrlTemplate(template) {
async setScriptedFieldUrlTemplate(template: string) {
log.debug('set scripted field Url Template = ' + template);
const urlTemplateField = await find.byCssSelector(
'input[data-test-subj="urlEditorUrlTemplate"]'
@ -512,7 +523,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await urlTemplateField.type(template);
}
async setScriptedFieldUrlLabelTemplate(labelTemplate) {
async setScriptedFieldUrlLabelTemplate(labelTemplate: string) {
log.debug('set scripted field Url Label Template = ' + labelTemplate);
const urlEditorLabelTemplate = await find.byCssSelector(
'input[data-test-subj="urlEditorLabelTemplate"]'
@ -520,7 +531,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await urlEditorLabelTemplate.type(labelTemplate);
}
async setScriptedFieldDatePattern(datePattern) {
async setScriptedFieldDatePattern(datePattern: string) {
log.debug('set scripted field Date Pattern = ' + datePattern);
const datePatternField = await find.byCssSelector(
'input[data-test-subj="dateEditorPattern"]'
@ -531,21 +542,21 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await datePatternField.type(datePattern);
}
async setScriptedFieldStringTransform(stringTransform) {
async setScriptedFieldStringTransform(stringTransform: string) {
log.debug('set scripted field string Transform = ' + stringTransform);
await find.clickByCssSelector(
'select[data-test-subj="stringEditorTransform"] > option[value="' + stringTransform + '"]'
);
}
async setScriptedFieldPopularity(popularity) {
async setScriptedFieldPopularity(popularity: string) {
log.debug('set scripted field popularity = ' + popularity);
const field = await testSubjects.find('editorFieldCount');
await field.clearValue();
await field.type(popularity);
}
async setScriptedFieldScript(script) {
async setScriptedFieldScript(script: string) {
log.debug('set scripted field script = ' + script);
const aceEditorCssSelector = '[data-test-subj="editorFieldScript"] .ace_editor';
await find.clickByCssSelector(aceEditorCssSelector);
@ -555,7 +566,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await browser.pressKeys(...script.split(''));
}
async openScriptedFieldHelp(activeTab) {
async openScriptedFieldHelp(activeTab: string) {
log.debug('open Scripted Fields help');
let isOpen = await testSubjects.exists('scriptedFieldsHelpFlyout');
if (!isOpen) {
@ -577,7 +588,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await flyout.ensureClosed('scriptedFieldsHelpFlyout');
}
async executeScriptedField(script, additionalField) {
async executeScriptedField(script: string, additionalField: string) {
log.debug('execute Scripted Fields help');
await this.closeScriptedFieldHelp(); // ensure script help is closed so script input is not blocked
await this.setScriptedFieldScript(script);
@ -595,7 +606,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
return scriptResults;
}
async importFile(path, overwriteAll = true) {
async importFile(path: string, overwriteAll = true) {
log.debug(`importFile(${path})`);
log.debug(`Clicking importObjects`);
@ -645,7 +656,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await testSubjects.click('importSavedObjectsConfirmBtn');
}
async associateIndexPattern(oldIndexPatternId, newIndexPatternTitle) {
async associateIndexPattern(oldIndexPatternId: string, newIndexPatternTitle: string) {
await find.clickByCssSelector(
`select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] >
[data-test-subj="indexPatternOption-${newIndexPatternTitle}"]`
@ -710,7 +721,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
return await deleteButton.isEnabled();
}
async canSavedObjectBeDeleted(id) {
async canSavedObjectBeDeleted(id: string) {
const allCheckBoxes = await testSubjects.findAll('checkboxSelectRow*');
for (const checkBox of allCheckBoxes) {
if (await checkBox.isSelected()) {
@ -722,6 +733,12 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await checkBox.click();
return await this.canSavedObjectsBeDeleted();
}
async setNavType(navType: NavSetting) {
await PageObjects.common.navigateToApp('settings');
await this.clickKibanaSettings();
await this.setAdvancedSettingsSelect('pageNavigation', navType);
}
}
return new SettingsPage();

View file

@ -28,7 +28,7 @@ import '../../plugins/core_app_status/public/types';
// eslint-disable-next-line import/no-default-export
export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common']);
const PageObjects = getPageObjects(['common', 'settings']);
const browser = getService('browser');
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
@ -48,6 +48,10 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
};
describe('application status management', () => {
before(async () => {
await PageObjects.settings.setNavType('individual');
});
beforeEach(async () => {
await PageObjects.common.navigateToApp('app_status_start');
});

View file

@ -122,7 +122,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
});
it('can navigate from NP apps to legacy apps', async () => {
await appsMenu.clickLink('Management');
await appsMenu.clickLink('Stack Management');
await loadingScreenShown();
await testSubjects.existOrFail('managementNav');
});

View file

@ -9,6 +9,7 @@ import { Server } from 'hapi';
import { resolve } from 'path';
import { APMPluginContract } from '../../../plugins/apm/server';
import { LegacyPluginInitializer } from '../../../../src/legacy/types';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
import mappings from './mappings.json';
import { makeApmUsageCollector } from './server/lib/apm_telemetry';
@ -18,7 +19,6 @@ export const apm: LegacyPluginInitializer = kibana => {
id: 'apm',
configPrefix: 'xpack.apm',
publicDir: resolve(__dirname, 'public'),
uiExports: {
app: {
title: 'APM',
@ -28,7 +28,8 @@ export const apm: LegacyPluginInitializer = kibana => {
main: 'plugins/apm/index',
icon: 'plugins/apm/icon.svg',
euiIconType: 'apmApp',
order: 8100
order: 8100,
category: DEFAULT_APP_CATEGORIES.observability
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
home: ['plugins/apm/legacy_register_feature'],

View file

@ -5,6 +5,7 @@
*/
import { resolve } from 'path';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
import { init } from './init';
import { mappings } from './server/mappings';
import { CANVAS_APP, CANVAS_TYPE, CUSTOM_ELEMENT_TYPE } from './common/lib';
@ -23,6 +24,7 @@ export function canvas(kibana) {
icon: 'plugins/canvas/icon.svg',
euiIconType: 'canvasApp',
main: 'plugins/canvas/legacy_start',
category: DEFAULT_APP_CATEGORIES.analyze,
},
interpreter: [
'plugins/canvas/browser_functions',

View file

@ -5,15 +5,13 @@
*/
import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
import { CONFIG_DASHBOARD_ONLY_MODE_ROLES } from './common';
import { createDashboardModeRequestInterceptor } from './server';
import { i18n } from '@kbn/i18n';
// Copied largely from plugins/kibana/index.js. The dashboard viewer includes just the dashboard section of
// the standard kibana plugin. We don't want to include code for the other links (visualize, dev tools, etc)
// the standard kibana plugin. We don't want to include code for the other links (visualize, dev tools, etc)
// since it's view only, but we want the urls to be the same, so we are using largely the same setup.
export function dashboardMode(kibana) {
const kbnBaseUrl = '/app/kibana';
@ -64,6 +62,7 @@ export function dashboardMode(kibana) {
}
),
icon: 'plugins/kibana/dashboard/assets/dashboard.svg',
category: DEFAULT_APP_CATEGORIES.analyze,
},
],
},

View file

@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
import migrations from './migrations';
import mappings from './mappings.json';
import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
export const graph: LegacyPluginInitializer = kibana => {
return new kibana.Plugin({
@ -25,6 +26,7 @@ export const graph: LegacyPluginInitializer = kibana => {
icon: 'plugins/graph/icon.png',
euiIconType: 'graphApp',
main: 'plugins/graph/index',
category: DEFAULT_APP_CATEGORIES.analyze,
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
mappings,

View file

@ -146,7 +146,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
);
if (noIndexPatterns) {
const managementUrl = chrome.navLinks.get('kibana:management')!.url;
const managementUrl = chrome.navLinks.get('kibana:stack_management')!.url;
const indexPatternUrl = `${managementUrl}/kibana/index_patterns`;
const sampleDataUrl = `${application.getUrlForApp(
'kibana'

View file

@ -18,6 +18,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../plugins/fea
import { SpacesPluginSetup } from '../../../plugins/spaces/server';
import { VisTypeTimeseriesSetup } from '../../../../src/plugins/vis_type_timeseries/server';
import { APMPluginContract } from '../../../plugins/apm/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
export const APP_ID = 'infra';
@ -55,6 +56,7 @@ export function infra(kibana: any) {
defaultMessage: 'Metrics',
}),
url: `/app/${APP_ID}#/infrastructure`,
category: DEFAULT_APP_CATEGORIES.observability,
},
{
description: i18n.translate('xpack.infra.linkLogsDescription', {
@ -68,6 +70,7 @@ export function infra(kibana: any) {
defaultMessage: 'Logs',
}),
url: `/app/${APP_ID}#/logs`,
category: DEFAULT_APP_CATEGORIES.observability,
},
],
mappings: savedObjectMappings,

View file

@ -4,15 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import mappings from './mappings.json';
import { i18n } from '@kbn/i18n';
import { resolve } from 'path';
import mappings from './mappings.json';
import { migrations } from './migrations';
import { initTelemetryCollection } from './server/maps_telemetry';
import { getAppTitle } from './common/i18n_getters';
import _ from 'lodash';
import { MapPlugin } from './server/plugin';
import { APP_ID, APP_ICON, createMapPath, MAP_SAVED_OBJECT_TYPE } from './common/constants';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
export function maps(kibana) {
return new kibana.Plugin({
@ -29,6 +30,7 @@ export function maps(kibana) {
main: 'plugins/maps/legacy',
icon: 'plugins/maps/icon.svg',
euiIconType: APP_ICON,
category: DEFAULT_APP_CATEGORIES.analyze,
},
injectDefaultVars(server) {
const serverConfig = server.config();

View file

@ -10,7 +10,7 @@ import KbnServer, { Server } from 'src/legacy/server/kbn_server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { plugin } from './server/new_platform';
import { CloudSetup } from '../../../plugins/cloud/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
import {
MlInitializerContext,
MlCoreSetup,
@ -42,6 +42,7 @@ export const ml = (kibana: any) => {
icon: 'plugins/ml/application/ml.svg',
euiIconType: 'machineLearningApp',
main: 'plugins/ml/legacy',
category: DEFAULT_APP_CATEGORIES.analyze,
},
styleSheetPaths: resolve(__dirname, 'public/application/index.scss'),
hacks: ['plugins/ml/application/hacks/toggle_app_link_in_nav'],

View file

@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import { resolve } from 'path';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
/**
* Configuration of dependency objects for the UI, which are needed for the
@ -26,6 +27,7 @@ export const getUiExports = () => ({
euiIconType: 'monitoringApp',
linkToLastSubUrl: false,
main: 'plugins/monitoring/monitoring',
category: DEFAULT_APP_CATEGORIES.management,
},
injectDefaultVars(server) {
const config = server.config();

View file

@ -32,6 +32,7 @@ import {
} from './common/constants';
import { defaultIndexPattern } from './default_index_pattern';
import { initServerWithKibana } from './server/kibana.index';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const siem = (kibana: any) => {
@ -62,6 +63,7 @@ export const siem = (kibana: any) => {
order: 9000,
title: APP_NAME,
url: `/app/${APP_ID}`,
category: DEFAULT_APP_CATEGORIES.security,
},
],
uiSettingDefaults: {

View file

@ -9,6 +9,7 @@ import { resolve } from 'path';
import { PluginInitializerContext } from 'src/core/server';
import { PLUGIN } from './common/constants';
import { KibanaServer, plugin } from './server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
export const uptime = (kibana: any) =>
new kibana.Plugin({
@ -30,6 +31,7 @@ export const uptime = (kibana: any) =>
main: 'plugins/uptime/app',
order: 8900,
url: '/app/uptime#/',
category: DEFAULT_APP_CATEGORIES.observability,
},
home: ['plugins/uptime/register_feature'],
},

View file

@ -450,7 +450,6 @@
"common.ui.flotCharts.thuLabel": "木",
"common.ui.flotCharts.tueLabel": "火",
"common.ui.flotCharts.wedLabel": "水",
"common.ui.management.breadcrumb": "管理",
"common.ui.modals.cancelButtonLabel": "キャンセル",
"common.ui.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}",
"common.ui.notify.fatalError.unavailableServerErrorMessage": "HTTP リクエストが接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。",
@ -1887,8 +1886,6 @@
"kbn.management.landing.header": "Kibana {version} 管理",
"kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。",
"kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。",
"kbn.management.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。",
"kbn.management.managementLabel": "管理",
"kbn.management.objects.confirmModalOptions.deleteButtonLabel": "削除",
"kbn.management.objects.confirmModalOptions.modalDescription": "削除されたオブジェクトは復元できません",
"kbn.management.objects.confirmModalOptions.modalTitle": "保存された Kibana オブジェクトを削除しますか?",
@ -13219,4 +13216,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}

View file

@ -450,7 +450,6 @@
"common.ui.flotCharts.thuLabel": "周四",
"common.ui.flotCharts.tueLabel": "周二",
"common.ui.flotCharts.wedLabel": "周三",
"common.ui.management.breadcrumb": "管理",
"common.ui.modals.cancelButtonLabel": "取消",
"common.ui.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}{errMessage}",
"common.ui.notify.fatalError.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。",
@ -1887,8 +1886,6 @@
"kbn.management.landing.header": "Kibana {version} 管理",
"kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。",
"kbn.management.landing.text": "在左侧菜单中可找到完整工具列表",
"kbn.management.managementDescription": "您用于管理 Elastic Stack 的中心控制台。",
"kbn.management.managementLabel": "管理",
"kbn.management.objects.confirmModalOptions.deleteButtonLabel": "删除",
"kbn.management.objects.confirmModalOptions.modalDescription": "您无法恢复删除的对象",
"kbn.management.objects.confirmModalOptions.modalTitle": "删除已保存 Kibana 对象?",
@ -13218,4 +13215,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
}
}

View file

@ -54,7 +54,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
expectSpaceSelector: false,
}
);
await kibanaServer.uiSettings.replace({});
await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' });
await PageObjects.settings.navigateTo();
});
@ -68,7 +68,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Management']);
expect(navLinks).to.eql(['Stack Management']);
});
it(`allows settings to be changed`, async () => {
@ -124,7 +124,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows Management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Management']);
expect(navLinks).to.eql(['Stack Management']);
});
it(`does not allow settings to be changed`, async () => {
@ -175,7 +175,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows Management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Discover', 'Management']);
expect(navLinks).to.eql(['Discover', 'Stack Management']);
});
it(`does not allow navigation to advanced settings; redirects to Kibana home`, async () => {

View file

@ -40,8 +40,9 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Management');
expect(navLinks).to.contain('Stack Management');
});
it(`allows settings to be changed`, async () => {

View file

@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows apm navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map(link => link.text)).to.eql(['APM', 'Management']);
expect(navLinks.map(link => link.text)).to.eql(['APM', 'Stack Management']);
});
it('can navigate to APM app', async () => {
@ -109,7 +109,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows apm navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['APM', 'Management']);
expect(navLinks).to.eql(['APM', 'Stack Management']);
});
it('can navigate to APM app', async () => {

View file

@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']);
const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
@ -30,6 +30,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('APM');
});

View file

@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows canvas navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Canvas', 'Management']);
expect(navLinks).to.eql(['Canvas', 'Stack Management']);
});
it(`landing page shows "Create new workpad" button`, async () => {
@ -142,7 +142,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows canvas navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Canvas', 'Management']);
expect(navLinks).to.eql(['Canvas', 'Stack Management']);
});
it(`landing page shows disabled "Create new workpad" button`, async () => {

View file

@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']);
const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector', 'settings']);
const appsMenu = getService('appsMenu');
describe('spaces feature controls', function() {
@ -40,6 +40,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Canvas');
});

View file

@ -75,7 +75,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows dashboard navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Management']);
expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Stack Management']);
});
it(`landing page shows "Create new Dashboard" button`, async () => {
@ -253,7 +253,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows dashboard navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Dashboard', 'Management']);
expect(navLinks).to.eql(['Dashboard', 'Stack Management']);
});
it(`landing page doesn't show "Create new Dashboard" button`, async () => {

View file

@ -13,7 +13,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']);
const PageObjects = getPageObjects([
'common',
'dashboard',
'security',
'spaceSelector',
'settings',
]);
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
@ -43,6 +49,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Dashboard');
});

View file

@ -39,6 +39,7 @@ export default function({ getService, getPageObjects }) {
await esArchiver.load('dashboard_view_mode');
await kibanaServer.uiSettings.replace({
defaultIndex: 'logstash-*',
pageNavigation: 'individual',
});
await browser.setWindowSize(1600, 1000);
@ -199,7 +200,7 @@ export default function({ getService, getPageObjects }) {
await PageObjects.security.forceLogout();
await PageObjects.security.login('mixeduser', '123456');
if (await appsMenu.linkExists('Management')) {
if (await appsMenu.linkExists('Stack Management')) {
throw new Error('Expected management nav link to not be shown');
}
});
@ -208,7 +209,7 @@ export default function({ getService, getPageObjects }) {
await PageObjects.security.forceLogout();
await PageObjects.security.login('mysuperuser', '123456');
if (!(await appsMenu.linkExists('Management'))) {
if (!(await appsMenu.linkExists('Stack Management'))) {
throw new Error('Expected management nav link to be shown');
}
});

View file

@ -63,7 +63,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows Dev Tools navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Management']);
expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Stack Management']);
});
describe('console', () => {
@ -144,7 +144,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it(`shows 'Dev Tools' navlink`, async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Dev Tools', 'Management']);
expect(navLinks).to.eql(['Dev Tools', 'Stack Management']);
});
describe('console', () => {

View file

@ -9,7 +9,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']);
const PageObjects = getPageObjects([
'common',
'dashboard',
'security',
'spaceSelector',
'settings',
]);
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
const grokDebugger = getService('grokDebugger');
@ -40,6 +46,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Dev Tools');
});

View file

@ -81,7 +81,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows discover navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Management']);
expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Stack Management']);
});
it('shows save button', async () => {
@ -168,7 +168,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows discover navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Discover', 'Management']);
expect(navLinks).to.eql(['Discover', 'Stack Management']);
});
it(`doesn't show save button`, async () => {

View file

@ -15,6 +15,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
'timePicker',
'security',
'spaceSelector',
'settings',
]);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
@ -49,6 +50,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Discover');
});

View file

@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows graph navlink', async () => {
const navLinks = await appsMenu.readLinks();
expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Management']);
expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Stack Management']);
});
it('landing page shows "Create new graph" button', async () => {
@ -127,7 +127,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows graph navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Graph', 'Management']);
expect(navLinks).to.eql(['Graph', 'Stack Management']);
});
it('does not show a "Create new Workspace" button', async () => {

View file

@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']);
const PageObjects = getPageObjects(['common', 'graph', 'security', 'error', 'settings']);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
@ -34,6 +34,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Graph');
});

View file

@ -70,7 +70,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Management']);
expect(navLinks).to.eql(['Stack Management']);
});
it(`index pattern listing shows create button`, async () => {
@ -113,7 +113,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
}
);
await kibanaServer.uiSettings.replace({});
await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' });
await PageObjects.settings.navigateTo();
});
@ -124,7 +124,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Management']);
expect(navLinks).to.eql(['Stack Management']);
});
it(`index pattern listing doesn't show create button`, async () => {
@ -176,7 +176,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows Management navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Discover', 'Management']);
expect(navLinks).to.eql(['Discover', 'Stack Management']);
});
it(`doesn't show Index Patterns in management side-nav`, async () => {

View file

@ -40,8 +40,9 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Management');
expect(navLinks).to.contain('Stack Management');
});
it(`index pattern listing shows create button`, async () => {

View file

@ -61,7 +61,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows metrics navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Metrics', 'Management']);
expect(navLinks).to.eql(['Metrics', 'Stack Management']);
});
describe('infrastructure landing page without data', () => {
@ -174,7 +174,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows metrics navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Metrics', 'Management']);
expect(navLinks).to.eql(['Metrics', 'Stack Management']);
});
describe('infrastructure landing page without data', () => {

View file

@ -12,7 +12,13 @@ const DATE_WITH_DATA = DATES.metricsAndLogs.hosts.withData;
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']);
const PageObjects = getPageObjects([
'common',
'infraHome',
'security',
'spaceSelector',
'settings',
]);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
const retry = getService('retry');
@ -31,7 +37,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
// we need to load the following in every situation as deleting
// a space deletes all of the associated saved objects
await esArchiver.load('empty_kibana');
await spacesService.create({
id: 'custom_space',
name: 'custom_space',
@ -48,6 +53,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Metrics');
});

View file

@ -58,7 +58,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows logs navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Logs', 'Management']);
expect(navLinks).to.eql(['Logs', 'Stack Management']);
});
describe('logs landing page without data', () => {
@ -121,7 +121,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows logs navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Logs', 'Management']);
expect(navLinks).to.eql(['Logs', 'Stack Management']);
});
describe('logs landing page without data', () => {

View file

@ -9,7 +9,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']);
const PageObjects = getPageObjects([
'common',
'infraHome',
'security',
'spaceSelector',
'settings',
]);
const testSubjects = getService('testSubjects');
const appsMenu = getService('appsMenu');
@ -36,6 +42,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Logs');
});

View file

@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const security = getService('security');
const appsMenu = getService('appsMenu');
const PageObjects = getPageObjects(['common', 'security']);
const PageObjects = getPageObjects(['common', 'security', 'settings']);
describe('security', () => {
before(async () => {
@ -94,6 +94,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
});
await PageObjects.security.login('machine_learning_user', 'machine_learning_user-password');
await PageObjects.settings.setNavType('individual');
});
after(async () => {

View file

@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']);
const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']);
const appsMenu = getService('appsMenu');
const testSubjects = getService('testSubjects');
@ -39,6 +39,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Machine Learning');
});

View file

@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows maps navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Maps', 'Management']);
expect(navLinks).to.eql(['Maps', 'Stack Management']);
});
it(`allows a map to be created`, async () => {
@ -153,7 +153,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('shows Maps navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Maps', 'Management']);
expect(navLinks).to.eql(['Maps', 'Stack Management']);
});
it(`does not show create new button`, async () => {
@ -248,7 +248,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
it('does not show Maps navlink', async () => {
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.eql(['Discover', 'Management']);
expect(navLinks).to.eql(['Discover', 'Stack Management']);
});
it(`returns a 404`, async () => {

View file

@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const security = getService('security');
const appsMenu = getService('appsMenu');
const PageObjects = getPageObjects(['common', 'security']);
const PageObjects = getPageObjects(['common', 'security', 'settings']);
describe('security', () => {
before(async () => {
@ -97,6 +97,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
});
it('shows monitoring navlink', async () => {
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Stack Monitoring');
});

View file

@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const spacesService = getService('spaces');
const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']);
const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']);
const appsMenu = getService('appsMenu');
const find = getService('find');
@ -37,10 +37,11 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
await spacesService.delete('custom_space');
});
it('shows Stack Monitoring navlink', async () => {
it('shows Stack Monitoring navlink fail', async () => {
await PageObjects.common.navigateToApp('home', {
basePath: '/s/custom_space',
});
await PageObjects.settings.setNavType('individual');
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
expect(navLinks).to.contain('Stack Monitoring');
});

Some files were not shown because too many files have changed in this diff Show more