diff --git a/docs/development/core/public/kibana-plugin-core-public.appcategory.id.md b/docs/development/core/public/kibana-plugin-core-public.appcategory.id.md new file mode 100644 index 00000000000000..0342a1d9ee95b8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appcategory.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppCategory](./kibana-plugin-core-public.appcategory.md) > [id](./kibana-plugin-core-public.appcategory.id.md) + +## AppCategory.id property + +Unique identifier for the categories + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appcategory.md b/docs/development/core/public/kibana-plugin-core-public.appcategory.md index b115baa1be1a37..d91727a1bbf293 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appcategory.md +++ b/docs/development/core/public/kibana-plugin-core-public.appcategory.md @@ -18,6 +18,7 @@ export interface AppCategory | --- | --- | --- | | [ariaLabel](./kibana-plugin-core-public.appcategory.arialabel.md) | string | If the visual label isn't appropriate for screen readers, can override it here | | [euiIconType](./kibana-plugin-core-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 | +| [id](./kibana-plugin-core-public.appcategory.id.md) | string | Unique identifier for the categories | | [label](./kibana-plugin-core-public.appcategory.label.md) | string | Label used for cateogry name. Also used as aria-label if one isn't set. | | [order](./kibana-plugin-core-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-core-public.chromestart.getnavtype_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md new file mode 100644 index 00000000000000..09864be43996da --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getnavtype_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getNavType$](./kibana-plugin-core-public.chromestart.getnavtype_.md) + +## ChromeStart.getNavType$() method + +Get the navigation type TODO \#64541 Can delete + +Signature: + +```typescript +getNavType$(): Observable; +``` +Returns: + +`Observable` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index c179e089d7cfd0..b4eadc93fe78d3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -58,6 +58,7 @@ core.chrome.setHelpExtension(elem => { | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | | [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | +| [getNavType$()](./kibana-plugin-core-public.chromestart.getnavtype_.md) | Get the navigation type TODO \#64541 Can delete | | [removeApplicationClass(className)](./kibana-plugin-core-public.chromestart.removeapplicationclass.md) | Remove a className added with addApplicationClass(). If className is unknown it is ignored. | | [setAppTitle(appTitle)](./kibana-plugin-core-public.chromestart.setapptitle.md) | Sets the current app's title | | [setBadge(badge)](./kibana-plugin-core-public.chromestart.setbadge.md) | Override the current badge | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index c24e4cf908b877..eafc81447ee03e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -158,6 +158,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IContextProvider](./kibana-plugin-core-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IToasts](./kibana-plugin-core-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-core-public.toastsapi.md). | | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | +| [NavType](./kibana-plugin-core-public.navtype.md) | | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | diff --git a/docs/development/core/public/kibana-plugin-core-public.navtype.md b/docs/development/core/public/kibana-plugin-core-public.navtype.md new file mode 100644 index 00000000000000..8f1d9a43517543 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.navtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [NavType](./kibana-plugin-core-public.navtype.md) + +## NavType type + +Signature: + +```typescript +export declare type NavType = 'modern' | 'legacy'; +``` diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 51910169e8673a..cafd50d92376fe 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -68,6 +68,9 @@ into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). +`pageNavigation`:: The style of navigation menu for Kibana. +Choices are Legacy, the legacy style where every plugin is represented in the nav, +and Modern, a new format that bundles related plugins together in flyaway nested navigation. `query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character in a query clause. Only applies when experimental query features are enabled in the query bar. To disallow leading wildcards in Lucene queries, diff --git a/package.json b/package.json index 178ccbac7d420c..8a92b464893081 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "@elastic/charts": "19.2.0", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", - "@elastic/eui": "22.3.0", + "@elastic/eui": "22.3.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.4.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index ae883a5032fe74..8259f251a9be3b 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@elastic/charts": "19.2.0", - "@elastic/eui": "22.3.0", + "@elastic/eui": "22.3.1", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 89007461b63e6c..4a79dd8869c1c6 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -23,7 +23,8 @@ import { ChromeBreadcrumb, ChromeService, InternalChromeStart, -} from './chrome_service'; + NavType, +} from './'; const createStartContractMock = () => { const startContract: DeeplyMockedKeys = { @@ -72,6 +73,7 @@ const createStartContractMock = () => { setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), getIsNavDrawerLocked$: jest.fn(), + getNavType$: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); @@ -81,6 +83,7 @@ const createStartContractMock = () => { startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); + startContract.getNavType$.mockReturnValue(new BehaviorSubject('modern' as NavType)); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index a765ed47ea712c..327be61cc63e39 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -17,18 +17,18 @@ * under the License. */ -import * as Rx from 'rxjs'; -import { take, toArray } from 'rxjs/operators'; import { shallow } from 'enzyme'; import React from 'react'; - +import * as Rx from 'rxjs'; +import { take, toArray } from 'rxjs/operators'; +import { App } from '../application'; import { applicationServiceMock } from '../application/application_service.mock'; +import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; -import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; +import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { ChromeService } from './chrome_service'; -import { App } from '../application'; class FakeApp implements App { public title = `${this.id} App`; @@ -51,6 +51,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 3d9eeff09ecceb..3fc22caaefb04a 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -17,27 +17,26 @@ * under the License. */ +import { Breadcrumb as EuiBreadcrumb, IconType } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; -import { BehaviorSubject, Observable, ReplaySubject, combineLatest, of, merge } from 'rxjs'; +import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs'; import { flatMap, map, takeUntil } from 'rxjs/operators'; import { parse } from 'url'; - -import { i18n } from '@kbn/i18n'; -import { IconType, Breadcrumb as EuiBreadcrumb } from '@elastic/eui'; - -import { InjectedMetadataStart } from '../injected_metadata'; -import { NotificationsStart } from '../notifications'; import { InternalApplicationStart } from '../application'; +import { DocLinksStart } from '../doc_links'; import { HttpStart } from '../http'; - +import { InjectedMetadataStart } from '../injected_metadata'; +import { NotificationsStart } from '../notifications'; +import { IUiSettingsClient } from '../ui_settings'; +import { KIBANA_ASK_ELASTIC_LINK } from './constants'; +import { ChromeDocTitle, DocTitleService } from './doc_title'; +import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; -import { NavControlsService, ChromeNavControls } from './nav_controls'; -import { DocTitleService, ChromeDocTitle } from './doc_title'; -import { LoadingIndicator, Header } from './ui'; -import { DocLinksStart } from '../doc_links'; +import { Header, LoadingIndicator } from './ui'; +import { NavType } from './ui/header'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; -import { KIBANA_ASK_ELASTIC_LINK } from './constants'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; @@ -84,6 +83,7 @@ interface StartDeps { http: HttpStart; injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; + uiSettings: IUiSettingsClient; } /** @internal */ @@ -136,6 +136,7 @@ export class ChromeService { http, injectedMetadata, notifications, + uiSettings, }: StartDeps): Promise { this.initVisibility(application); @@ -160,6 +161,10 @@ export class ChromeService { const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); + // TODO #64541 + // Can delete + const getNavType$ = uiSettings.get$('pageNavigation').pipe(takeUntil(this.stop$)); + if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning( i18n.translate('core.chrome.legacyBrowserWarning', { @@ -197,6 +202,7 @@ export class ChromeService { navControlsRight$={navControls.getRight$()} onIsLockedUpdate={setIsNavDrawerLocked} isLocked$={getIsNavDrawerLocked$} + navType$={getNavType$} /> ), @@ -257,6 +263,8 @@ export class ChromeService { setHelpSupportUrl: (url: string) => helpSupportUrl$.next(url), getIsNavDrawerLocked$: () => getIsNavDrawerLocked$, + + getNavType$: () => getNavType$, }; } @@ -403,6 +411,13 @@ export interface ChromeStart { * Get an observable of the current locked state of the nav drawer. */ getIsNavDrawerLocked$(): Observable; + + /** + * Get the navigation type + * TODO #64541 + * Can delete + */ + getNavType$(): Observable; } /** @internal */ diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index 4a500836990a7a..cc1e0851f59442 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -33,6 +33,7 @@ export { ChromeHelpExtensionMenuDocumentationLink, ChromeHelpExtensionMenuGitHubLink, } from './ui/header/header_help_menu'; +export { NavType } from './ui'; export { ChromeNavLink, ChromeNavLinks, ChromeNavLinkUpdateableFields } from './nav_links'; export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem } from './recently_accessed'; export { ChromeNavControl, ChromeNavControls } from './nav_controls'; diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap new file mode 100644 index 00000000000000..14d5b2e8fdcbb8 --- /dev/null +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -0,0 +1,4506 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollapsibleNav renders links grouped by category 1`] = ` + + + + + + } + /> + + + + +
+ +
+
+
+ + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + + +
+
+ +
+ + + + + +`; + +exports[`CollapsibleNav renders the default nav 1`] = ` + + + +`; + +exports[`CollapsibleNav renders the default nav 2`] = ` + + + + + + } + /> + + + + +
+ +
+
+
+ + + + +
+
+ +
+ + + + + +`; + +exports[`CollapsibleNav renders the default nav 3`] = ` + + + + + + +
+ +
+
+
+ + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + +
+ } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + + +
+
+ +
+ + + + + +`; diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index f19728a52dd70b..5c5e7f18b60a47 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,25 +1,12 @@ -@import '@elastic/eui/src/components/header/variables'; -@import '@elastic/eui/src/components/nav_drawer/variables'; - -.chrHeaderWrapper { +// TODO #64541 +// Delete this block +.chrHeaderWrapper:not(.headerWrapper) { width: 100%; position: fixed; top: 0; z-index: 10; } -.chrHeaderWrapper ~ .app-wrapper:not(.hidden-chrome) { - top: $euiHeaderChildSize; - left: $euiHeaderChildSize; - - // HOTFIX: Temporary fix for flyouts not inside portals - // SASSTODO: Find an actual solution - .euiFlyout { - top: $euiHeaderChildSize; - height: calc(100% - #{$euiHeaderChildSize}); - } -} - .chrHeaderHelpMenu__version { text-transform: none; } @@ -29,19 +16,8 @@ margin-right: $euiSize; } -// Mobile header is smaller -@include euiBreakpoint('xs', 's') { - .chrHeaderWrapper ~ .app-wrapper:not(.hidden-chrome) { - left: 0; - } -} - -@include euiBreakpoint('xl') { - .chrHeaderWrapper--navIsLocked { - ~ .app-wrapper:not(.hidden-chrome) { - // Shrink the content from the left so it's no longer overlapped by the nav drawer (ALWAYS) - left: $euiNavDrawerWidthExpanded !important; // sass-lint:disable-line no-important - transition: left $euiAnimSpeedFast $euiAnimSlightResistance; - } +.header__toggleNavButtonSection { + .euiBody--collapsibleNavIsDocked & { + display: none; } } diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx new file mode 100644 index 00000000000000..4a9d3071b93be7 --- /dev/null +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -0,0 +1,136 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; +import { CollapsibleNav } from './collapsible_nav'; +import { AppCategory } from '../../../../types'; +import { DEFAULT_APP_CATEGORIES } from '../../..'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'mockId', +})); + +const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; + +function mockLink(label: string, category?: AppCategory) { + return { + key: label, + label, + href: label, + isActive: true, + onClick: () => {}, + category, + 'data-test-subj': label, + }; +} + +function mockRecentNavLink(label: string) { + return { + href: label, + label, + title: label, + 'aria-label': label, + }; +} + +function mockProps() { + return { + id: 'collapsible-nav', + homeHref: '/', + isLocked: false, + isOpen: false, + navLinks: [], + recentNavLinks: [], + storage: new StubBrowserStorage(), + onIsOpenUpdate: () => {}, + onIsLockedUpdate: () => {}, + }; +} + +describe('CollapsibleNav', () => { + // this test is mostly an "EUI works as expected" sanity check + it('renders the default nav', () => { + const onLock = sinon.spy(); + const component = mount(); + expect(component).toMatchSnapshot(); + + component.setProps({ isOpen: true }); + expect(component).toMatchSnapshot(); + + component.setProps({ isLocked: true }); + expect(component).toMatchSnapshot(); + + // limit the find to buttons because jest also renders data-test-subj on a JSX wrapper element + component.find('button[data-test-subj="collapsible-nav-lock"]').simulate('click'); + expect(onLock.callCount).toEqual(1); + }); + + it('renders links grouped by category', () => { + // just a test of category functionality, categories are not accurate + const navLinks = [ + mockLink('discover', kibana), + mockLink('siem', security), + mockLink('metrics', observability), + mockLink('monitoring', management), + mockLink('visualize', kibana), + mockLink('dashboard', kibana), + mockLink('canvas'), // links should be able to be rendered top level as well + mockLink('logs', observability), + ]; + const recentNavLinks = [mockRecentNavLink('recent 1'), mockRecentNavLink('recent 2')]; + const component = mount( + + ); + expect(component).toMatchSnapshot(); + }); + + it('remembers collapsible section state', () => { + function expectNavLinksCount(component: ReactWrapper, count: number) { + expect( + component.find('.euiAccordion-isOpen a[data-test-subj="collapsibleNavAppLink"]').length + ).toEqual(count); + } + + const navLinks = [ + mockLink('discover', kibana), + mockLink('siem', security), + mockLink('metrics', observability), + mockLink('monitoring', management), + mockLink('visualize', kibana), + mockLink('dashboard', kibana), + mockLink('logs', observability), + ]; + const component = mount(); + expectNavLinksCount(component, 7); + component.find('[data-test-subj="collapsibleNavGroup-kibana"] button').simulate('click'); + expectNavLinksCount(component, 4); + component.setProps({ isOpen: false }); + expectNavLinksCount(component, 0); // double check the nav closed + component.setProps({ isOpen: true }); + expectNavLinksCount(component, 4); + }); +}); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx new file mode 100644 index 00000000000000..274195f1917a5e --- /dev/null +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -0,0 +1,281 @@ +/* + * 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 { + EuiCollapsibleNav, + EuiCollapsibleNavGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiListGroup, + EuiListGroupItem, + EuiShowFor, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { groupBy, sortBy } from 'lodash'; +import React, { useRef } from 'react'; +import { AppCategory } from '../../../../types'; +import { OnIsLockedUpdate } from './'; +import { NavLink, RecentNavLink } from './nav_link'; + +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 + ); +} + +function getCategoryLocalStorageKey(id: string) { + return `core.navGroup.${id}`; +} + +function getIsCategoryOpen(id: string, storage: Storage) { + const value = storage.getItem(getCategoryLocalStorageKey(id)) ?? 'true'; + + return value === 'true'; +} + +function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) { + storage.setItem(getCategoryLocalStorageKey(id), `${isOpen}`); +} + +interface Props { + isLocked: boolean; + isOpen: boolean; + navLinks: NavLink[]; + recentNavLinks: RecentNavLink[]; + homeHref: string; + id: string; + storage?: Storage; + onIsLockedUpdate: OnIsLockedUpdate; + onIsOpenUpdate: (isOpen?: boolean) => void; +} + +export function CollapsibleNav({ + isLocked, + isOpen, + navLinks, + recentNavLinks, + onIsLockedUpdate, + onIsOpenUpdate, + homeHref, + id, + storage = window.localStorage, +}: Props) { + const lockRef = useRef(null); + const groupedNavLinks = groupBy(navLinks, link => link?.category?.id); + const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; + const categoryDictionary = getAllCategories(allCategorizedLinks); + const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); + + return ( + + {/* Pinned items */} + + + onIsOpenUpdate(false), + }, + ]} + maxWidth="none" + color="text" + gutterSize="none" + size="s" + /> + + + + + + + {/* Recently viewed */} + setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)} + > + {recentNavLinks.length > 0 ? ( + { + // TODO #64541 + // Can remove icon from recent links completely + const { iconType, ...linkWithoutIcon } = link; + return linkWithoutIcon; + })} + maxWidth="none" + color="subdued" + gutterSize="none" + size="s" + /> + ) : ( + +

+ {i18n.translate('core.ui.EmptyRecentlyViewed', { + defaultMessage: 'No recently viewed items', + })} +

+
+ )} +
+ + {/* Kibana, Observability, Security, and Management sections */} + {orderedCategories.map((categoryName, i) => { + const category = categoryDictionary[categoryName]!; + const links = allCategorizedLinks[categoryName].map( + ({ label, href, isActive, isDisabled, onClick }: NavLink) => ({ + label, + href, + isActive, + isDisabled, + 'data-test-subj': 'collapsibleNavAppLink', + onClick: (e: React.MouseEvent) => { + onIsOpenUpdate(false); + onClick(e); + }, + }) + ); + + return ( + setIsCategoryOpen(category.id, isCategoryOpen, storage)} + data-test-subj={`collapsibleNavGroup-${category.id}`} + > + + + ); + })} + + {/* Things with no category (largely for custom plugins) */} + {unknowns.map(({ label, href, icon, isActive, isDisabled, onClick }, i) => ( + + + ) => { + onIsOpenUpdate(false); + onClick(e); + }} + /> + + + ))} + + {/* Docking button only for larger screens that can support it*/} + + + + { + onIsLockedUpdate(!isLocked); + if (lockRef.current) { + lockRef.current.focus(); + } + }} + iconType={isLocked ? 'lock' : 'lockOpen'} + /> + + + +
+
+ ); +} diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 66b34c3db7bad8..fb94ef46cdc2c9 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -25,8 +25,8 @@ import { EuiIcon, // @ts-ignore EuiNavDrawer, - // @ts-ignore EuiShowFor, + htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Component, createRef } from 'react'; @@ -43,13 +43,14 @@ import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; import { HeaderBadge } from './header_badge'; -import { OnIsLockedUpdate } from './'; +import { NavType, 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 { createNavLink, createRecentNavLink } from './nav_link'; import { HeaderLogo } from './header_logo'; import { NavDrawer } from './nav_drawer'; +import { CollapsibleNav } from './collapsible_nav'; export interface HeaderProps { kibanaVersion: string; @@ -70,6 +71,7 @@ export interface HeaderProps { navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; isLocked$: Rx.Observable; + navType$: Rx.Observable; onIsLockedUpdate: OnIsLockedUpdate; } @@ -83,11 +85,14 @@ interface State { navControlsRight: readonly ChromeNavControl[]; currentAppId: string | undefined; isLocked: boolean; + navType: NavType; + isOpen: boolean; } export class Header extends Component { private subscription?: Rx.Subscription; private navDrawerRef = createRef(); + private toggleCollapsibleNavRef = createRef(); constructor(props: HeaderProps) { super(props); @@ -105,6 +110,8 @@ export class Header extends Component { navControlsRight: [], currentAppId: '', isLocked, + navType: 'modern', + isOpen: false, }; } @@ -120,7 +127,8 @@ export class Header extends Component { this.props.navControlsLeft$, this.props.navControlsRight$, this.props.application.currentAppId$, - this.props.isLocked$ + this.props.isLocked$, + this.props.navType$ ) ).subscribe({ next: ([ @@ -129,7 +137,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, isLocked], + [navControlsLeft, navControlsRight, currentAppId, isLocked, navType], ]) => { this.setState({ appTitle, @@ -141,6 +149,7 @@ export class Header extends Component { navControlsRight, currentAppId, isLocked, + navType, }); }, }); @@ -176,7 +185,7 @@ export class Header extends Component { kibanaVersion, } = this.props; const navLinks = this.state.navLinks.map(link => - euiNavLink( + createNavLink( link, this.props.legacyMode, this.state.currentAppId, @@ -184,26 +193,54 @@ export class Header extends Component { this.props.application.navigateToApp ) ); + const recentNavLinks = this.state.recentlyAccessed.map(link => + createRecentNavLink(link, this.state.navLinks, this.props.basePath) + ); if (!isVisible) { return null; } const className = classnames( - 'chrHeaderWrapper', + 'chrHeaderWrapper', // TODO #64541 - delete this + 'hide-for-sharing', { 'chrHeaderWrapper--navIsLocked': this.state.isLocked, - }, - 'hide-for-sharing' + headerWrapper: this.state.navType === 'modern', + } ); - + const navId = htmlIdGenerator()(); return (
- + - - {this.renderMenuTrigger()} - + {this.state.navType === 'modern' ? ( + + { + this.setState({ isOpen: !this.state.isOpen }); + }} + aria-expanded={this.state.isOpen} + aria-pressed={this.state.isOpen} + aria-controls={navId} + ref={this.toggleCollapsibleNavRef} + > + + + + ) : ( + // TODO #64541 + // Delete this block + + + {this.renderMenuTrigger()} + + + )} { - + {this.state.navType === 'modern' ? ( + { + this.setState({ isOpen }); + if (this.toggleCollapsibleNavRef.current) { + this.toggleCollapsibleNavRef.current.focus(); + } + }} + /> + ) : ( + // TODO #64541 + // Delete this block + + )}
); } diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index 49e002a66d9390..a492273a65ba8f 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -18,6 +18,7 @@ */ export { Header, HeaderProps } from './header'; +export { OnIsLockedUpdate, NavType } from './types'; export { ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, @@ -25,4 +26,3 @@ export { ChromeHelpExtensionMenuDocumentationLink, ChromeHelpExtensionMenuGitHubLink, } from './header_help_menu'; -export type OnIsLockedUpdate = (isLocked: boolean) => void; diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index c57faec1e428d7..7faee8edea43b6 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -22,22 +22,18 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; import { OnIsLockedUpdate } from './'; -import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; -import { HttpStart } from '../../../http'; -import { NavLink } from './nav_link'; +import { NavLink, RecentNavLink } from './nav_link'; import { RecentLinks } from './recent_links'; export interface Props { isLocked?: boolean; onIsLockedUpdate?: OnIsLockedUpdate; navLinks: NavLink[]; - chromeNavLinks: ChromeNavLink[]; - recentlyAccessedItems: ChromeRecentlyAccessedHistoryItem[]; - basePath: HttpStart['basePath']; + recentNavLinks: RecentNavLink[]; } function navDrawerRenderer( - { isLocked, onIsLockedUpdate, navLinks, chromeNavLinks, recentlyAccessedItems, basePath }: Props, + { isLocked, onIsLockedUpdate, navLinks, recentNavLinks }: Props, ref: React.Ref ) { return ( @@ -50,11 +46,7 @@ function navDrawerRenderer( defaultMessage: 'Primary', })} > - {RecentLinks({ - recentlyAccessedItems, - navLinks: chromeNavLinks, - basePath, - })} + {RecentLinks({ recentNavLinks })} ) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); } @@ -30,15 +32,36 @@ function LinkIcon({ url }: { url: string }) { return ; } -export type NavLink = ReturnType; +export interface NavLink { + key: string; + label: string; + href: string; + isActive: boolean; + onClick(event: React.MouseEvent): void; + category?: AppCategory; + isDisabled?: boolean; + iconType?: string; + icon?: JSX.Element; + order?: number; + 'data-test-subj': string; +} -export function euiNavLink( +/** + * Create a link that's actually ready to be passed into EUI + * + * @param navLink + * @param legacyMode + * @param currentAppId + * @param basePath + * @param navigateToApp + */ +export function createNavLink( navLink: ChromeNavLink, legacyMode: boolean, currentAppId: string | undefined, basePath: HttpStart['basePath'], navigateToApp: CoreStart['application']['navigateToApp'] -) { +): NavLink { const { legacy, url, @@ -64,7 +87,7 @@ export function euiNavLink( 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) { + onClick(event) { if ( !legacyMode && // ignore when in legacy mode !legacy && // ignore links to legacy apps @@ -85,3 +108,76 @@ export function euiNavLink( 'data-test-subj': 'navDrawerAppsMenuLink', }; } + +// 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; + +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; +} + +export interface RecentNavLink { + href: string; + label: string; + title: string; + 'aria-label': string; + iconType?: string; +} + +/** + * Add saved object type info to recently links + * + * Recent nav links are similar to normal nav links but are missing some Kibana Platform magic and + * because of legacy reasons have slightly different properties. + * @param recentLink + * @param navLinks + * @param basePath + */ +export function createRecentNavLink( + recentLink: ChromeRecentlyAccessedHistoryItem, + navLinks: ChromeNavLink[], + basePath: HttpStart['basePath'] +) { + const { link, label } = recentLink; + 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, + }; +} diff --git a/src/core/public/chrome/ui/header/recent_links.tsx b/src/core/public/chrome/ui/header/recent_links.tsx index 57cb1d9541bcd1..019cdce0b43c6f 100644 --- a/src/core/public/chrome/ui/header/recent_links.tsx +++ b/src/core/public/chrome/ui/header/recent_links.tsx @@ -21,73 +21,13 @@ 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, - }; - }); -} +import { RecentNavLink } from './nav_link'; interface Props { - recentlyAccessedItems: ChromeRecentlyAccessedHistoryItem[]; - navLinks: ChromeNavLink[]; - basePath: HttpStart['basePath']; + recentNavLinks: RecentNavLink[]; } -export function RecentLinks({ recentlyAccessedItems, navLinks, basePath }: Props) { +export function RecentLinks({ recentNavLinks }: Props) { return ( void; +export type NavType = 'modern' | 'legacy'; diff --git a/src/core/public/chrome/ui/index.ts b/src/core/public/chrome/ui/index.ts index 460e19b7d97801..4f6ad90cb96a35 100644 --- a/src/core/public/chrome/ui/index.ts +++ b/src/core/public/chrome/ui/index.ts @@ -25,4 +25,5 @@ export { ChromeHelpExtensionMenuDiscussLink, ChromeHelpExtensionMenuDocumentationLink, ChromeHelpExtensionMenuGitHubLink, + NavType, } from './header'; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 59f0142bb88902..e58114b69dcc1e 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -240,6 +240,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 c30996b83c946a..3b2d9ed3c0b022 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -54,6 +54,7 @@ import { ChromeStart, ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, + NavType, } from './chrome'; import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors'; import { HttpSetup, HttpStart } from './http'; @@ -354,4 +355,5 @@ export { PluginOpaqueId, IUiSettingsClient, UiSettingsState, + NavType, }; diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index 6b4b22b8541bce..fa83b34e06b813 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -13,12 +13,7 @@ Array [ Array [
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ Array [
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index c9fad5952bc7af..225ef611c0298f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -55,6 +55,7 @@ export interface AppBase { export interface AppCategory { ariaLabel?: string; euiIconType?: string; + id: string; label: string; order?: number; } @@ -343,6 +344,7 @@ export interface ChromeStart { getHelpExtension$(): Observable; getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; + getNavType$(): Observable; navControls: ChromeNavControls; navLinks: ChromeNavLinks; recentlyAccessed: ChromeRecentlyAccessed; @@ -443,23 +445,28 @@ export function deepFreeze(object: T): RecursiveReadonly // @internal (undocumented) export const DEFAULT_APP_CATEGORIES: Readonly<{ - analyze: { + kibana: { + id: string; label: string; + euiIconType: string; order: number; }; observability: { + id: string; label: string; euiIconType: string; order: number; }; security: { + id: string; label: string; order: number; euiIconType: string; }; management: { + id: string; label: string; - euiIconType: string; + order: number; }; }>; @@ -883,6 +890,11 @@ export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulPart // @public export type MountPoint = (element: T) => UnmountCallback; +// Warning: (ae-missing-release-tag) "NavType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type NavType = 'modern' | 'legacy'; + // @public (undocumented) export interface NotificationsSetup { // (undocumented) diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index ff28fc75e367db..8032bc458822f4 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -1,3 +1,6 @@ +@import '@elastic/eui/src/components/header/variables'; +@import '@elastic/eui/src/components/nav_drawer/variables'; + /** * stretch the root element of the Kibana application to set the base-size that * flexed children should keep. Only works when paired with root styles applied @@ -9,7 +12,9 @@ min-height: 100%; } -.app-wrapper { +// TODO #64541 +// Delete this block +.chrHeaderWrapper:not(.headerWrapper) ~ .app-wrapper { display: flex; flex-flow: column nowrap; position: absolute; @@ -20,6 +25,22 @@ z-index: 5; margin: 0 auto; + &:not(.hidden-chrome) { + top: $euiHeaderChildSize; + left: $euiHeaderChildSize; + + // HOTFIX: Temporary fix for flyouts not inside portals + // SASSTODO: Find an actual solution + .euiFlyout { + top: $euiHeaderChildSize; + height: calc(100% - #{$euiHeaderChildSize}); + } + + @include euiBreakpoint('xs', 's') { + left: 0; + } + } + /** * 1. Dirty, but we need to override the .kbnGlobalNav-isOpen state * when we're looking at the log-in screen. @@ -33,6 +54,32 @@ } } +// TODO #64541 +// Delete this block +@include euiBreakpoint('xl') { + .chrHeaderWrapper--navIsLocked:not(.headerWrapper) { + ~ .app-wrapper:not(.hidden-chrome) { + // Shrink the content from the left so it's no longer overlapped by the nav drawer (ALWAYS) + left: $euiNavDrawerWidthExpanded !important; // sass-lint:disable-line no-important + transition: left $euiAnimSpeedFast $euiAnimSlightResistance; + } + } +} + +// TODO #64541 +// Remove .headerWrapper and header conditionals +.headerWrapper ~ .app-wrapper, +:not(header) ~ .app-wrapper { + display: flex; + flex-flow: column nowrap; + margin: 0 auto; + min-height: calc(100vh - #{$euiHeaderHeightCompensation}); + + &.hidden-chrome { + min-height: 100vh; + } +} + .app-wrapper-panel { display: flex; flex-grow: 1; diff --git a/src/core/server/legacy/plugins/get_nav_links.test.ts b/src/core/server/legacy/plugins/get_nav_links.test.ts index 44d080ec37a255..5e84f27acabd58 100644 --- a/src/core/server/legacy/plugins/get_nav_links.test.ts +++ b/src/core/server/legacy/plugins/get_nav_links.test.ts @@ -133,6 +133,7 @@ describe('getNavLinks', () => { id: 'app-a', title: 'AppA', category: { + id: 'foo', label: 'My Category', }, order: 42, @@ -151,6 +152,7 @@ describe('getNavLinks', () => { id: 'app-a', title: 'AppA', category: { + id: 'foo', label: 'My Category', }, order: 42, @@ -211,6 +213,7 @@ describe('getNavLinks', () => { id: 'link-a', title: 'AppA', category: { + id: 'foo', label: 'My Second Cat', }, order: 72, @@ -232,6 +235,7 @@ describe('getNavLinks', () => { id: 'link-a', title: 'AppA', category: { + id: 'foo', label: 'My Second Cat', }, order: 72, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 54b7a2ada69ad7..62d11ee7cf9a7a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -700,23 +700,28 @@ export function deepFreeze(object: T): RecursiveReadonly // @internal (undocumented) export const DEFAULT_APP_CATEGORIES: Readonly<{ - analyze: { + kibana: { + id: string; label: string; + euiIconType: string; order: number; }; observability: { + id: string; label: string; euiIconType: string; order: number; }; security: { + id: string; label: string; order: number; euiIconType: string; }; management: { + id: string; label: string; - euiIconType: string; + order: number; }; }>; diff --git a/src/core/types/app_category.ts b/src/core/types/app_category.ts index 83a3693f009b6c..8b39889b43a828 100644 --- a/src/core/types/app_category.ts +++ b/src/core/types/app_category.ts @@ -24,6 +24,11 @@ * @public */ export interface AppCategory { + /** + * Unique identifier for the categories + */ + id: string; + /** * Label used for cateogry name. * Also used as aria-label if one isn't set. diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 2285bd6afd3651..5708bcfeac31a7 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -21,13 +21,16 @@ import { i18n } from '@kbn/i18n'; /** @internal */ export const DEFAULT_APP_CATEGORIES = Object.freeze({ - analyze: { - label: i18n.translate('core.ui.analyzeNavList.label', { - defaultMessage: 'Analyze', + kibana: { + id: 'kibana', + label: i18n.translate('core.ui.kibanaNavList.label', { + defaultMessage: 'Kibana', }), + euiIconType: 'logoKibana', order: 1000, }, observability: { + id: 'observability', label: i18n.translate('core.ui.observabilityNavList.label', { defaultMessage: 'Observability', }), @@ -35,6 +38,7 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ order: 2000, }, security: { + id: 'security', label: i18n.translate('core.ui.securityNavList.label', { defaultMessage: 'Security', }), @@ -42,9 +46,10 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ euiIconType: 'logoSecurity', }, management: { + id: 'management', label: i18n.translate('core.ui.managementNavList.label', { defaultMessage: 'Management', }), - euiIconType: 'managementApp', + order: 5000, }, }); diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index e786890567740f..6664cf0d7366da 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -67,33 +67,33 @@ export default function(kibana) { title: i18n.translate('kbn.discoverTitle', { defaultMessage: 'Discover', }), - order: -1003, + order: 2000, url: `${kbnBaseUrl}#/discover`, euiIconType: 'discoverApp', disableSubUrlTracking: true, - category: DEFAULT_APP_CATEGORIES.analyze, + category: DEFAULT_APP_CATEGORIES.kibana, }, { id: 'kibana:visualize', title: i18n.translate('kbn.visualizeTitle', { defaultMessage: 'Visualize', }), - order: -1002, + order: 7000, url: `${kbnBaseUrl}#/visualize`, euiIconType: 'visualizeApp', disableSubUrlTracking: true, - category: DEFAULT_APP_CATEGORIES.analyze, + category: DEFAULT_APP_CATEGORIES.kibana, }, { id: 'kibana:dashboard', title: i18n.translate('kbn.dashboardTitle', { defaultMessage: 'Dashboard', }), - order: -1001, + order: 1000, url: `${kbnBaseUrl}#/dashboards`, euiIconType: 'dashboardApp', disableSubUrlTracking: true, - category: DEFAULT_APP_CATEGORIES.analyze, + category: DEFAULT_APP_CATEGORIES.kibana, }, { id: 'kibana:dev_tools', @@ -108,7 +108,7 @@ export default function(kibana) { { id: 'kibana:stack_management', title: i18n.translate('kbn.managementTitle', { - defaultMessage: 'Management', + defaultMessage: 'Stack Management', }), order: 9003, url: `${kbnBaseUrl}#/management`, diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 6a36391c56b5c3..2cba9fab7be222 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -69,7 +69,7 @@ export function updateLandingPage(version) {

diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index 64b7dfe22fd57b..91c61886d216cd 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -1172,5 +1172,25 @@ export function getUiSettingDefaults() { category: ['accessibility'], requiresPageReload: true, }, + pageNavigation: { + name: i18n.translate('kbn.advancedSettings.pageNavigationName', { + defaultMessage: 'Side nav style', + }), + value: 'modern', + description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', { + defaultMessage: 'Change the style of navigation', + }), + type: 'select', + options: ['modern', 'legacy'], + optionLabels: { + modern: i18n.translate('kbn.advancedSettings.pageNavigationModern', { + defaultMessage: 'Modern', + }), + legacy: i18n.translate('kbn.advancedSettings.pageNavigationLegacy', { + defaultMessage: 'Legacy', + }), + }, + schema: schema.oneOf([schema.literal('modern'), schema.literal('legacy')]), + }, }; } diff --git a/src/legacy/core_plugins/timelion/index.ts b/src/legacy/core_plugins/timelion/index.ts index 4fdf27d7cf6556..31926f658ec133 100644 --- a/src/legacy/core_plugins/timelion/index.ts +++ b/src/legacy/core_plugins/timelion/index.ts @@ -54,11 +54,11 @@ const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPl uiExports: { app: { title: 'Timelion', - order: -1000, + order: 8000, icon: 'plugins/timelion/icon.svg', euiIconType: 'timelionApp', main: 'plugins/timelion/app', - category: DEFAULT_APP_CATEGORIES.analyze, + category: DEFAULT_APP_CATEGORIES.kibana, }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), hacks: [resolve(__dirname, 'public/legacy')], diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap index cba8e85a652494..f0766df176c0da 100644 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap @@ -4,12 +4,7 @@ exports[`LabelTemplateFlyout should not render if not visible 1`] = `""`; exports[`LabelTemplateFlyout should render normally 1`] = ` diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap index 849e307f7b5274..fd697a2a4c70ad 100644 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap @@ -4,12 +4,7 @@ exports[`UrlTemplateFlyout should not render if not visible 1`] = `""`; exports[`UrlTemplateFlyout should render normally 1`] = ` diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap index 282e8e311d984d..6991281dc86a96 100644 --- a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap +++ b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap @@ -2,13 +2,8 @@ exports[`ScriptingHelpFlyout should render normally 1`] = ` { renderBottomBar = () => { const areChangesInvalid = this.areChangesInvalid(); - const bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', { - 'mgtAdvancedSettingsForm__bottomBar--pushForNav': - localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true', - }); + + // TODO #64541 + // Delete these classes + let bottomBarClasses = ''; + const pageNav = this.props.settings.general.find(setting => setting.name === 'pageNavigation'); + + if (pageNav?.value === 'legacy') { + bottomBarClasses = classNames('mgtAdvancedSettingsForm__bottomBar', { + 'mgtAdvancedSettingsForm__bottomBar--pushForNav': + localStorage.getItem(NAV_IS_LOCKED_KEY) === 'true', + }); + } return ( extends Component< +export class PhraseSuggestorUI extends React.Component< T, PhraseSuggestorState > { diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 6ca1b7582001f8..8ad1b5d392f3b9 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -180,6 +180,7 @@ export function SavedQueryManagementComponent({ }} anchorPosition="downLeft" panelPaddingSize="none" + buffer={-8} ownFocus >
= ({ ); const ariaLabelWithoutTitle = i18n.translate( 'embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel', - { - defaultMessage: 'Panel options', - } + { defaultMessage: 'Panel options' } ); const button = ( diff --git a/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap index 43e2af6d099e8c..eab52795fefaa3 100644 --- a/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap +++ b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap @@ -8,6 +8,7 @@ exports[`disableMsg 1`] = ` label="list control" > diff --git a/src/plugins/input_control_vis/public/components/vis/list_control.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.tsx index 6ded66917a3fda..cf95eed470bebc 100644 --- a/src/plugins/input_control_vis/public/components/vis/list_control.tsx +++ b/src/plugins/input_control_vis/public/components/vis/list_control.tsx @@ -114,6 +114,10 @@ class ListControlUi extends PureComponent void; - min?: ValueMember; - max?: ValueMember; + min?: number; + max?: number; } interface State { @@ -72,6 +72,18 @@ export class ValidatedDualRange extends Component { return null; } + // Can remove after eui#3412 is resolved + componentDidMount() { + if (this.trackRef.current) { + const track = this.trackRef.current.querySelector('.euiRangeTrack'); + if (track) { + track.setAttribute('aria-hidden', 'true'); + } + } + } + + trackRef = createRef(); + // @ts-ignore state populated by getDerivedStateFromProps state: State = {}; @@ -103,29 +115,38 @@ export class ValidatedDualRange extends Component { value, // eslint-disable-line no-unused-vars onChange, // eslint-disable-line no-unused-vars allowEmptyRange, // eslint-disable-line no-unused-vars - // @ts-ignore ...rest // TODO: Consider alternatives for spread operator in component } = this.props; return ( - - + - + isInvalid={!this.state.isValid} + error={this.state.errorMessage ? [this.state.errorMessage] : []} + label={label} + display={formRowDisplay} + > + + +
); } } diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js index 63d919377f89ea..aae58ba3e4651f 100644 --- a/src/plugins/management/public/legacy/sections_register.js +++ b/src/plugins/management/public/legacy/sections_register.js @@ -27,7 +27,7 @@ export class LegacyManagementAdapter { 'management', { display: i18n.translate('management.displayName', { - defaultMessage: 'Management', + defaultMessage: 'Stack Management', }), }, capabilities diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx index 38db1039042e57..843bbfde654eed 100644 --- a/src/plugins/management/public/management_app.tsx +++ b/src/plugins/management/public/management_app.tsx @@ -64,7 +64,7 @@ export class ManagementApp { coreStart.chrome.setBreadcrumbs([ { text: i18n.translate('management.breadcrumb', { - defaultMessage: 'Management', + defaultMessage: 'Stack Management', }), href: '#/management', }, diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 1c9e1d5c895509..df2398412dac25 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -36,8 +36,8 @@ export class ManagementPlugin implements Plugin diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index 4cbc4dd2a053c7..e3504c7c5d3013 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -17,6 +17,8 @@ * under the License. */ +// @ts-ignore +import React from 'react'; import { Action, ActionContext as Context, ActionDefinition } from './action'; import { Presentable } from '../util/presentable'; import { uiToReactComponent } from '../../../kibana_react/public'; diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap index afaea0d9b84621..43fc2671ba42a2 100644 --- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap @@ -17,7 +17,6 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = ` class="euiTextColor euiTextColor--subdued" >