From da54657b915c4710964dfdaf6678af59fc84d725 Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Tue, 21 Jan 2020 12:48:07 -0500 Subject: [PATCH 1/7] Grouped Kibana nav (#53545) Adds concept of `category` to nav links, grouping them by this in the side nav --- .../kibana-plugin-public.appbase.category.md | 13 + .../public/kibana-plugin-public.appbase.md | 1 + ...ana-plugin-public.appcategory.arialabel.md | 13 + ...a-plugin-public.appcategory.euiicontype.md | 13 + .../kibana-plugin-public.appcategory.label.md | 13 + .../kibana-plugin-public.appcategory.md | 23 + .../kibana-plugin-public.appcategory.order.md | 13 + ...na-plugin-public.chromenavlink.category.md | 13 + .../kibana-plugin-public.chromenavlink.md | 1 + ...na-plugin-public.legacynavlink.category.md | 11 + .../kibana-plugin-public.legacynavlink.md | 1 + .../core/public/kibana-plugin-public.md | 1 + packages/kbn-i18n/src/loader.ts | 7 +- src/core/public/application/types.ts | 8 + src/core/public/chrome/chrome_service.test.ts | 2 + src/core/public/chrome/chrome_service.tsx | 6 +- src/core/public/chrome/nav_links/nav_link.ts | 6 + .../__snapshots__/nav_drawer.test.tsx.snap | 5224 +++++++++++++++++ src/core/public/chrome/ui/header/header.tsx | 349 +- .../public/chrome/ui/header/header_logo.tsx | 104 + src/core/public/chrome/ui/header/index.ts | 2 + .../chrome/ui/header/nav_drawer.test.tsx | 103 + .../public/chrome/ui/header/nav_drawer.tsx | 170 + src/core/public/chrome/ui/header/nav_link.tsx | 87 + .../public/chrome/ui/header/recent_links.tsx | 113 + src/core/public/core_system.ts | 1 + src/core/public/index.ts | 3 +- .../injected_metadata_service.ts | 4 + src/core/public/legacy/legacy_service.ts | 1 + src/core/public/public.api.md | 32 + .../plugins/find_legacy_plugin_specs.ts | 2 + src/core/server/legacy/types.ts | 2 +- src/core/types/app_category.ts | 52 + src/core/types/index.ts | 1 + src/core/utils/default_app_categories.ts | 48 + src/core/utils/index.ts | 1 + src/legacy/core_plugins/kibana/index.js | 10 +- .../components/fetch_error/fetch_error.tsx | 2 +- .../home/np_ready/components/home.test.js | 4 +- .../kibana/public/management/index.js | 12 +- .../kibana/ui_setting_defaults.js | 19 + src/legacy/core_plugins/management/index.ts | 2 +- .../telemetry/common/constants.ts | 2 +- .../telemetry_management_collector.ts | 4 +- src/legacy/core_plugins/timelion/index.ts | 2 + src/legacy/plugin_discovery/types.ts | 2 + .../ui/public/management/breadcrumbs.ts | 4 +- src/legacy/ui/ui_apps/ui_app.js | 4 + .../ui/ui_exports/ui_export_types/ui_apps.js | 2 + .../ui/ui_nav_links/__tests__/ui_nav_link.js | 1 + src/legacy/ui/ui_nav_links/ui_nav_link.js | 3 + .../management_sidebar_nav.tsx | 5 +- .../public/legacy/sections_register.js | 4 +- .../management/public/management_app.tsx | 3 +- .../dashboard/create_and_add_embeddables.js | 5 +- .../apps/management/_index_pattern_filter.js | 2 +- test/functional/apps/visualize/_lab_mode.js | 5 +- test/functional/page_objects/header_page.js | 4 +- .../{settings_page.js => settings_page.ts} | 93 +- .../core_plugins/application_status.ts | 6 +- .../test_suites/core_plugins/applications.ts | 2 +- x-pack/legacy/plugins/apm/index.ts | 5 +- x-pack/legacy/plugins/canvas/index.js | 2 + x-pack/legacy/plugins/dashboard_mode/index.js | 9 +- x-pack/legacy/plugins/graph/index.ts | 2 + .../guidance_panel/guidance_panel.tsx | 2 +- x-pack/legacy/plugins/infra/index.ts | 3 + x-pack/legacy/plugins/maps/index.js | 6 +- x-pack/legacy/plugins/ml/index.ts | 3 +- .../legacy/plugins/monitoring/ui_exports.js | 2 + x-pack/legacy/plugins/siem/index.ts | 2 + x-pack/legacy/plugins/uptime/index.ts | 2 + .../translations/translations/ja-JP.json | 5 +- .../translations/translations/zh-CN.json | 5 +- .../advanced_settings_security.ts | 8 +- .../advanced_settings_spaces.ts | 3 +- .../apps/apm/feature_controls/apm_security.ts | 4 +- .../apps/apm/feature_controls/apm_spaces.ts | 3 +- .../feature_controls/canvas_security.ts | 4 +- .../canvas/feature_controls/canvas_spaces.ts | 3 +- .../feature_controls/dashboard_security.ts | 4 +- .../feature_controls/dashboard_spaces.ts | 9 +- .../dashboard_mode/dashboard_view_mode.js | 5 +- .../feature_controls/dev_tools_security.ts | 4 +- .../feature_controls/dev_tools_spaces.ts | 9 +- .../feature_controls/discover_security.ts | 4 +- .../feature_controls/discover_spaces.ts | 2 + .../graph/feature_controls/graph_security.ts | 4 +- .../graph/feature_controls/graph_spaces.ts | 3 +- .../index_patterns_security.ts | 8 +- .../feature_controls/index_patterns_spaces.ts | 3 +- .../infrastructure_security.ts | 4 +- .../feature_controls/infrastructure_spaces.ts | 10 +- .../infra/feature_controls/logs_security.ts | 4 +- .../infra/feature_controls/logs_spaces.ts | 9 +- .../feature_controls/ml_security.ts | 3 +- .../feature_controls/ml_spaces.ts | 3 +- .../maps/feature_controls/maps_security.ts | 6 +- .../feature_controls/monitoring_security.ts | 3 +- .../feature_controls/monitoring_spaces.ts | 5 +- .../feature_controls/spaces_security.ts | 5 +- .../feature_controls/timelion_security.ts | 4 +- .../feature_controls/timelion_spaces.ts | 9 +- .../feature_controls/uptime_security.ts | 4 +- .../uptime/feature_controls/uptime_spaces.ts | 3 +- .../feature_controls/visualize_security.ts | 4 +- .../feature_controls/visualize_spaces.ts | 9 +- .../common/nav_links_builder.ts | 2 +- .../common/services/ui_capabilities.ts | 2 +- 109 files changed, 6419 insertions(+), 444 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.appbase.category.md create mode 100644 docs/development/core/public/kibana-plugin-public.appcategory.arialabel.md create mode 100644 docs/development/core/public/kibana-plugin-public.appcategory.euiicontype.md create mode 100644 docs/development/core/public/kibana-plugin-public.appcategory.label.md create mode 100644 docs/development/core/public/kibana-plugin-public.appcategory.md create mode 100644 docs/development/core/public/kibana-plugin-public.appcategory.order.md create mode 100644 docs/development/core/public/kibana-plugin-public.chromenavlink.category.md create mode 100644 docs/development/core/public/kibana-plugin-public.legacynavlink.category.md create mode 100644 src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap create mode 100644 src/core/public/chrome/ui/header/header_logo.tsx create mode 100644 src/core/public/chrome/ui/header/nav_drawer.test.tsx create mode 100644 src/core/public/chrome/ui/header/nav_drawer.tsx create mode 100644 src/core/public/chrome/ui/header/nav_link.tsx create mode 100644 src/core/public/chrome/ui/header/recent_links.tsx create mode 100644 src/core/types/app_category.ts create mode 100644 src/core/utils/default_app_categories.ts rename test/functional/page_objects/{settings_page.js => settings_page.ts} (90%) diff --git a/docs/development/core/public/kibana-plugin-public.appbase.category.md b/docs/development/core/public/kibana-plugin-public.appbase.category.md new file mode 100644 index 00000000000000..215ebbbd0e1863 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.category.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [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 + +Signature: + +```typescript +category?: AppCategory; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.md b/docs/development/core/public/kibana-plugin-public.appbase.md index eb6d91cb924888..6f547450b6a129 100644 --- a/docs/development/core/public/kibana-plugin-public.appbase.md +++ b/docs/development/core/public/kibana-plugin-public.appbase.md @@ -16,6 +16,7 @@ export interface AppBase | Property | Type | Description | | --- | --- | --- | | [capabilities](./kibana-plugin-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | +| [category](./kibana-plugin-public.appbase.category.md) | AppCategory | 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) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | | [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [icon](./kibana-plugin-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.arialabel.md b/docs/development/core/public/kibana-plugin-public.appcategory.arialabel.md new file mode 100644 index 00000000000000..0245b548ae74f5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.arialabel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) > [ariaLabel](./kibana-plugin-public.appcategory.arialabel.md) + +## AppCategory.ariaLabel property + +If the visual label isn't appropriate for screen readers, can override it here + +Signature: + +```typescript +ariaLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.euiicontype.md b/docs/development/core/public/kibana-plugin-public.appcategory.euiicontype.md new file mode 100644 index 00000000000000..90133735a00824 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.euiicontype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) > [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 + +Signature: + +```typescript +euiIconType?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.label.md b/docs/development/core/public/kibana-plugin-public.appcategory.label.md new file mode 100644 index 00000000000000..171b1627f9ef8a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.label.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) > [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. + +Signature: + +```typescript +label: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.md b/docs/development/core/public/kibana-plugin-public.appcategory.md new file mode 100644 index 00000000000000..f1085e73252721 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [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 + +Signature: + +```typescript +export interface AppCategory +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [ariaLabel](./kibana-plugin-public.appcategory.arialabel.md) | string | If the visual label isn't appropriate for screen readers, can override it here | +| [euiIconType](./kibana-plugin-public.appcategory.euiicontype.md) | string | 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) | string | Label used for cateogry name. Also used as aria-label if one isn't set. | +| [order](./kibana-plugin-public.appcategory.order.md) | number | 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) | + diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.order.md b/docs/development/core/public/kibana-plugin-public.appcategory.order.md new file mode 100644 index 00000000000000..ef17ac04b78d6a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.order.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) > [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) + +Signature: + +```typescript +order?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.category.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.category.md new file mode 100644 index 00000000000000..19d5a43a293079 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.category.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [category](./kibana-plugin-public.chromenavlink.category.md) + +## ChromeNavLink.category property + +The category the app lives in + +Signature: + +```typescript +readonly category?: AppCategory; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md index 4cb9080222ac54..2afd6ce2d58c46 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -17,6 +17,7 @@ export interface ChromeNavLink | --- | --- | --- | | [active](./kibana-plugin-public.chromenavlink.active.md) | boolean | Indicates whether or not this app is currently on the screen. | | [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | +| [category](./kibana-plugin-public.chromenavlink.category.md) | AppCategory | The category the app lives in | | [disabled](./kibana-plugin-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | | [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [hidden](./kibana-plugin-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.category.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.category.md new file mode 100644 index 00000000000000..7026e9b519cc03 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.category.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [category](./kibana-plugin-public.legacynavlink.category.md) + +## LegacyNavLink.category property + +Signature: + +```typescript +category?: AppCategory; +``` diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.md index fc0c445f517b3b..e112110dd10f85 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.md @@ -15,6 +15,7 @@ export interface LegacyNavLink | Property | Type | Description | | --- | --- | --- | +| [category](./kibana-plugin-public.legacynavlink.category.md) | AppCategory | | | [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md) | string | | | [icon](./kibana-plugin-public.legacynavlink.icon.md) | string | | | [id](./kibana-plugin-public.legacynavlink.id.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 27037d46926c18..52aca7501e64dd 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -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) | | diff --git a/packages/kbn-i18n/src/loader.ts b/packages/kbn-i18n/src/loader.ts index 2d68079735c032..21f540f588f46f 100644 --- a/packages/kbn-i18n/src/loader.ts +++ b/packages/kbn-i18n/src/loader.ts @@ -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 { - 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')); } /** diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 0d955482d2226c..63e542b0127ed1 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -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` diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index abd04722a49f20..9018b219736345 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -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) { diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 09ea1afe35766c..6ab9fe158742a6 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -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 { this.initVisibility(application); @@ -173,7 +175,6 @@ export class ChromeService { getHeaderComponent: () => ( -
), diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index 3b16c030ddcc93..4d3a1e9ecd1991 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -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. */ diff --git a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap new file mode 100644 index 00000000000000..0ebc44ba67862a --- /dev/null +++ b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap @@ -0,0 +1,5224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` + + + + + + + +`; + +exports[`NavDrawer Advanced setting set to grouped renders individual items if there are less than 7 1`] = ` + + + + + + + +`; + +exports[`NavDrawer Advanced setting set to grouped renders individual items if there is only 1 category 1`] = ` + + + + + + + +`; + +exports[`NavDrawer Advanced setting set to individual renders individual items 1`] = ` + + + + + + + +`; diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index d05a6bb53405c5..c3cefd180b16f6 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -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'; - -// 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>; +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'; -interface Props { +export interface HeaderProps { kibanaVersion: string; application: InternalApplicationStart; appTitle$: Rx.Observable; @@ -168,28 +67,29 @@ interface Props { legacyMode: boolean; navControlsLeft$: Rx.Observable; navControlsRight$: Rx.Observable; - intl: InjectedIntl; basePath: HttpStart['basePath']; isLocked?: boolean; - onIsLockedUpdate?: (isLocked: boolean) => void; + navSetting$: Rx.Observable; + onIsLockedUpdate?: OnIsLockedUpdate; } interface State { appTitle: string; - currentAppId?: string; isVisible: boolean; - navLinks: ReadonlyArray>; - recentlyAccessed: ReadonlyArray>; + navLinks: ChromeNavLink[]; + recentlyAccessed: ChromeRecentlyAccessedHistoryItem[]; forceNavigation: boolean; navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; + navSetting: NavSetting; + currentAppId: string | undefined; } -class HeaderUI extends Component { +export class Header extends Component { private subscription?: Rx.Subscription; private navDrawerRef = createRef(); - constructor(props: Props) { + constructor(props: HeaderProps) { super(props); this.state = { @@ -200,6 +100,8 @@ class HeaderUI extends Component { forceNavigation: false, navControlsLeft: [], navControlsRight: [], + navSetting: 'grouped', + currentAppId: '', }; } @@ -214,7 +116,8 @@ class HeaderUI extends Component { 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 { 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 { } } - public renderLogo() { - const { homeHref, intl } = this.props; - return ( - - ); - } - public renderMenuTrigger() { return ( this.navDrawerRef.current.toggleOpen()} > @@ -275,98 +163,29 @@ class HeaderUI extends Component { } 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 ? ( - - ) : ( - 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 (
@@ -375,7 +194,13 @@ class HeaderUI extends Component { {this.renderMenuTrigger()} - {this.renderLogo()} + + + @@ -399,75 +224,17 @@ class HeaderUI extends Component { - - - - - - + />
); } - - private onNavClick = (event: React.MouseEvent) => { - 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); diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx new file mode 100644 index 00000000000000..793b8646dabf78 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -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, + 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 ( + onClick(e, forceNavigation, navLinks)} + href={href} + aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { + defaultMessage: 'Go to home page', + })} + /> + ); +} diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index 6d59fc6d9433b8..b396c94b3f2a3e 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -26,3 +26,5 @@ export { ChromeHelpExtensionMenuDocumentationLink, ChromeHelpExtensionMenuGitHubLink, } from './header_help_menu'; +export type NavSetting = 'grouped' | 'individual'; +export type OnIsLockedUpdate = (isLocked: boolean) => void; diff --git a/src/core/public/chrome/ui/header/nav_drawer.test.tsx b/src/core/public/chrome/ui/header/nav_drawer.test.tsx new file mode 100644 index 00000000000000..7272935b93a520 --- /dev/null +++ b/src/core/public/chrome/ui/header/nav_drawer.test.tsx @@ -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( + + ); + 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(); + 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(); + expect(component).toMatchSnapshot(); + }); + it('renders grouped items', () => { + const component = mount(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx new file mode 100644 index 00000000000000..dbb68d5dd3901e --- /dev/null +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -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) { + const allCategories = {} as Record; + + for (const [key, value] of Object.entries(allCategorizedLinks)) { + allCategories[key] = value[0].category; + } + + return allCategories; +} + +function getOrderedCategories( + mainCategories: Record, + categoryDictionary: ReturnType +) { + 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 +) { + 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 ( + + {RecentLinks({ + recentlyAccessedItems, + navLinks: chromeNavLinks, + basePath, + })} + + {showUngroupedNav ? ( + + ) : ( + <> + { + 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'), + ]} + /> + + { + link['data-test-subj'] = 'navDrawerFlyoutLink'; + return link; + }), + }, + }, + ]} + /> + + )} + + ); +} + +export const NavDrawer = React.forwardRef(navDrawerRenderer); diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx new file mode 100644 index 00000000000000..52b59c53b658c0 --- /dev/null +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -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 ; +} + +export type NavLink = ReturnType; + +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 ? : undefined, + order, + 'data-test-subj': 'navDrawerAppsMenuLink', + }; +} diff --git a/src/core/public/chrome/ui/header/recent_links.tsx b/src/core/public/chrome/ui/header/recent_links.tsx new file mode 100644 index 00000000000000..a947ab1c450563 --- /dev/null +++ b/src/core/public/chrome/ui/header/recent_links.tsx @@ -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 ( + + ); +} diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index be69cacdd271a7..5fb12ec1549521 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -239,6 +239,7 @@ export class CoreSystem { http, injectedMetadata, notifications, + uiSettings, }); application.registerMountContext(this.coreContext.coreId, 'core', () => ({ diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 5e732dd05e6168..bf8cab9a3c7787 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -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, diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 0bde1b68e1876a..1075a7741ee324 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -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; user?: Record; diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index f906aff1759e2e..cc3210771eecc2 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -74,6 +74,7 @@ export class LegacyPlatformService { appUrl: navLink.url, subUrlBase: navLink.subUrlBase, linkToLastSubUrl: navLink.linkToLastSubUrl, + category: navLink.category, }) ); diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index f7b260c68ee96b..0da6e0d422f2d7 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -26,6 +26,7 @@ export interface App extends AppBase { // @public (undocumented) export interface AppBase { capabilities?: Partial; + category?: AppCategory; chromeless?: boolean; euiIconType?: string; icon?: string; @@ -40,6 +41,14 @@ export interface AppBase { updater$?: Observable; } +// @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) diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 9867274d224bd0..a19133c30659b0 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -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(spec => ({ id: spec.id, + category: spec.category, title: spec.title, order: typeof spec.order === 'number' ? spec.order : 0, url: spec.url, diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index 40b8244a318903..d51058ca561c6b 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -139,7 +139,7 @@ export type LegacyNavLinkSpec = Record & 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 }; /** diff --git a/src/core/types/app_category.ts b/src/core/types/app_category.ts new file mode 100644 index 00000000000000..83a3693f009b6c --- /dev/null +++ b/src/core/types/app_category.ts @@ -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; +} diff --git a/src/core/types/index.ts b/src/core/types/index.ts index d01b514c770a77..7ddb6b0d8dfbbc 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -23,3 +23,4 @@ */ export * from './core_service'; export * from './capabilities'; +export * from './app_category'; diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts new file mode 100644 index 00000000000000..3e3cc2fef2a229 --- /dev/null +++ b/src/core/utils/default_app_categories.ts @@ -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', + }, +}); diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 7c8ed481c0a7d0..7317c222d3bc32 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -28,3 +28,4 @@ export * from './pick'; export * from './promise'; export * from './url'; export * from './unset'; +export * from './default_app_categories'; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 0366d8b27f2118..55bd8520502187 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -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, }, ], diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx index d2dda32f318fe5..1aad7e953b8de0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx @@ -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`; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index be2ceb66f69d03..27d4f1a8b1c1fa 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -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', diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index d62770956b88ef..1305310b6f6151 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -74,7 +74,7 @@ export function updateLandingPage(version) {

@@ -93,7 +93,7 @@ export function updateLandingPage(version) {

@@ -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', diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index dc8fee4a849c50..9b848666541ce7 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -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', + }), + }, + }, }; } diff --git a/src/legacy/core_plugins/management/index.ts b/src/legacy/core_plugins/management/index.ts index 65601b53718151..4962c948f842f2 100644 --- a/src/legacy/core_plugins/management/index.ts +++ b/src/legacy/core_plugins/management/index.ts @@ -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({ diff --git a/src/legacy/core_plugins/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts index cb4ff79969a322..cf2c9c883871bf 100644 --- a/src/legacy/core_plugins/telemetry/common/constants.ts +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -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'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/management/telemetry_management_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/management/telemetry_management_collector.ts index f45cf7fc6bb33f..44926b644ced5c 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/management/telemetry_management_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/management/telemetry_management_collector.ts @@ -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), }); diff --git a/src/legacy/core_plugins/timelion/index.ts b/src/legacy/core_plugins/timelion/index.ts index ec121647f4e477..d725327e2365b5 100644 --- a/src/legacy/core_plugins/timelion/index.ts +++ b/src/legacy/core_plugins/timelion/index.ts @@ -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')], diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts index fe886b9d178111..9425003eae8747 100644 --- a/src/legacy/plugin_discovery/types.ts +++ b/src/legacy/plugin_discovery/types.ts @@ -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; diff --git a/src/legacy/ui/public/management/breadcrumbs.ts b/src/legacy/ui/public/management/breadcrumbs.ts index fe53bcfde9e1f8..936e99caff565f 100644 --- a/src/legacy/ui/public/management/breadcrumbs.ts +++ b/src/legacy/ui/public/management/breadcrumbs.ts @@ -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', }); diff --git a/src/legacy/ui/ui_apps/ui_app.js b/src/legacy/ui/ui_apps/ui_app.js index 9c82ff2abedb57..1cfd54588b516e 100644 --- a/src/legacy/ui/ui_apps/ui_app.js +++ b/src/legacy/ui/ui_apps/ui_app.js @@ -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, }; } } diff --git a/src/legacy/ui/ui_exports/ui_export_types/ui_apps.js b/src/legacy/ui/ui_exports/ui_export_types/ui_apps.js index d7ac49d9d49a32..639a5a7c58e180 100644 --- a/src/legacy/ui/ui_exports/ui_export_types/ui_apps.js +++ b/src/legacy/ui/ui_exports/ui_export_types/ui_apps.js @@ -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, }; } diff --git a/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js b/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js index 37e023127ed419..543fe05b13e43e 100644 --- a/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js +++ b/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js @@ -45,6 +45,7 @@ describe('UiNavLink', () => { euiIconType: spec.euiIconType, hidden: spec.hidden, disabled: spec.disabled, + category: undefined, // defaults linkToLastSubUrl: true, diff --git a/src/legacy/ui/ui_nav_links/ui_nav_link.js b/src/legacy/ui/ui_nav_links/ui_nav_link.js index 7537a60adbcf2d..5888c21a53c95c 100644 --- a/src/legacy/ui/ui_nav_links/ui_nav_link.js +++ b/src/legacy/ui/ui_nav_links/ui_nav_link.js @@ -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, }; } } diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx index cb0b82d0f0bde4..69ba813d2347ed 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -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 ( <>

{i18n.translate('management.nav.label', { - defaultMessage: 'Management', + // todo + defaultMessage: 'Stack Management', })}

diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js index 63d919377f89ea..ca35db56c340be 100644 --- a/src/plugins/management/public/legacy/sections_register.js +++ b/src/plugins/management/public/legacy/sections_register.js @@ -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, diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx index 705d98eaaf2ffd..02b3ea306c23d8 100644 --- a/src/plugins/management/public/management_app.tsx +++ b/src/plugins/management/public/management_app.tsx @@ -64,7 +64,8 @@ export class ManagementApp { coreStart.chrome.setBreadcrumbs([ { text: i18n.translate('management.breadcrumb', { - defaultMessage: 'Management', + // todo + defaultMessage: 'Stack Management', }), href: '#/management', }, diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.js b/test/functional/apps/dashboard/create_and_add_embeddables.js index 90f02c36b3b7fa..0b628100a98bd6 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.js +++ b/test/functional/apps/dashboard/create_and_add_embeddables.js @@ -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(); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index a32024adb5ec77..e685c43e9ce98a 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -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(); }); diff --git a/test/functional/apps/visualize/_lab_mode.js b/test/functional/apps/visualize/_lab_mode.js index 3ee806af8165db..b082480d95a2ee 100644 --- a/test/functional/apps/visualize/_lab_mode.js +++ b/test/functional/apps/visualize/_lab_mode.js @@ -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'); }); diff --git a/test/functional/page_objects/header_page.js b/test/functional/page_objects/header_page.js index f82e4e4387e270..05edd64545a565 100644 --- a/test/functional/page_objects/header_page.js +++ b/test/functional/page_objects/header_page.js @@ -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(); } diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.ts similarity index 90% rename from test/functional/page_objects/settings_page.js rename to test/functional/page_objects/settings_page.ts index a4ae361b12ed83..e92780143f09a6 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.ts @@ -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) { 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, + 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(); diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts index b6d13a5604011f..0cc64277efe11b 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_status.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -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'); }); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index f50d4605325560..6567837f653095 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -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'); }); diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index 0934cb0019f44c..c52e6742ddae58 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -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'], diff --git a/x-pack/legacy/plugins/canvas/index.js b/x-pack/legacy/plugins/canvas/index.js index 8e742de6de9448..ebd4f35db8175c 100644 --- a/x-pack/legacy/plugins/canvas/index.js +++ b/x-pack/legacy/plugins/canvas/index.js @@ -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', diff --git a/x-pack/legacy/plugins/dashboard_mode/index.js b/x-pack/legacy/plugins/dashboard_mode/index.js index 4a042498443220..94655adf981b48 100644 --- a/x-pack/legacy/plugins/dashboard_mode/index.js +++ b/x-pack/legacy/plugins/dashboard_mode/index.js @@ -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, }, ], }, diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts index 601a239574e6b5..f798fa5e9f39d4 100644 --- a/x-pack/legacy/plugins/graph/index.ts +++ b/x-pack/legacy/plugins/graph/index.ts @@ -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, diff --git a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx index f34b82d6bb1a3c..d1fcbea2ff5b72 100644 --- a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx +++ b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx @@ -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' diff --git a/x-pack/legacy/plugins/infra/index.ts b/x-pack/legacy/plugins/infra/index.ts index 196950b51be3ab..d9abadcb5125c5 100644 --- a/x-pack/legacy/plugins/infra/index.ts +++ b/x-pack/legacy/plugins/infra/index.ts @@ -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, diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index d38a23560fa9f7..4f679905fc352a 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -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(); diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index c4289389b0d56d..fc1cec7c16208a 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -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'], diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js index ba659aa74f10ca..2b5ea21a2bb452 100644 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ b/x-pack/legacy/plugins/monitoring/ui_exports.js @@ -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(); diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index edbb62feb580f8..f6f2ead2d64fac 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -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: { diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index e090a2c85e1366..cf7332f97d466b 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -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'], }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 88fd8360ec728f..d828556df4fa45 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ad62993d50f062..ac4152328717a0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index ad4f81777e7804..2649c5d26309db 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index ee58be76928b3a..79bb10e0bded16 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index e2d5efac4644cc..7c9c9f9c8c155a 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts index 1ac1784e0e05db..474240b201face 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index d0e37ec8e3f359..71c10bd8248be8 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 28b572401892b5..5395f125bbd22b 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index d25fae3c4894cd..6a6e2f23785e30 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts index ebe08a60c25636..002ae627c488de 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index bab798dacc453b..1189fe909ca320 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -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'); } }); diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 494fd71ea6f34a..9db9a913e9a4b5 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -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', () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts index 4184d223a96864..f917792eea027f 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 1912b16d96f36c..1796858165a2ba 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index e6b6f28f8b92fc..c38dda536f2530 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index a2b062e6ef84fb..37de93a0a7e910 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts index a0b0d5bef96680..d0d0232b5a8b14 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index 30cdc95b38e62e..ed25816e68712b 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts index 6a2b77de17f457..75020d6eab7e4a 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index 5062f094061c03..b7c5667a575065 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -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', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts index 7c2a11a542d66e..90458ef53dfc28 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index b9634c29dac1c9..5008f93feeb015 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -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', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts index 6b078d2cfa71af..61a57e09f96c57 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index 8fb6f21c778d3f..c25c1bfe4b7318 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts index fc94688e98811b..c633852a2da0a7 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index 804ad5725edfd3..ece162cbd96cc7 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts index d985da42ab5eda..130aefb3cae2ac 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts index 7459b53ca4a32f..0465cbcf54541c 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -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'); }); diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 1e79c76bf83e5e..d71d197a6ea199 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -16,6 +16,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { describe('security feature controls', () => { before(async () => { await esArchiver.load('empty_kibana'); + await PageObjects.settings.setNavType('individual'); }); after(async () => { @@ -56,7 +57,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Management'); + expect(navLinks).to.contain('Stack Management'); }); it(`displays Spaces management section`, async () => { @@ -130,7 +131,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Management'); + expect(navLinks).to.contain('Stack Management'); }); it(`doesn't display Spaces management section`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index dea45f161e4510..62483a10552e34 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Management']); + expect(navLinks).to.eql(['Timelion', 'Stack Management']); }); it(`allows a timelion sheet to be created`, async () => { @@ -112,7 +112,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Management']); + expect(navLinks).to.eql(['Timelion', 'Stack Management']); }); it(`does not allow a timelion sheet to be created`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts index fb203a23359bdd..7e0fe731301a64 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -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', 'timelion', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'timelion', + 'security', + 'spaceSelector', + 'settings', + ]); const appsMenu = getService('appsMenu'); describe('timelion', () => { @@ -38,6 +44,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('Timelion'); }); diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index a004f8db66823c..4ff82484db91c4 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Management']); + expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Stack Management']); }); it('can navigate to Uptime app', async () => { @@ -115,7 +115,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Uptime', 'Management']); + expect(navLinks).to.eql(['Uptime', 'Stack Management']); }); it('can navigate to Uptime app', async () => { diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts index 77c5b323340bf8..c3dcb1b27771fb 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts @@ -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('Uptime'); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index d55076cb0ab43b..767dbd71655672 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -75,7 +75,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Management']); + expect(navLinks).to.eql(['Visualize', 'Stack Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -189,7 +189,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Management']); + expect(navLinks).to.eql(['Visualize', 'Stack Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts index 9193862d2ba9e4..066042896c122f 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -10,7 +10,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', 'visualize', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'security', + 'spaceSelector', + 'settings', + ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -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('Visualize'); }); diff --git a/x-pack/test/ui_capabilities/common/nav_links_builder.ts b/x-pack/test/ui_capabilities/common/nav_links_builder.ts index 8b7741469362e8..aaeb22852bcc09 100644 --- a/x-pack/test/ui_capabilities/common/nav_links_builder.ts +++ b/x-pack/test/ui_capabilities/common/nav_links_builder.ts @@ -13,7 +13,7 @@ export class NavLinksBuilder { ...features, // management isn't a first-class "feature", but it makes our life easier here to pretend like it is management: { - navLinkId: 'kibana:management', + navLinkId: 'kibana:stack_management', }, }; } diff --git a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts index 4af7d81e5a7b4f..5c13e6b0eb51eb 100644 --- a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -68,7 +68,7 @@ export class UICapabilitiesService { : {}; const response = await this.axios.post( `${spaceUrlPrefix}/api/core/capabilities`, - { applications: [...applications, 'kibana:management'] }, + { applications: [...applications, 'kibana:stack_management'] }, { headers: requestHeaders, } From fc21c49c3565cadc77c2929c8012df66f244133d Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 21 Jan 2020 19:20:53 +0100 Subject: [PATCH 2/7] [SIEM] Fix Detections page breadcrumbs (#55173) --- package.json | 3 +- .../link_to/redirect_to_detection_engine.tsx | 18 +-- .../navigation/breadcrumbs/index.ts | 22 ++++ .../components/navigation/index.test.tsx | 1 + .../public/components/navigation/index.tsx | 116 ++++++++++-------- .../detection_engine/detection_engine.tsx | 9 +- .../public/pages/detection_engine/index.tsx | 11 +- .../detection_engine/rules/details/index.tsx | 10 +- .../detection_engine/rules/edit/index.tsx | 4 +- .../detection_engine/rules/translations.ts | 8 ++ .../pages/detection_engine/rules/utils.ts | 98 +++++++++++++++ .../siem/public/pages/hosts/details/utils.ts | 6 +- .../public/pages/network/ip_details/utils.ts | 6 +- .../siem/public/utils/route/helpers.ts | 1 + .../siem/public/utils/route/spy_routes.tsx | 20 ++- .../plugins/siem/public/utils/route/types.ts | 10 +- yarn.lock | 12 +- 17 files changed, 262 insertions(+), 93 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts diff --git a/package.json b/package.json index 8b96fcc9d396e7..9707d3863d295d 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,8 @@ "**/react": "^16.12.0", "**/react-test-renderer": "^16.12.0", "**/deepmerge": "^4.2.2", - "**/serialize-javascript": "^2.1.1" + "**/serialize-javascript": "^2.1.1", + "**/fast-deep-equal": "^3.1.1" }, "workspaces": { "packages": [ diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx index 0a91f38061734a..2b7a2f14dfea76 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx @@ -52,11 +52,15 @@ export const RedirectToEditRulePage = ({ location: { search } }: DetectionEngine ); }; -export const getDetectionEngineUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; +const baseDetectionEngineUrl = `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; + +export const getDetectionEngineUrl = () => `${baseDetectionEngineUrl}`; export const getDetectionEngineAlertUrl = () => - `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/${DetectionEngineTab.alerts}`; -export const getRulesUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules`; -export const getCreateRuleUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/create-rule`; -export const getRuleDetailsUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details`; -export const getEditRuleUrl = () => - `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details/edit-rule`; + `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}`; +export const getDetectionEngineTabUrl = (tabPath: string) => `${baseDetectionEngineUrl}/${tabPath}`; +export const getRulesUrl = () => `${baseDetectionEngineUrl}/rules`; +export const getCreateRuleUrl = () => `${baseDetectionEngineUrl}/rules/create`; +export const getRuleDetailsUrl = (detailName: string) => + `${baseDetectionEngineUrl}/rules/id/${detailName}`; +export const getEditRuleUrl = (detailName: string) => + `${baseDetectionEngineUrl}/rules/id/${detailName}/edit`; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index 9eee5b21e83f3c..91055bca066c46 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -10,6 +10,7 @@ import { getOr, omit } from 'lodash/fp'; import { APP_NAME } from '../../../../common/constants'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; +import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; import { SiemPageName } from '../../../pages/home/types'; import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; import { getOverviewUrl } from '../../link_to'; @@ -38,6 +39,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => spyState != null && spyState.pageName === SiemPageName.hosts; +const isDetectionsRoutes = (spyState: RouteSpyState) => + spyState != null && spyState.pageName === SiemPageName.detections; + export const getBreadcrumbsForRoute = ( object: RouteSpyState & TabNavigationProps ): Breadcrumb[] | null => { @@ -76,6 +80,24 @@ export const getBreadcrumbsForRoute = ( ), ]; } + if (isDetectionsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getDetectionRulesBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } if ( spyState != null && object.navTabs && diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index b6efc07ad8fe3b..56be39f67b1bdd 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -187,6 +187,7 @@ describe('SIEM Navigation', () => { query: { language: 'kuery', query: '' }, savedQuery: undefined, search: '', + state: undefined, tabName: 'authentications', timeline: { id: '', isOpen: false }, timerange: { diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index 61ac84667d80f7..040a6e7847b772 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; +import isEqual from 'lodash/fp/isEqual'; +import deepEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; @@ -16,67 +17,78 @@ import { setBreadcrumbs } from './breadcrumbs'; import { TabNavigation } from './tab_navigation'; import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; -export const SiemNavigationComponent = React.memo< - SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState ->( - ({ detailName, display, navTabs, pageName, pathName, search, tabName, urlState, flowTarget }) => { - useEffect(() => { - if (pathName) { - setBreadcrumbs({ - query: urlState.query, - detailName, - filters: urlState.filters, - navTabs, - pageName, - pathName, - savedQuery: urlState.savedQuery, - search, - tabName, - flowTarget, - timerange: urlState.timerange, - timeline: urlState.timeline, - }); - } - }, [pathName, search, navTabs, urlState]); +export const SiemNavigationComponent: React.FC = ({ + detailName, + display, + navTabs, + pageName, + pathName, + search, + tabName, + urlState, + flowTarget, + state, +}) => { + useEffect(() => { + if (pathName) { + setBreadcrumbs({ + query: urlState.query, + detailName, + filters: urlState.filters, + navTabs, + pageName, + pathName, + savedQuery: urlState.savedQuery, + search, + tabName, + flowTarget, + timerange: urlState.timerange, + timeline: urlState.timeline, + state, + }); + } + }, [pathName, search, navTabs, urlState, state]); - return ( - - ); - }, - (prevProps, nextProps) => { - return ( + return ( + + ); +}; + +export const SiemNavigationRedux = compose< + React.ComponentClass +>(connect(makeMapStateToProps))( + React.memo( + SiemNavigationComponent, + (prevProps, nextProps) => prevProps.pathName === nextProps.pathName && prevProps.search === nextProps.search && isEqual(prevProps.navTabs, nextProps.navTabs) && - isEqual(prevProps.urlState, nextProps.urlState) - ); - } + isEqual(prevProps.urlState, nextProps.urlState) && + deepEqual(prevProps.state, nextProps.state) + ) ); -SiemNavigationComponent.displayName = 'SiemNavigationComponent'; - -export const SiemNavigationRedux = compose< - React.ComponentClass ->(connect(makeMapStateToProps))(SiemNavigationComponent); - -export const SiemNavigation = React.memo(props => { +const SiemNavigationContainer: React.FC = props => { const [routeProps] = useRouteSpy(); const stateNavReduxProps: RouteSpyState & SiemNavigationProps = { ...routeProps, ...props, }; + return ; -}); +}; -SiemNavigation.displayName = 'SiemNavigation'; +export const SiemNavigation = SiemNavigationContainer; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index d9e0377b34060e..5586749ce38d83 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -230,8 +230,13 @@ const makeMapStateToProps = () => { }; }; -export const DetectionEngine = connect(makeMapStateToProps, { +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -})(DetectionEngineComponent); +}; + +export const DetectionEngine = connect( + makeMapStateToProps, + mapDispatchToProps +)(DetectionEngineComponent); DetectionEngine.displayName = 'DetectionEngine'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index 33186d2787d8ae..6db8d93e46ac9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -19,7 +19,7 @@ const detectionEnginePath = `/:pageName(detections)`; type Props = Partial> & { url: string }; -export const DetectionEngineContainer = React.memo(() => ( +const DetectionEngineContainerComponent: React.FC = () => ( (() => ( - + - + (() => ( /> -)); -DetectionEngineContainer.displayName = 'DetectionEngineContainer'; +); + +export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index a23c681a5aab2f..40c694160f73bb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -109,7 +109,7 @@ const RuleDetailsComponent = memo( hasIndexWrite, signalIndexName, } = useUserInfo(); - const { ruleId } = useParams(); + const { detailName: ruleId } = useParams(); const [isLoading, rule] = useRule(ruleId); // This is used to re-trigger api rule status when user de/activate rule const [ruleEnabled, setRuleEnabled] = useState(null); @@ -381,7 +381,7 @@ const RuleDetailsComponent = memo( }} - + ); } @@ -402,8 +402,10 @@ const makeMapStateToProps = () => { }; }; -export const RuleDetails = connect(makeMapStateToProps, { +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -})(RuleDetailsComponent); +}; + +export const RuleDetails = connect(makeMapStateToProps, mapDispatchToProps)(RuleDetailsComponent); RuleDetails.displayName = 'RuleDetails'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 9b7833afd7f4da..be56e916ae6c94 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -57,7 +57,7 @@ export const EditRuleComponent = memo(() => { canUserCRUD, hasManageApiKey, } = useUserInfo(); - const { ruleId } = useParams(); + const { detailName: ruleId } = useParams(); const [loading, rule] = useRule(ruleId); const userHasNoPermissions = @@ -347,7 +347,7 @@ export const EditRuleComponent = memo(() => { - + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index e1257007d44a3b..d144a6d56a1687 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -25,6 +25,14 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageT defaultMessage: 'Signal detection rules', }); +export const ADD_PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.addPageTitle', { + defaultMessage: 'Create', +}); + +export const EDIT_PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.editPageTitle', { + defaultMessage: 'Edit', +}); + export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules.refreshTitle', { defaultMessage: 'Refresh', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts new file mode 100644 index 00000000000000..55772aa73ecf33 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Breadcrumb } from 'ui/chrome'; +import { isEmpty } from 'lodash/fp'; + +import { + getDetectionEngineUrl, + getDetectionEngineTabUrl, + getRulesUrl, + getRuleDetailsUrl, + getCreateRuleUrl, + getEditRuleUrl, +} from '../../../components/link_to/redirect_to_detection_engine'; +import * as i18nDetections from '../translations'; +import * as i18nRules from './translations'; +import { RouteSpyState } from '../../../utils/route/types'; + +const getTabBreadcrumb = (pathname: string, search: string[]) => { + const tabPath = pathname.split('/')[2]; + + if (tabPath === 'alerts') { + return { + text: i18nDetections.ALERT, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'signals') { + return { + text: i18nDetections.SIGNAL, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'rules') { + return { + text: i18nRules.PAGE_TITLE, + href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } +}; + +const isRuleCreatePage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/create'); + +const isRuleEditPage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/edit'); + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => { + let breadcrumb = [ + { + text: i18nDetections.PAGE_TITLE, + href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + + const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); + + if (tabBreadcrumb) { + breadcrumb = [...breadcrumb, tabBreadcrumb]; + } + + if (params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state.ruleName, + href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleCreatePage(params.pathName)) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.ADD_PAGE_TITLE, + href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.EDIT_PAGE_TITLE, + href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts index 52e016502940b1..c321478f101741 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts @@ -5,8 +5,8 @@ */ import { Breadcrumb } from 'ui/chrome'; +import { get, isEmpty } from 'lodash/fp'; -import { get } from 'lodash/fp'; import { hostsModel } from '../../../store'; import { HostsTableType } from '../../../store/hosts/model'; import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; @@ -29,7 +29,7 @@ export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): Bre let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: `${getHostsUrl()}${search && search[0] ? search[0] : ''}`, + href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, }, ]; @@ -38,7 +38,7 @@ export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): Bre ...breadcrumb, { text: params.detailName, - href: `${getHostDetailsUrl(params.detailName)}${search && search[1] ? search[1] : ''}`, + href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, }, ]; } diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts index fed832167a60e7..a468812e2718d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts @@ -5,8 +5,8 @@ */ import { Breadcrumb } from 'ui/chrome'; +import { get, isEmpty } from 'lodash/fp'; -import { get } from 'lodash/fp'; import { decodeIpv6 } from '../../../lib/helpers'; import { getNetworkUrl, getIPDetailsUrl } from '../../../components/link_to/redirect_to_network'; import { networkModel } from '../../../store/network'; @@ -28,7 +28,7 @@ export const getBreadcrumbs = (params: NetworkRouteSpyState, search: string[]): let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: `${getNetworkUrl()}${search && search[0] ? search[0] : ''}`, + href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, }, ]; if (params.detailName != null) { @@ -37,7 +37,7 @@ export const getBreadcrumbs = (params: NetworkRouteSpyState, search: string[]): { text: decodeIpv6(params.detailName), href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${ - search && search[1] ? search[1] : '' + !isEmpty(search[1]) ? search[1] : '' }`, }, ]; diff --git a/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts b/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts index 188ae9c6c1866d..39efccc9f45b85 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts @@ -15,6 +15,7 @@ export const initRouteSpy: RouteSpyState = { tabName: undefined, search: '', pathName: '/', + state: undefined, }; export const RouterSpyStateContext = createContext<[RouteSpyState, Dispatch]>([ diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx index 5c24b2f48488d8..c88562abef6ae0 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx @@ -8,6 +8,7 @@ import * as H from 'history'; import { isEqual } from 'lodash/fp'; import { memo, useEffect, useState } from 'react'; import { withRouter } from 'react-router-dom'; +import deepEqual from 'fast-deep-equal'; import { SpyRouteProps } from './types'; import { useRouteSpy } from './use_route_spy'; @@ -19,6 +20,7 @@ export const SpyRouteComponent = memo( match: { params: { pageName, detailName, tabName, flowTarget }, }, + state, }) => { const [isInitializing, setIsInitializing] = useState(true); const [route, dispatch] = useRouteSpy(); @@ -61,8 +63,24 @@ export const SpyRouteComponent = memo( }, }); } + } else { + if (pageName && !deepEqual(state, route.state)) { + dispatch({ + type: 'updateRoute', + route: { + pageName, + detailName, + tabName, + search, + pathName: pathname, + history, + flowTarget, + state, + }, + }); + } } - }, [pathname, search, pageName, detailName, tabName, flowTarget]); + }, [pathname, search, pageName, detailName, tabName, flowTarget, state]); return null; } ); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/types.ts b/x-pack/legacy/plugins/siem/public/utils/route/types.ts index 79d2677eff06f6..d3eca36bd0d96d 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/types.ts +++ b/x-pack/legacy/plugins/siem/public/utils/route/types.ts @@ -21,6 +21,7 @@ export interface RouteSpyState { pathName: string; history?: H.History; flowTarget?: FlowTarget; + state?: Record; } export interface HostRouteSpyState extends RouteSpyState { @@ -38,7 +39,10 @@ export type RouteSpyAction = } | { type: 'updateRouteWithOutSearch'; - route: Pick; + route: Pick< + RouteSpyState, + 'pageName' & 'detailName' & 'tabName' & 'pathName' & 'history' & 'state' + >; } | { type: 'updateRoute'; @@ -55,4 +59,6 @@ export type SpyRouteProps = RouteComponentProps<{ tabName: HostsTableType | undefined; search: string; flowTarget: FlowTarget | undefined; -}>; +}> & { + state?: Record; +}; diff --git a/yarn.lock b/yarn.lock index 198174d0132fae..3563ee3fc2733f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13308,17 +13308,7 @@ fancy-log@^1.3.2: color-support "^1.1.3" time-stamp "^1.0.0" -fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" - integrity sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-deep-equal@^3.1.1: +fast-deep-equal@^1.0.0, fast-deep-equal@^2.0.1, fast-deep-equal@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== From 740d4d1afa7ea04f350855d2547d5e4f461e292a Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 21 Jan 2020 13:23:49 -0500 Subject: [PATCH 3/7] [File upload] Enforce file-type check in file dialog (#55063) --- .../public/components/json_index_file_picker.js | 9 ++++++++- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js b/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js index f1e74919d734b3..0ee4f76ebf9d0c 100644 --- a/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js +++ b/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js @@ -13,6 +13,8 @@ import { MAX_FILE_SIZE } from '../../common/constants/file_import'; import _ from 'lodash'; const ACCEPTABLE_FILETYPES = ['json', 'geojson']; +const acceptedFileTypeString = ACCEPTABLE_FILETYPES.map(type => `.${type}`).join(','); +const acceptedFileTypeStringMessage = ACCEPTABLE_FILETYPES.map(type => `.${type}`).join(', '); export class JsonIndexFilePicker extends Component { state = { @@ -103,6 +105,7 @@ export class JsonIndexFilePicker extends Component { const splitNameArr = name.split('.'); const fileType = splitNameArr.pop(); if (!ACCEPTABLE_FILETYPES.includes(fileType)) { + //should only occur if browser does not accept the accept parameter throw new Error( i18n.translate('xpack.fileUpload.jsonIndexFilePicker.acceptableTypesError', { defaultMessage: 'File is not one of acceptable types: {types}', @@ -252,7 +255,10 @@ export class JsonIndexFilePicker extends Component { ) : ( {i18n.translate('xpack.fileUpload.jsonIndexFilePicker.formatsAccepted', { - defaultMessage: 'Formats accepted: .json, .geojson', + defaultMessage: 'Formats accepted: {acceptedFileTypeStringMessage}', + values: { + acceptedFileTypeStringMessage, + }, })}{' '}
} onChange={this._fileHandler} + accept={acceptedFileTypeString} /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d828556df4fa45..f61dfa8d886c28 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5668,7 +5668,6 @@ "xpack.fileUpload.jsonIndexFilePicker.filePickerLabel": "アップロードするファイルを選択", "xpack.fileUpload.jsonIndexFilePicker.fileProcessingError": "ファイル処理エラー: {errorMessage}", "xpack.fileUpload.jsonIndexFilePicker.fileSizeError": "ファイルサイズエラー: {errorMessage}", - "xpack.fileUpload.jsonIndexFilePicker.formatsAccepted": "許可されている形式:.json、.geojson", "xpack.fileUpload.jsonIndexFilePicker.maxSize": "最大サイズ:{maxFileSize}", "xpack.fileUpload.jsonIndexFilePicker.noFileNameError": "ファイル名が指定されていません", "xpack.fileUpload.jsonIndexFilePicker.parsingFile": "{featuresProcessed} 件の機能が解析されました…", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ac4152328717a0..2c2e5325969838 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5667,7 +5667,6 @@ "xpack.fileUpload.jsonIndexFilePicker.filePickerLabel": "选择文件进行上传", "xpack.fileUpload.jsonIndexFilePicker.fileProcessingError": "文件处理错误:{errorMessage}", "xpack.fileUpload.jsonIndexFilePicker.fileSizeError": "文件大小错误:{errorMessage}", - "xpack.fileUpload.jsonIndexFilePicker.formatsAccepted": "接受的格式:.json、.geojson", "xpack.fileUpload.jsonIndexFilePicker.maxSize": "最大大小:{maxFileSize}", "xpack.fileUpload.jsonIndexFilePicker.noFileNameError": "未提供任何文件名称", "xpack.fileUpload.jsonIndexFilePicker.parsingFile": "{featuresProcessed} 特征已解析......", From 16b5fd7e04048d37d49081071a4c5c9bae2877a7 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 21 Jan 2020 13:41:36 -0500 Subject: [PATCH 4/7] [Uptime] Handle locations with names but no geo data (#55234) * Handle locations with names but no geo data. * Fix broken types, add a comment explaining some weird ts-related code. Co-authored-by: Elastic Machine --- .../location_map/embeddables/embedded_map.tsx | 6 ++--- .../location_map/embeddables/map_config.ts | 4 ++-- .../functional/location_map/location_map.tsx | 24 ++++++++++++------- .../elasticsearch_monitors_adapter.ts | 14 ++++++----- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx index 98780d23c5a629..11f6565734782e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx @@ -12,6 +12,7 @@ import { start } from '../../../../../../../../../src/legacy/core_plugins/embedd import * as i18n from './translations'; // @ts-ignore import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; +import { Location } from '../../../../../common/runtime_types'; import { MapEmbeddable } from './types'; import { getLayerList } from './map_config'; @@ -22,10 +23,7 @@ export interface EmbeddedMapProps { downPoints: LocationPoint[]; } -export interface LocationPoint { - lat: string; - lon: string; -} +export type LocationPoint = Required; const EmbeddedPanel = styled.div` z-index: auto; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts index d4601baefdf30c..a43edae4382527 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts @@ -5,7 +5,7 @@ */ import lowPolyLayerFeatures from './low_poly_layer.json'; -import { LocationPoint } from './embedded_map'; +import { LocationPoint } from './embedded_map.js'; import { UptimeAppColors } from '../../../../uptime_app'; /** @@ -16,7 +16,7 @@ import { UptimeAppColors } from '../../../../uptime_app'; export const getLayerList = ( upPoints: LocationPoint[], downPoints: LocationPoint[], - { gray, danger }: Pick + { danger }: Pick ) => { return [getLowPolyLayer(), getDownPointsLayer(downPoints, danger), getUpPointsLayer(upPoints)]; }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx index d35e1281260e2e..c93e16d0a080b5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx @@ -9,7 +9,7 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary } from '@elastic/eui'; import { LocationStatusTags } from './location_status_tags'; import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; -import { MonitorLocations } from '../../../../common/runtime_types'; +import { MonitorLocations, MonitorLocation } from '../../../../common/runtime_types'; import { UNNAMED_LOCATION } from '../../../../common/constants'; import { LocationMissingWarning } from './location_missing'; @@ -32,15 +32,23 @@ export const LocationMap = ({ monitorLocations }: LocationMapProps) => { let isGeoInfoMissing = false; if (monitorLocations?.locations) { - monitorLocations.locations.forEach((item: any) => { - if (item.geo?.name !== UNNAMED_LOCATION) { - if (item.summary.down === 0) { - upPoints.push(item.geo.location); + monitorLocations.locations.forEach((item: MonitorLocation) => { + if (item.geo?.name === UNNAMED_LOCATION || !item.geo?.location) { + isGeoInfoMissing = true; + } else if ( + item.geo?.name !== UNNAMED_LOCATION && + !!item.geo.location.lat && + !!item.geo.location.lon + ) { + // TypeScript doesn't infer that the above checks in this block's condition + // ensure that lat and lon are defined when we try to pass the location object directly, + // but if we destructure the values it does. Improvement to this block is welcome. + const { lat, lon } = item.geo.location; + if (item?.summary?.down === 0) { + upPoints.push({ lat, lon }); } else { - downPoints.push(item.geo.location); + downPoints.push({ lat, lon }); } - } else if (item.geo?.name === UNNAMED_LOCATION) { - isGeoInfoMissing = true; } }); } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts index c86e0db9ae04ad..e433931f03c8e6 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts @@ -346,16 +346,18 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = { const result = await callES('search', params); const locations = result?.aggregations?.location?.buckets ?? []; - const getGeo = (locGeo: any) => { + const getGeo = (locGeo: { name: string; location?: string }) => { if (locGeo) { const { name, location } = locGeo; - const latLon = location.trim().split(','); + const latLon = location?.trim().split(','); return { name, - location: { - lat: latLon[0], - lon: latLon[1], - }, + location: latLon + ? { + lat: latLon[0], + lon: latLon[1], + } + : undefined, }; } else { return { From 85edc661253617fbeafab207da3d5ed88273f04f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 21 Jan 2020 19:42:22 +0100 Subject: [PATCH 5/7] =?UTF-8?q?[Logs=20UI]=20Use=20the=20correct=20icons?= =?UTF-8?q?=20and=20labels=20in=20the=20feature=20cont=E2=80=A6=20(#55292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes logs and metrics icons as well as the metrics label in the feature control lists which were missed during the renaming. fixes #55283 --- x-pack/legacy/plugins/infra/server/features.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/infra/server/features.ts b/x-pack/legacy/plugins/infra/server/features.ts index fc20813c777b69..02658002694d27 100644 --- a/x-pack/legacy/plugins/infra/server/features.ts +++ b/x-pack/legacy/plugins/infra/server/features.ts @@ -9,9 +9,9 @@ import { i18n } from '@kbn/i18n'; export const METRICS_FEATURE = { id: 'infrastructure', name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { - defaultMessage: 'Infrastructure', + defaultMessage: 'Metrics', }), - icon: 'infraApp', + icon: 'metricsApp', navLinkId: 'infra:home', app: ['infra', 'kibana'], catalogue: ['infraops'], @@ -40,7 +40,7 @@ export const LOGS_FEATURE = { name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { defaultMessage: 'Logs', }), - icon: 'loggingApp', + icon: 'logsApp', navLinkId: 'infra:logs', app: ['infra', 'kibana'], catalogue: ['infralogging'], From 5a5bade8be51c2d93a3535dc9952e7ca47309dc0 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 21 Jan 2020 14:21:33 -0500 Subject: [PATCH 6/7] [Endpoint] Fix saga to start only after store is created and stopped on app unmount (#55245) - added `stop()`/`start()` methods to the Saga Middleware creator factory - adjust tests based on changes - changed application `renderApp` to stop sagas when react app is unmounted --- .../public/applications/endpoint/index.tsx | 3 +- .../applications/endpoint/lib/saga.test.ts | 57 ++++++++++++------- .../public/applications/endpoint/lib/saga.ts | 40 +++++++++++-- .../endpoint/store/endpoint_list/saga.test.ts | 15 +++-- .../applications/endpoint/store/index.ts | 11 ++-- 5 files changed, 90 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index d69e068bdea3ad..7598141bdea659 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -19,12 +19,13 @@ import { appStoreFactory } from './store'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); - const store = appStoreFactory(coreStart); + const [store, stopSagas] = appStoreFactory(coreStart); ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); + stopSagas(); }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts index 0387eac0e7c7fa..91841f75c24fec 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts @@ -3,18 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { createSagaMiddleware, SagaContext } from './index'; -import { applyMiddleware, createStore, Reducer } from 'redux'; + +import { createSagaMiddleware, SagaContext, SagaMiddleware } from './index'; +import { applyMiddleware, createStore, Reducer, Store } from 'redux'; describe('saga', () => { const INCREMENT_COUNTER = 'INCREMENT'; const DELAYED_INCREMENT_COUNTER = 'DELAYED INCREMENT COUNTER'; const STOP_SAGA_PROCESSING = 'BREAK ASYNC ITERATOR'; - const sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms)); + const sleep = (ms = 10) => new Promise(resolve => setTimeout(resolve, ms)); + let store: Store; let reducerA: Reducer; let sideAffect: (a: unknown, s: unknown) => void; let sagaExe: (sagaContext: SagaContext) => Promise; + let sagaExeReduxMiddleware: SagaMiddleware; beforeEach(() => { reducerA = jest.fn((prevState = { count: 0 }, { type }) => { @@ -47,53 +50,63 @@ describe('saga', () => { } } }); + + sagaExeReduxMiddleware = createSagaMiddleware(sagaExe); + store = createStore(reducerA, applyMiddleware(sagaExeReduxMiddleware)); }); - test('it returns Redux Middleware from createSagaMiddleware()', () => { - const sagaMiddleware = createSagaMiddleware(async () => {}); - expect(sagaMiddleware).toBeInstanceOf(Function); + afterEach(() => { + sagaExeReduxMiddleware.stop(); }); + test('it does nothing if saga is not started', () => { - const store = createStore(reducerA, applyMiddleware(createSagaMiddleware(sagaExe))); - expect(store.getState().count).toEqual(0); - expect(reducerA).toHaveBeenCalled(); - expect(sagaExe).toHaveBeenCalled(); - expect(sideAffect).not.toHaveBeenCalled(); - expect(store.getState()).toEqual({ count: 0 }); + expect(sagaExe).not.toHaveBeenCalled(); }); - test('it updates store once running', async () => { - const sagaMiddleware = createSagaMiddleware(sagaExe); - const store = createStore(reducerA, applyMiddleware(sagaMiddleware)); + test('it can dispatch store actions once running', async () => { + sagaExeReduxMiddleware.start(); expect(store.getState()).toEqual({ count: 0 }); expect(sagaExe).toHaveBeenCalled(); store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); expect(store.getState()).toEqual({ count: 0 }); - await sleep(100); + await sleep(); expect(sideAffect).toHaveBeenCalled(); expect(store.getState()).toEqual({ count: 1 }); }); - test('it stops processing if break out of loop', async () => { - const sagaMiddleware = createSagaMiddleware(sagaExe); - const store = createStore(reducerA, applyMiddleware(sagaMiddleware)); + test('it stops processing if break out of loop', async () => { + sagaExeReduxMiddleware.start(); store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - await sleep(100); + await sleep(); expect(store.getState()).toEqual({ count: 1 }); expect(sideAffect).toHaveBeenCalledTimes(2); store.dispatch({ type: STOP_SAGA_PROCESSING }); - await sleep(100); + await sleep(); + + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); + await sleep(); + + expect(store.getState()).toEqual({ count: 1 }); + expect(sideAffect).toHaveBeenCalledTimes(2); + }); + + test('it stops saga middleware when stop() is called', async () => { + sagaExeReduxMiddleware.start(); + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); + await sleep(); expect(store.getState()).toEqual({ count: 1 }); expect(sideAffect).toHaveBeenCalledTimes(2); + sagaExeReduxMiddleware.stop(); + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - await sleep(100); + await sleep(); expect(store.getState()).toEqual({ count: 1 }); expect(sideAffect).toHaveBeenCalledTimes(2); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts index b93360ec6b5aa7..bca6aa6563fe52 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -35,7 +35,20 @@ export interface SagaContext { dispatch: Dispatch; } +export interface SagaMiddleware extends Middleware { + /** + * Start the saga. Should be called after the `store` has been created + */ + start: () => void; + + /** + * Stop the saga by exiting the internal generator `for await...of` loop. + */ + stop: () => void; +} + const noop = () => {}; +const STOP = Symbol('STOP'); /** * Creates Saga Middleware for use with Redux. @@ -43,7 +56,7 @@ const noop = () => {}; * @param {Saga} saga The `saga` should initialize a long-running `for await...of` loop against * the return value of the `actionsAndState()` method provided by the `SagaContext`. * - * @return {Middleware} + * @return {SagaMiddleware} * * @example * @@ -64,22 +77,31 @@ const noop = () => {}; * //.... * const store = createStore(reducers, [ endpointsSagaMiddleware ]); */ -export function createSagaMiddleware(saga: Saga): Middleware { +export function createSagaMiddleware(saga: Saga): SagaMiddleware { const iteratorInstances = new Set(); let runSaga: () => void = noop; + let stopSaga: () => void = noop; + let runningPromise: Promise; async function* getActionsAndStateIterator(): StoreActionsAndState { const instance: IteratorInstance = { queue: [], nextResolve: null }; iteratorInstances.add(instance); + try { while (true) { - yield await nextActionAndState(); + const actionAndState = await Promise.race([nextActionAndState(), runningPromise]); + + if (actionAndState === STOP) { + break; + } + + yield actionAndState as QueuedAction; } } finally { // If the consumer stops consuming this (e.g. `break` or `return` is called in the `for await` // then this `finally` block will run and unregister this instance and reset `runSaga` iteratorInstances.delete(instance); - runSaga = noop; + runSaga = stopSaga = noop; } function nextActionAndState() { @@ -109,7 +131,6 @@ export function createSagaMiddleware(saga: Saga): Middleware { actionsAndState: getActionsAndStateIterator, dispatch, }); - runSaga(); } return (next: Dispatch) => (action: AnyAction) => { // Call the next dispatch method in the middleware chain. @@ -125,5 +146,14 @@ export function createSagaMiddleware(saga: Saga): Middleware { }; } + middleware.start = () => { + runningPromise = new Promise(resolve => (stopSaga = () => resolve(STOP))); + runSaga(); + }; + + middleware.stop = () => { + stopSaga(); + }; + return middleware; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts index 92bf3b7fd92dd8..6bf946873e1797 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts @@ -24,6 +24,7 @@ describe('endpoint list saga', () => { let fakeHttpServices: jest.Mocked; let store: Store; let dispatch: Dispatch; + let stopSagas: () => void; // TODO: consolidate the below ++ helpers in `index.test.ts` into a `test_helpers.ts`?? const generateEndpoint = (): EndpointData => { @@ -89,13 +90,19 @@ describe('endpoint list saga', () => { beforeEach(() => { fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); fakeHttpServices = fakeCoreStart.http as jest.Mocked; - store = createStore( - endpointListReducer, - applyMiddleware(createSagaMiddleware(endpointListSagaFactory())) - ); + + const sagaMiddleware = createSagaMiddleware(endpointListSagaFactory()); + store = createStore(endpointListReducer, applyMiddleware(sagaMiddleware)); + + sagaMiddleware.start(); + stopSagas = sagaMiddleware.stop; dispatch = store.dispatch; }); + afterEach(() => { + stopSagas(); + }); + test('it handles `userEnteredEndpointListPage`', async () => { const apiResponse = getEndpointListApiResponse(); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index d0dc002031ce2f..bfa1385b9f0ac0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore, compose, applyMiddleware } from 'redux'; +import { createStore, compose, applyMiddleware, Store } from 'redux'; import { CoreStart } from 'kibana/public'; import { appSagaFactory } from './saga'; import { appReducer } from './reducer'; @@ -15,10 +15,13 @@ const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMP ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'EndpointApp' }) : compose; -export const appStoreFactory = (coreStart: CoreStart) => { +export const appStoreFactory = (coreStart: CoreStart): [Store, () => void] => { + const sagaReduxMiddleware = appSagaFactory(coreStart); const store = createStore( appReducer, - composeWithReduxDevTools(applyMiddleware(appSagaFactory(coreStart))) + composeWithReduxDevTools(applyMiddleware(sagaReduxMiddleware)) ); - return store; + + sagaReduxMiddleware.start(); + return [store, sagaReduxMiddleware.stop]; }; From 0ac60494d676ddfd763024d57451618cf8792013 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 21 Jan 2020 14:01:28 -0600 Subject: [PATCH 7/7] [DOCS] Updates to heat map page (#55097) --- docs/user/visualize.asciidoc | 43 ++++++-- docs/visualize/aggregations.asciidoc | 144 +++++++++++--------------- docs/visualize/heatmap.asciidoc | 40 +++++++ docs/visualize/lens.asciidoc | 16 ++- docs/visualize/metric.asciidoc | 12 +++ docs/visualize/most-frequent.asciidoc | 37 ++++--- docs/visualize/regionmap.asciidoc | 15 +-- docs/visualize/tilemap.asciidoc | 31 +----- docs/visualize/tsvb.asciidoc | 24 +++-- 9 files changed, 211 insertions(+), 151 deletions(-) create mode 100644 docs/visualize/heatmap.asciidoc diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index e69d62daf74353..cfd2bac4989c11 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -3,9 +3,9 @@ [partintro] -- -_Visualize_ enables you to create visualizations of the data from your Elasticsearch indices, which you can then add to dashboards for analysis. +_Visualize_ enables you to create visualizations of the data from your {es} indices, which you can then add to dashboards for analysis. -{kib} visualizations are based on Elasticsearch queries. By using a series of {es} {ref}/search-aggregations.html[aggregations] to extract and process your data, you can create charts that show you the trends, spikes, and dips you need to know about. +{kib} visualizations are based on {es} queries. By using a series of {es} {ref}/search-aggregations.html[aggregations] to extract and process your data, you can create charts that show you the trends, spikes, and dips you need to know about. [float] [[create-a-visualization]] @@ -15,42 +15,59 @@ _Visualize_ enables you to create visualizations of the data from your Elasticse . Click *Create new visualization*. . Choose the visualization type: + -* *Basic charts* -<>:: Quickly build several types of basic visualizations by simply dragging and dropping the data fields you want to display. +* Basic charts +[horizontal] +<>:: +Quickly build several types of basic visualizations by simply dragging and dropping the data fields you want to display. + * *<>* [horizontal] Line, area, and bar charts:: Compare different series in X/Y charts. + Pie chart:: Display each source contribution to a total. + Data table:: Flattens aggregations into table format. + Metric:: Display a single number. + Goal and gauge:: Display a number with progress indicators. -Heat maps:: Display shaded cells within a matrix. + Tag cloud:: Display words in a cloud, where the size of the word corresponds to its importance. + * *Time series optimized* [horizontal] <>:: Visualize time series data using pipeline aggregations. + <>:: Compute and combine data from multiple time series data sets. + * *Maps* [horizontal] -<>:: The most powerful way of visualizing map data in {kib}. -<>:: Displays points on a map using a geohash aggregation. -<>:: Merge any structured map data onto a shape. -* *<>* +<>:: The most powerful way of visualizing map data in {kib}. + +<>:: Displays points on a map using a geohash aggregation. + +<>:: Merge any structured map data onto a shape. + +<>:: Display shaded cells within a matrix. + +* *<>* [horizontal] <>:: Provides the ability to add interactive inputs to a Dashboard. + <>:: Display free-form information or instructions. + * *For developers* [horizontal] <>:: Complete control over query and display. . Specify a search query to retrieve the data for your visualization: ** To enter new search criteria, select the <> for the indices that -contain the data you want to visualize. This opens the visualization builder +contain the data you want to visualize. The visualization builder opens with a wildcard query that matches all of the documents in the selected indices. ** To build a visualization from a saved search, click the name of the saved -search you want to use. This opens the visualization builder and loads the +search you want to use. The visualization builder opens and loads the selected query. + NOTE: When you build a visualization from a saved search, any subsequent @@ -58,9 +75,12 @@ modifications to the saved search are automatically reflected in the visualization. To disable automatic updates, you can disconnect a visualization from the saved search. + -- include::{kib-repo-dir}/visualize/visualize_rollup_data.asciidoc[] +include::{kib-repo-dir}/visualize/aggregations.asciidoc[] + include::{kib-repo-dir}/visualize/lens.asciidoc[] include::{kib-repo-dir}/visualize/most-frequent.asciidoc[] @@ -70,6 +90,7 @@ include::{kib-repo-dir}/visualize/timelion.asciidoc[] include::{kib-repo-dir}/visualize/tilemap.asciidoc[] include::{kib-repo-dir}/visualize/regionmap.asciidoc[] +include::{kib-repo-dir}/visualize/heatmap.asciidoc[] include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] diff --git a/docs/visualize/aggregations.asciidoc b/docs/visualize/aggregations.asciidoc index 36ddb0063dfc30..95aa586e6ba180 100644 --- a/docs/visualize/aggregations.asciidoc +++ b/docs/visualize/aggregations.asciidoc @@ -1,136 +1,112 @@ [[supported-aggregations]] -=== Supported aggregations +== Supported aggregations -The most frequently used visualizations support the following aggregations. +Use the supported aggregations to build your visualizations. [float] [[visualize-metric-aggregations]] -==== Metric aggregations +=== Metric aggregations -The *Count* metric lets you visualize the number of documents in a bucket. -If there are no bucket aggregations defined, this is the total number of documents that match the query. -It is the default selection. - -All other metric aggregations require a field selection, which will read from the indexed values. Alternatively, -you can override field values with a script using the <>. The -other metric aggregations are: +Metric aggregations extract field from documents to generate data values. {ref}/search-aggregations-metrics-avg-aggregation.html[Average]:: The mean value. -{ref}/search-aggregations-metrics-max-aggregation.html[Maximum]:: The highest value. -{ref}/search-aggregations-metrics-percentile-aggregation.html[Median]:: The value that is in the 50% percentile. -{ref}/search-aggregations-metrics-min-aggregation.html[Minimum]:: The lowest value. -{ref}/search-aggregations-metrics-sum-aggregation.html[Sum]:: The total value. -Unique Count:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[Cardinality] of the field within the bucket. -Supports any data type. +{ref}/search-aggregations-metrics-valuecount-aggregation.html[Count]:: The total number of documents that match the query, which allows you to visualize the number of documents in a bucket. Count is the default value. -Standard Deviation:: Requires a numeric field. Uses the {ref}/search-aggregations-metrics-extendedstats-aggregation.html[_extended stats_] aggregation. +{ref}/search-aggregations-metrics-max-aggregation.html[Max]:: The highest value. + +{ref}/search-aggregations-metrics-percentile-aggregation.html[Median]:: The value that is in the 50% percentile. + +{ref}/search-aggregations-metrics-min-aggregation.html[Min]:: The lowest value. -{ref}/search-aggregations-metrics-top-hits-aggregation.html[Top Hit]:: Returns a sample of individual documents. When the Top Hit aggregation is matched to more than one document, you must choose a technique for combining the values. Techniques include average, minimum, maximum, and sum. +{ref}/search-aggregations-metrics-percentile-rank-aggregation.html[Percentile ranks]:: Returns the percentile rankings for the values in the specified numeric field. Select a numeric field from the drop-down, then specify one or more percentile rank values in the *Values* fields. {ref}/search-aggregations-metrics-percentile-aggregation.html[Percentiles]:: Divides the -values in a numeric field into specified percentile bands. Select a field from the drop-down, then specify one or more ranges in the *Percentiles* fields. Click the *X* to remove a percentile field. Click *+ Add* to add a percentile field. +values in a numeric field into specified percentile bands. Select a field from the drop-down, then specify one or more ranges in the *Percentiles* fields. -{ref}/search-aggregations-metrics-percentile-rank-aggregation.html[Percentile Rank]:: Returns the percentile rankings for the values in the specified numeric field. Select a numeric field from the drop-down, then specify one or more percentile rank values in the *Values* fields. Click the *X* to remove a values field. Click *+Add* to add a values field. +Standard Deviation:: Requires a numeric field. Uses the {ref}/search-aggregations-metrics-extendedstats-aggregation.html[_extended stats_] aggregation. -[float] -[[visualize-sibling-pipeline-aggregations]] -==== Sibling pipeline aggregations +{ref}/search-aggregations-metrics-sum-aggregation.html[Sum]:: The total value. -For each of the sibling pipeline aggregations you have to define a bucket and metric to calculate. This -has the effect of condensing many buckets into one number. +{ref}/search-aggregations-metrics-top-hits-aggregation.html[Top hit]:: Returns a sample of individual documents. When the Top Hit aggregation is matched to more than one document, you must choose a technique for combining the values. Techniques include average, minimum, maximum, and sum. -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Average Bucket]:: Calculates the mean, or average, value of a specified metric in a sibling aggregation. +Unique Count:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[Cardinality] of the field within the bucket. -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Sum Bucket]:: Calculates the sum of the values of a specified metric in a sibling aggregation. +Alternatively, you can override the field values with a script using JSON input. For example: -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Min Bucket]:: Calculates the minimum value of a specified metric in a sibling aggregation. +[source,shell] +{ "script" : "doc['grade'].value * 1.2" } -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Max Bucket]:: Calculates the maximum value of a specified metric in a sibling aggregation. +The example implements a {es} {ref}/search-aggregations.html[Script Value Source], which replaces +the value in the metric. The options available depend on the aggregation you choose. [float] -[[visualize-bucket-aggregations]] -==== Bucket aggregations - -{ref}/search-aggregations-bucket-datehistogram-aggregation.html[Date Histogram]:: Splits a date field into buckets by interval. If the date field is the primary time field for the index pattern, it will pick an automatic interval for you. You can also choose a minimum time interval, or specify a custom interval frame by selecting *Custom* as the interval and -specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes, -*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision, -down to one millisecond. Intervals are labeled at the start of the interval, using the date-key returned by Elasticsearch.For example, the tooltip for a monthly interval will show the first day of the month. +[[visualize-parent-pipeline-aggregations]] +=== Parent pipeline aggregations -{ref}/search-aggregations-bucket-histogram-aggregation.html[Histogram]:: Builds from a numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty intervals in the histogram. +Parent pipeline aggregations assume the bucket aggregations are ordered and are especially useful for time series data. For each parent pipeline aggregation, you must define a bucket aggregation and metric aggregation. -{ref}/search-aggregations-bucket-range-aggregation.html[Range]:: Specify ranges of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove a range. +You can also nest these aggregations. For example, if you want to produce a third derivative. -{ref}/search-aggregations-bucket-daterange-aggregation.html[Date Range]:: Reports values that are within a range of dates that you specify. You can specify the ranges for the dates using {ref}/common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints. -Click the red *(x)* symbol to remove a range. +{ref}/search-aggregations-pipeline-bucket-script-aggregation.html[Bucket script]:: Executes a script that performs computations for each bucket that specifies metrics in the parent multi-bucket aggregation. -{ref}/search-aggregations-bucket-iprange-aggregation.html[IPv4 Range]:: Specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove a range. +{ref}/search-aggregations-pipeline-cumulative-sum-aggregation.html[Cumulative sum]:: Calculates the cumulative sum of a specified metric in a parent histogram. -*Filters*:: Each filter creates a bucket of documents. You can specify a filter as a -<> or <> query string. Click *Add Filter* to -add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where -you can type in a name to display on the visualization. +{ref}/search-aggregations-pipeline-derivative-aggregation.html[Derivative]:: Calculates the derivative of specific metrics. -{ref}/search-aggregations-bucket-terms-aggregation.html[Terms]:: Specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. +{ref}/search-aggregations-pipeline-movavg-aggregation.html[Moving avg]:: Slides a window across the data and emits the average value of the window. -{ref}/search-aggregations-bucket-significantterms-aggregation.html[Significant Terms]:: Returns interesting or unusual occurrences of terms in a set. +{ref}/search-aggregations-pipeline-serialdiff-aggregation.html[Serial diff]:: Values in a time series are subtracted from itself at different time lags or periods. -Both Terms and Significant Terms support {es} {ref}/search-aggregations-bucket-terms-aggregation.html#_filtering_values_4[exclude and include patterns] which -are available by clicking *Advanced* after selecting a field. +Custom {kib} plugins can <>, which includes support for adding more aggregations. -Kibana only supports filtering string fields with regular expression patterns, it does not support matching with arrays or filtering numeric fields. -Patterns are case sensitive. +[float] +[[visualize-sibling-pipeline-aggregations]] +=== Sibling pipeline aggregations -Example: +Sibling pipeline aggregations condense many buckets into one. For each sibling pipeline aggregation, you must define a bucket aggregations and metric aggregation. -* You want to exclude the metricbeat process from your visualization of top processes: `metricbeat.*` -* You only want to show processes collecting beats: `.*beat` -* You want to exclude two specific values, the string `"empty"` and `"none"`: `empty|none` +{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Average bucket]:: Calculates the mean, or average, value of a specified metric in a sibling aggregation. -*Geo aggregations* +{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Max Bucket]:: Calculates the maximum value of a specified metric in a sibling aggregation. -These are only supported by the tile map and table visualizations: +{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Min Bucket]:: Calculates the minimum value of a specified metric in a sibling aggregation. -{ref}/search-aggregations-bucket-geohashgrid-aggregation.html[Geohash]:: Displays points based on a geohash. +{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Sum Bucket]:: Calculates the sum of the values of a specified metric in a sibling aggregation. -{ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile]:: Groups points based on web map tiling. +[float] +[[visualize-bucket-aggregations]] +=== Bucket aggregations +Bucket aggregations sort documents into buckets, depending on the contents of the document. -[float] -[[visualize-parent-pipeline-aggregations]] -==== Parent pipeline aggregations +{ref}/search-aggregations-bucket-datehistogram-aggregation.html[Date histogram]:: Splits a date field into buckets by interval. If the date field is the primary time field for the index pattern, it chooses an automatic interval for you. Intervals are labeled at the start of the interval, using the date-key returned by {es}. For example, the tooltip for a monthly interval displays the first day of the month. -For each of the parent pipeline aggregations you have to define a bucket and metric to calculate. These -metrics expect the buckets to be ordered, and are especially useful for time series data. -You can also nest these aggregations. For example, if you want to produce a third derivative. +{ref}/search-aggregations-bucket-daterange-aggregation.html[Date range]:: Reports values that are within a range of dates that you specify. You can specify the ranges for the dates using {ref}/common-options.html#date-math[_date math_] expressions. -These visualizations support parent pipeline aggregations: +{ref}/search-aggregations-bucket-filter-aggregation.html[Filter]:: Each filter creates a bucket of documents. You can specify a filter as a +<> or <> query string. -* Line, Area and Bar charts -* Data table +{ref}/search-aggregations-bucket-geohashgrid-aggregation.html[Geohash]:: Displays points based on a geohash. Supported by the tile map and data table visualizations. -{ref}/search-aggregations-pipeline-derivative-aggregation.html[Derivative]:: Calculates the derivative of specific metrics. +{ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile]:: Groups points based on web map tiling. Supported by the tile map and data table visualizations. -{ref}/search-aggregations-pipeline-cumulative-sum-aggregation.html[Cumulative Sum]:: Calculates the cumulative sum of a specified metric in a parent histogram. +{ref}/search-aggregations-bucket-histogram-aggregation.html[Histogram]:: Builds from a numeric field. -{ref}/search-aggregations-pipeline-movavg-aggregation.html[Moving Average]:: Slides a window across the data and emits the average value of the window. +{ref}/search-aggregations-bucket-iprange-aggregation.html[IPv4 range]:: Specify ranges of IPv4 addresses. -{ref}/search-aggregations-pipeline-serialdiff-aggregation.html[Serial Diff]:: Values in a time series are subtracted from itself at different time lags or periods. +{ref}/search-aggregations-bucket-range-aggregation.html[Range]:: Specify ranges of values for a numeric field. -Custom {kib} plugins can <>, which includes support for adding more aggregations. +{ref}/search-aggregations-bucket-significantterms-aggregation.html[Significant terms]:: Returns interesting or unusual occurrences of terms in a set. Supports {es} {ref}/search-aggregations-bucket-terms-aggregation.html#_filtering_values_4[exclude and include patterns]. -[float] -[[visualize-advanced-aggregation-options]] -==== Advanced aggregation options +{ref}/search-aggregations-bucket-terms-aggregation.html[Terms]:: Specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. Supports {es} {ref}/search-aggregations-bucket-terms-aggregation.html#_filtering_values_4[exclude and include patterns]. -*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation -definition, as in the following example: +{kib} filters string fields with only regular expression patterns, and does not filter numeric fields or match with arrays. -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } +For example: -This example implements a {es} {ref}/search-aggregations.html[Script Value Source] which replaces -the value in the metric. The availability of these options varies depending on the aggregation -you choose. +* You want to exclude the metricbeat process from your visualization of top processes: `metricbeat.*` +* You only want to show processes collecting beats: `.*beat` +* You want to exclude two specific values, the string `"empty"` and `"none"`: `empty|none` -When multiple bucket aggregations are defined, you can use the drag target on each aggregation to change the priority. For more information about working with aggregation order, see https://www.elastic.co/blog/kibana-aggregation-execution-order-and-you[Kibana, Aggregation Execution Order, and You]. +Patterns are case sensitive. diff --git a/docs/visualize/heatmap.asciidoc b/docs/visualize/heatmap.asciidoc new file mode 100644 index 00000000000000..18c4018213390b --- /dev/null +++ b/docs/visualize/heatmap.asciidoc @@ -0,0 +1,40 @@ +[[heatmap]] +== Heat map + +Heat maps are graphical representations of data where the individual values are represented as colors. + +NOTE: Heat map has been replaced with <>, which offers more functionality and is easier to use. + +[float] +[[heatmap-aggregation]] +=== Supported aggregations + +Heat maps support the following aggregations: + +* <> + +* <> + +* <> + +* <> + +[float] +[[navigate-heatmap]] +=== Change the color ranges + +When only one color displays on the heat map, you might need to change the color ranges. + +To specify the number of color ranges: + +. Click *Options*. + +. Enter the *Number of colors* to display. + +To specify custom ranges: + +. Click *Options*. + +. Select *Use custom ranges*. + +. Enter the ranges to display. diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc index e61895a29891ba..e3f61565453b5e 100644 --- a/docs/visualize/lens.asciidoc +++ b/docs/visualize/lens.asciidoc @@ -4,7 +4,7 @@ beta[] -*Lens* provides you with a simple and fast way to create visualizations from your Elasticsearch data. With Lens, you can: +*Lens* provides you with a simple and fast way to create visualizations from your {es} data. With Lens, you can: * Quickly build visualizations by dragging and dropping data fields. @@ -14,6 +14,20 @@ beta[] * Save your visualization for use in a dashboard. +[float] +[[lens-aggregation]] +=== Supported aggregations + +Lens supports the following aggregations: + +* <> + +* <> + +* <> + +* <> + [float] [[drag-drop]] === Drag and drop diff --git a/docs/visualize/metric.asciidoc b/docs/visualize/metric.asciidoc index 9cbc4a0f7a5508..ddcf5fe3b73bdb 100644 --- a/docs/visualize/metric.asciidoc +++ b/docs/visualize/metric.asciidoc @@ -2,3 +2,15 @@ === Metric Click the *Options* tab to display the font size slider. + +[float] +[[metric-aggregation]] +==== Supported aggregations + +Metric support the following aggregations: + +* <> + +* <> + +* <> diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc index e9085d18185ec3..2cb8aa7cb3c1fb 100644 --- a/docs/visualize/most-frequent.asciidoc +++ b/docs/visualize/most-frequent.asciidoc @@ -1,30 +1,41 @@ [[most-frequent]] == Most frequently used visualizations -The most frequently used visualizations allow you to plot aggregated data from a <> or <>. They all support a single level of -Elasticsearch {es} {ref}/search-aggregations-metrics.html[metric] aggregations, and one or more -levels of {es} {ref}/search-aggregations-bucket.html[bucket] aggregations. +The most frequently used visualizations allow you to plot aggregated data from a <> or <>. The most frequently used visualizations include: * Line, area, and bar charts -* Pie charts -* Data tables -* Metric, goals, and gauges -* Heat maps -* Tag clouds +* Pie chart +* Data table +* Metric, goal, and gauge +* Tag cloud + +[float] +[[frequently-used-viz-aggregation]] +=== Supported aggregations + +The most frequently used visualizations support the following aggregations: + +* <> + +* <> + +* <> + +* <> [float] === Configure your visualization -You configure visualizations using the default editor, which is broken into metrics and buckets, and includes a default count -metric. Each visualization supports different configurations for what the metrics and buckets -represent. For example, a bar chart allows you to add an X-axis: +You configure visualizations using the default editor. Each visualization supports different configurations of the metrics and buckets. + +For example, a bar chart allows you to add an x-axis: [role="screenshot"] image::images/add-bucket.png["",height=478] -A common configuration for the X-axis is to use a {es} {ref}/search-aggregations-bucket-datehistogram-aggregation.html[date histogram] aggregation: +A common configuration for the x-axis is to use a {es} {ref}/search-aggregations-bucket-datehistogram-aggregation.html[date histogram] aggregation: [role="screenshot"] image::images/visualize-date-histogram.png[] @@ -58,5 +69,3 @@ Each visualization also has its own customization options. Most visualizations a [role="screenshot"] image::images/color-picker.png[An array of color dots that users can select,height=267] - -include::aggregations.asciidoc[] diff --git a/docs/visualize/regionmap.asciidoc b/docs/visualize/regionmap.asciidoc index c39282963ef7bc..accabd16e5fcdf 100644 --- a/docs/visualize/regionmap.asciidoc +++ b/docs/visualize/regionmap.asciidoc @@ -18,17 +18,18 @@ To create a region map, you configure an inner join that joins the result of an and a reference vector file based on a shared key. [float] -==== Data +[[region-map-aggregation]] +=== Supported aggregations -[float] -===== Metrics +Region maps support the following aggregations: -Select any of the supported _Metric_ or _Sibling Pipeline Aggregations_. +* <> -[float] -===== Buckets +* <> + +* <> -Configure a _Terms_ aggregation. The term is the _key_ that is used to join the results to the vector data on the map. +Use the _key_ term to join the results to the vector data on the map. [float] ==== Options diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 2b499d098a956e..08cf666345e34d 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -17,36 +17,11 @@ in `kibana.yml`. [[coordinate-map-aggregation]] === Supported aggregations -Coordinate maps support the metric and bucket aggregations. +Coordinate maps support the following aggregations: -[float] -===== Metric aggregations - -The following metric aggregations are supported: - -{ref}/search-aggregations-metrics-valuecount-aggregation.html[Count]:: Returns a raw count of -the elements in the index pattern. The default metrics aggregation for a coordinate map is *Count*. - -{ref}/search-aggregations-metrics-avg-aggregation.html[Average]:: Returns the average of a numeric -field. - -{ref}/search-aggregations-metrics-sum-aggregation.html[Sum]:: Returns the total sum of a numeric -field. - -{ref}/search-aggregations-metrics-min-aggregation.html[Min]:: Returns the minimum value of a -numeric field. - -{ref}/search-aggregations-metrics-max-aggregation.html[Max]:: Returns the maximum value of a -numeric field. - -{ref}/search-aggregations-metrics-cardinality-aggregation.html[Unique Count]:: Returns -the number of unique values in a field. - -[float] -[[coordinate-bucket-aggregation]] -===== Bucket aggregation +* <> -Coordinate maps support the {ref}/search-aggregations-bucket-geohashgrid-aggregation.html[_geohash_] bucket aggregation. +* <> When you deselect *Change precision on map zoom*, the *Precision* slider appears. The *Precision* slider determines the granularity of the results displayed on the map. For details on the area specified by each precision level, refer to {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[geohash grid]. diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc index ff4160d1ac9d20..69d6985acd1e42 100644 --- a/docs/visualize/tsvb.asciidoc +++ b/docs/visualize/tsvb.asciidoc @@ -1,8 +1,8 @@ [[TSVB]] == TSVB -TSVB is a time series data visualizer that allows you to use the full power of the -Elasticsearch aggregation framework. With TSVB, you can combine an infinite +TSVB is a time series data visualizer that allows you to use the full power of the +Elasticsearch aggregation framework. With TSVB, you can combine an infinite number of aggregations to display complex data. NOTE: In Elasticsearch version 7.3.0 and later, the time series data visualizer is now referred to as TSVB instead of Time Series Visual Builder. @@ -43,6 +43,18 @@ Table:: Display data from multiple time series by defining the field group to sh [role="screenshot"] image:images/tsvb-table.png["Table visualization"] +[float] +[[tsvb-aggregation]] +=== Supported aggregations + +TSVB supports the following aggregations: + +* <> + +* <> + +* <> + [float] [[create-tsvb-visualization]] === Create TSVB visualizations @@ -60,7 +72,7 @@ To create a single metric, add multiple data series with multiple aggregations. . Specify the data series labels and colors. .. Select *Data*. -+ ++ If you are using the *Table* visualization, select *Columns*. .. In the *Label* field, enter a name for the data series, which is used on legends and titles. @@ -79,7 +91,7 @@ For series that are grouped by a term, you can specify a mustache variable of `{ .. To add another metric, click *+*. + -When you add more than one metric, the last metric value is displayed, which is indicated by the eye icon. +When you add more than one metric, the last metric value is displayed, which is indicated by the eye icon. . To specify the format and display options, select *Options*. @@ -95,7 +107,7 @@ Change the data that you want to display and choose the style options for the pa . Select *Panel options*. -. Under *Data*, specify how much of the data that you want to display in the visualization. +. Under *Data*, specify how much of the data that you want to display in the visualization. . Under *Style*, specify how you want the visualization to look. @@ -113,7 +125,7 @@ If you are using the Time Series visualization, add annotation data sources. [[tsvb-enter-markdown]] ==== Enter Markdown text -Edit the source for the Markdown visualization. +Edit the source for the Markdown visualization. . Select *Markdown*.