From 604a02f6a12765e703bd8a2d6623120b3c92ea25 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 27 Apr 2023 17:13:14 -0700 Subject: [PATCH] [SharedUx] Chrome/Navigation package (#152510) **The API for the link definition is subject to change.** This PR provides linking functionality and structure that will give solution devs a starting point for side nav in their projects. The API uses simple hrefs for now, because it's the easiest thing to start with. Planning forward, we are thinking of a different navigation model that separates nav structure from presentation of each nav item - this will allow reuse of the structure without reusing the presentation. ## Isolated dependencies In order for this component to be usable in `main`, a bit further work is currently required in the ChromeStart service and the Serverless plugin. These links are examples of a usable implementation that link to a POC branch: * Light mode for the side nav container: https://github.com/tsullivan/kibana/commit/0a32333cdf0 * Extensions to Serverless plugin and the ChromeStart service: https://github.com/tsullivan/kibana/commit/16b0aad610f ## Summary Introduces a component to host the side navigation in Kibana. Solution teams can insert their own content, and have other small options to customize the presentation: see the storybook demos for more. Closes https://github.com/elastic/kibana/issues/154479 Closes https://github.com/elastic/kibana/issues/154484 Closes https://github.com/elastic/kibana/issues/154485 ~~Closes https://github.com/elastic/kibana/issues/154489~~ Closes https://github.com/elastic/kibana/issues/154481 Closes #154480 Closes #154486 Closes #154487 image ## Developer documentation See the Storybook demos: * run: `yarn storybook shared_ux` * Find the `Chrome > Navigation` section in the Storybook app ### Checklist Delete any items that are not applicable to this PR. - [x] Home icon links to Project's "home" - or the customer user setting - [x] Home icon shows loading indicator - [x] All the Platform links navigate to the correct place - ~~Platform links are not shown if the underlying plugin is disabled~~ - [x] Nav items define their links using `href` only - [x] All href links work - [x] Nav menu item to link to Cloud deployment - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + package.json | 1 + .../shared-ux/chrome/navigation/README.mdx | 26 +++ packages/shared-ux/chrome/navigation/index.ts | 11 + .../chrome/navigation/jest.config.js | 13 ++ .../shared-ux/chrome/navigation/kibana.jsonc | 5 + .../chrome/navigation/mocks/index.ts | 14 ++ .../chrome/navigation/mocks/src/jest.ts | 72 ++++++ .../chrome/navigation/mocks/src/storybook.ts | 59 +++++ .../shared-ux/chrome/navigation/package.json | 6 + .../navigation/src/model/create_side_nav.ts | 77 +++++++ .../chrome/navigation/src/model/index.ts | 41 ++++ .../chrome/navigation/src/model/model.ts | 107 +++++++++ .../src/model/platform_nav/analytics.ts | 33 +++ .../src/model/platform_nav/devtools.ts | 38 ++++ .../model/platform_nav/machine_learning.ts | 123 ++++++++++ .../src/model/platform_nav/management.ts | 210 ++++++++++++++++++ .../chrome/navigation/src/services.tsx | 63 ++++++ .../shared-ux/chrome/navigation/src/styles.ts | 15 ++ .../chrome/navigation/src/ui/elastic_mark.tsx | 23 ++ .../chrome/navigation/src/ui/header_logo.scss | 4 + .../chrome/navigation/src/ui/i18n_strings.ts | 30 +++ .../navigation/src/ui/navigation.stories.tsx | 167 ++++++++++++++ .../navigation/src/ui/navigation.test.tsx | 127 +++++++++++ .../chrome/navigation/src/ui/navigation.tsx | 135 +++++++++++ .../navigation/src/ui/navigation_bucket.tsx | 42 ++++ .../shared-ux/chrome/navigation/tsconfig.json | 28 +++ .../chrome/navigation/types/index.ts | 132 +++++++++++ .../chrome/navigation/types/internal.ts | 28 +++ tsconfig.base.json | 2 + yarn.lock | 4 + 31 files changed, 1637 insertions(+) create mode 100644 packages/shared-ux/chrome/navigation/README.mdx create mode 100644 packages/shared-ux/chrome/navigation/index.ts create mode 100644 packages/shared-ux/chrome/navigation/jest.config.js create mode 100644 packages/shared-ux/chrome/navigation/kibana.jsonc create mode 100644 packages/shared-ux/chrome/navigation/mocks/index.ts create mode 100644 packages/shared-ux/chrome/navigation/mocks/src/jest.ts create mode 100644 packages/shared-ux/chrome/navigation/mocks/src/storybook.ts create mode 100644 packages/shared-ux/chrome/navigation/package.json create mode 100644 packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts create mode 100644 packages/shared-ux/chrome/navigation/src/model/index.ts create mode 100644 packages/shared-ux/chrome/navigation/src/model/model.ts create mode 100644 packages/shared-ux/chrome/navigation/src/model/platform_nav/analytics.ts create mode 100644 packages/shared-ux/chrome/navigation/src/model/platform_nav/devtools.ts create mode 100644 packages/shared-ux/chrome/navigation/src/model/platform_nav/machine_learning.ts create mode 100644 packages/shared-ux/chrome/navigation/src/model/platform_nav/management.ts create mode 100644 packages/shared-ux/chrome/navigation/src/services.tsx create mode 100644 packages/shared-ux/chrome/navigation/src/styles.ts create mode 100644 packages/shared-ux/chrome/navigation/src/ui/elastic_mark.tsx create mode 100644 packages/shared-ux/chrome/navigation/src/ui/header_logo.scss create mode 100644 packages/shared-ux/chrome/navigation/src/ui/i18n_strings.ts create mode 100644 packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx create mode 100644 packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx create mode 100644 packages/shared-ux/chrome/navigation/src/ui/navigation.tsx create mode 100644 packages/shared-ux/chrome/navigation/src/ui/navigation_bucket.tsx create mode 100644 packages/shared-ux/chrome/navigation/tsconfig.json create mode 100644 packages/shared-ux/chrome/navigation/types/index.ts create mode 100644 packages/shared-ux/chrome/navigation/types/internal.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 49dde9429d95f..57531048dbdda 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -592,6 +592,7 @@ packages/shared-ux/button_toolbar @elastic/appex-sharedux packages/shared-ux/card/no_data/impl @elastic/appex-sharedux packages/shared-ux/card/no_data/mocks @elastic/appex-sharedux packages/shared-ux/card/no_data/types @elastic/appex-sharedux +packages/shared-ux/chrome/navigation @elastic/appex-sharedux packages/shared-ux/file/context @elastic/appex-sharedux packages/shared-ux/file/image/impl @elastic/appex-sharedux packages/shared-ux/file/image/mocks @elastic/appex-sharedux diff --git a/package.json b/package.json index e43aa4a92eec5..aa387dec5f854 100644 --- a/package.json +++ b/package.json @@ -592,6 +592,7 @@ "@kbn/shared-ux-card-no-data": "link:packages/shared-ux/card/no_data/impl", "@kbn/shared-ux-card-no-data-mocks": "link:packages/shared-ux/card/no_data/mocks", "@kbn/shared-ux-card-no-data-types": "link:packages/shared-ux/card/no_data/types", + "@kbn/shared-ux-chrome-navigation": "link:packages/shared-ux/chrome/navigation", "@kbn/shared-ux-file-context": "link:packages/shared-ux/file/context", "@kbn/shared-ux-file-image": "link:packages/shared-ux/file/image/impl", "@kbn/shared-ux-file-image-mocks": "link:packages/shared-ux/file/image/mocks", diff --git a/packages/shared-ux/chrome/navigation/README.mdx b/packages/shared-ux/chrome/navigation/README.mdx new file mode 100644 index 0000000000000..c2e04ebbeb98f --- /dev/null +++ b/packages/shared-ux/chrome/navigation/README.mdx @@ -0,0 +1,26 @@ +--- +id: sharedUX/Chrome/Navigation +slug: /shared-ux/chrome/navigation +title: Kibana Chrome Navigation +description: Navigation container to render items for cross-app linking +tags: ['shared-ux', 'component', 'chrome', 'navigation'] +date: 2023-02-28 +--- + +## Description + +Empty package generated by @kbn/generate +@kbn/shared-ux-chrome-navigation +Navigation container to render items for cross-app linking + +## API + +| Export | Description | +|---|---| +| `NavigationProvider` | Provides contextual services to `Navigation`. | +| `NavigationKibanaProvider` | Maps Kibana dependencies to provide contextual services to `Navigation`. | +| `Navigation` | Uses a `Provider` to access contextual services and render the component. | + +## EUI Promotion Status + +This component is not currently considered for promotion to EUI. diff --git a/packages/shared-ux/chrome/navigation/index.ts b/packages/shared-ux/chrome/navigation/index.ts new file mode 100644 index 0000000000000..f6070a30a5836 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { NavigationKibanaProvider, NavigationProvider } from './src/services'; +export { Navigation } from './src/ui/navigation'; +export type { NavigationProps, NavigationServices, NavItemProps } from './types'; diff --git a/packages/shared-ux/chrome/navigation/jest.config.js b/packages/shared-ux/chrome/navigation/jest.config.js new file mode 100644 index 0000000000000..808dc82a089c2 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/shared-ux/chrome/navigation'], +}; diff --git a/packages/shared-ux/chrome/navigation/kibana.jsonc b/packages/shared-ux/chrome/navigation/kibana.jsonc new file mode 100644 index 0000000000000..74cd1ccea3252 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-chrome-navigation", + "owner": "@elastic/appex-sharedux" +} diff --git a/packages/shared-ux/chrome/navigation/mocks/index.ts b/packages/shared-ux/chrome/navigation/mocks/index.ts new file mode 100644 index 0000000000000..a72e07ce52132 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/mocks/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + getServicesMock as getNavigationServicesMock, + getSolutionPropertiesMock, +} from './src/jest'; +export { StorybookMock as NavigationStorybookMock } from './src/storybook'; +export type { Params as NavigationStorybookParams } from './src/storybook'; diff --git a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts new file mode 100644 index 0000000000000..166dc73b6290c --- /dev/null +++ b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { NavigationServices, SolutionProperties } from '../../types'; + +export const getServicesMock = (): NavigationServices => { + const navigateToUrl = jest.fn().mockResolvedValue(undefined); + const basePath = { prepend: jest.fn((path: string) => `/base${path}`) }; + const loadingCount = 0; + + return { + basePath, + loadingCount, + navIsOpen: true, + navigateToUrl, + }; +}; + +export const getSolutionPropertiesMock = (): SolutionProperties => ({ + id: 'example_project', + icon: 'logoObservability', + name: 'Example project', + items: [ + { + id: 'root', + name: '', + items: [ + { + id: 'get_started', + name: 'Get started', + href: '/app/example_project/get_started', + }, + { + id: 'alerts', + name: 'Alerts', + href: '/app/example_project/alerts', + }, + { + id: 'cases', + name: 'Cases', + href: '/app/example_project/cases', + }, + ], + }, + { + id: 'example_settings', + name: 'Settings', + items: [ + { + id: 'logs', + name: 'Logs', + href: '/app/management/logs', + }, + { + id: 'signals', + name: 'Signals', + href: '/app/management/signals', + }, + { + id: 'tracing', + name: 'Tracing', + href: '/app/management/tracing', + }, + ], + }, + ], +}); diff --git a/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts b/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts new file mode 100644 index 0000000000000..27e27393dc5bb --- /dev/null +++ b/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock'; +import { action } from '@storybook/addon-actions'; +import { NavigationProps, NavigationServices } from '../../types'; + +type Arguments = NavigationProps & NavigationServices; +export type Params = Pick< + Arguments, + 'activeNavItemId' | 'loadingCount' | 'navIsOpen' | 'platformConfig' | 'solutions' +>; + +export class StorybookMock extends AbstractStorybookMock { + propArguments = {}; + + serviceArguments = { + navIsOpen: { + control: 'boolean', + defaultValue: true, + }, + loadingCount: { + control: 'number', + defaultValue: 0, + }, + }; + + dependencies = []; + + getServices(params: Params): NavigationServices { + const { navIsOpen } = params; + + const navAction = action('Navigate to'); + const navigateToUrl = (url: string) => { + navAction(url); + return Promise.resolve(); + }; + + return { + ...params, + basePath: { prepend: (suffix: string) => `/basepath${suffix}` }, + navigateToUrl, + navIsOpen, + }; + } + + getProps(params: Params): NavigationProps { + return { + ...params, + homeHref: '#', + linkToCloud: 'projects', + }; + } +} diff --git a/packages/shared-ux/chrome/navigation/package.json b/packages/shared-ux/chrome/navigation/package.json new file mode 100644 index 0000000000000..312c11c6f7c1e --- /dev/null +++ b/packages/shared-ux/chrome/navigation/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/shared-ux-chrome-navigation", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts b/packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts new file mode 100644 index 0000000000000..aac2fdf346ffc --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EuiSideNavItemType } from '@elastic/eui'; +import type { NavigationModelDeps } from '.'; +import type { NavItemProps, PlatformSectionConfig } from '../../types'; + +type MyEuiSideNavItem = EuiSideNavItemType; +type OnClickFn = MyEuiSideNavItem['onClick']; + +/** + * Factory function to return a function that processes modeled nav items into EuiSideNavItemType + * The factory puts memoized function arguments in scope for iterations of the recursive item processing. + */ +export const createSideNavDataFactory = ( + deps: NavigationModelDeps, + activeNavItemId: string | undefined +) => { + const { basePath, navigateToUrl } = deps; + const createSideNavData = ( + parentIds: string | number = '', + navItems: NavItemProps[], + platformSectionConfig?: PlatformSectionConfig + ): Array> => + navItems.reduce((accum, item) => { + const { id, name, items: subNav, href } = item; + const config = platformSectionConfig?.properties?.[id]; + if (config?.enabled === false) { + // return accumulated set without the item that is not enabled + return accum; + } + + let onClick: OnClickFn | undefined; + + const fullId = [parentIds, id].filter(Boolean).join('.'); + + if (href) { + onClick = (event: React.MouseEvent) => { + event.preventDefault(); + navigateToUrl(basePath.prepend(href)); + }; + } + + let filteredSubNav: MyEuiSideNavItem[] | undefined; + if (subNav) { + // recursion + const nextConfig = platformSectionConfig?.properties?.[id]; + filteredSubNav = createSideNavData(fullId, subNav, nextConfig); + } + + let isSelected: boolean = false; + let subjId = fullId; + if (!subNav && fullId === activeNavItemId) { + // if there are no subnav items and ID is current, mark the item as selected + isSelected = true; + subjId += '-selected'; + } + + const next: MyEuiSideNavItem = { + id: fullId, + name, + isSelected, + onClick, + href, + items: filteredSubNav, + ['data-test-subj']: `nav-item-${subjId}`, + }; + return [...accum, next]; + }, []); + + return createSideNavData; +}; diff --git a/packages/shared-ux/chrome/navigation/src/model/index.ts b/packages/shared-ux/chrome/navigation/src/model/index.ts new file mode 100644 index 0000000000000..8e8d94e995019 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/model/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { BasePathService, NavigateToUrlFn } from '../../types/internal'; +import { analyticsItemSet } from './platform_nav/analytics'; +import { devtoolsItemSet } from './platform_nav/devtools'; +import { mlItemSet } from './platform_nav/machine_learning'; +import { managementItemSet } from './platform_nav/management'; + +export interface NavigationModelDeps { + basePath: BasePathService; + navigateToUrl: NavigateToUrlFn; +} + +/** + * @public + */ +export enum Platform { + Recents = 'recents', + Analytics = 'analytics', + MachineLearning = 'ml', + DevTools = 'devTools', + Management = 'management', +} + +/** + * @public + */ +export const navItemSet = { + [Platform.Analytics]: analyticsItemSet, + [Platform.MachineLearning]: mlItemSet, + [Platform.DevTools]: devtoolsItemSet, + [Platform.Management]: managementItemSet, +}; + +export { NavigationModel } from './model'; diff --git a/packages/shared-ux/chrome/navigation/src/model/model.ts b/packages/shared-ux/chrome/navigation/src/model/model.ts new file mode 100644 index 0000000000000..0d3e3266be780 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/model/model.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EuiSideNavItemType } from '@elastic/eui'; +import type { NavigationModelDeps } from '.'; +import { navItemSet, Platform } from '.'; +import type { + NavigationBucketProps, + NavigationProps, + NavItemProps, + PlatformId, + PlatformSectionConfig, + SolutionProperties, +} from '../../types'; +import { createSideNavDataFactory } from './create_side_nav'; + +/** + * @internal + */ +export class NavigationModel { + private createSideNavData: ( + parentIds: string | number | undefined, + navItems: Array>, + config?: PlatformSectionConfig | undefined + ) => Array>; + + constructor( + deps: NavigationModelDeps, + private platformConfig: NavigationProps['platformConfig'] | undefined, + private solutions: SolutionProperties[], + activeNavItemId: string | undefined + ) { + this.createSideNavData = createSideNavDataFactory(deps, activeNavItemId); + } + + private convertToSideNavItems( + id: string, + items: NavItemProps[] | undefined, + platformConfig?: PlatformSectionConfig + ) { + return items ? this.createSideNavData(id, items, platformConfig) : undefined; + } + + public getPlatform(): Record { + return { + [Platform.Analytics]: { + id: Platform.Analytics, + icon: 'stats', + name: 'Data exploration', + items: this.convertToSideNavItems( + Platform.Analytics, + navItemSet[Platform.Analytics], + this.platformConfig?.[Platform.Analytics] + ), + }, + [Platform.MachineLearning]: { + id: Platform.MachineLearning, + icon: 'indexMapping', + name: 'Machine learning', + items: this.convertToSideNavItems( + Platform.MachineLearning, + navItemSet[Platform.MachineLearning], + this.platformConfig?.[Platform.MachineLearning] + ), + }, + [Platform.DevTools]: { + id: Platform.DevTools, + icon: 'editorCodeBlock', + name: 'Developer tools', + items: this.convertToSideNavItems( + Platform.DevTools, + navItemSet[Platform.DevTools], + this.platformConfig?.[Platform.DevTools] + ), + }, + [Platform.Management]: { + id: Platform.Management, + icon: 'gear', + name: 'Management', + items: this.convertToSideNavItems( + Platform.Management, + navItemSet[Platform.Management], + this.platformConfig?.[Platform.Management] + ), + }, + }; + } + + public getSolutions(): NavigationBucketProps[] { + // Allow multiple solutions' collapsible nav buckets side-by-side + return this.solutions.map((s) => ({ + id: s.id, + name: s.name, + icon: s.icon, + items: this.convertToSideNavItems(s.id, s.items), + })); + } + + public isEnabled(sectionId: PlatformId) { + return this.platformConfig?.[sectionId]?.enabled !== false; + } +} diff --git a/packages/shared-ux/chrome/navigation/src/model/platform_nav/analytics.ts b/packages/shared-ux/chrome/navigation/src/model/platform_nav/analytics.ts new file mode 100644 index 0000000000000..4e52e8c161bbd --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/model/platform_nav/analytics.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { NavItemProps } from '../../../types'; + +export const analyticsItemSet: NavItemProps[] = [ + { + name: '', + id: 'root', + items: [ + { + name: 'Discover', + id: 'discover', + href: '/app/discover', + }, + { + name: 'Dashboard', + id: 'dashboard', + href: '/app/dashboards', + }, + { + name: 'Visualize Library', + id: 'visualize_library', + href: '/app/visualize', + }, + ], + }, +]; diff --git a/packages/shared-ux/chrome/navigation/src/model/platform_nav/devtools.ts b/packages/shared-ux/chrome/navigation/src/model/platform_nav/devtools.ts new file mode 100644 index 0000000000000..049a537c96cc6 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/model/platform_nav/devtools.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { NavItemProps } from '../../../types'; + +export const devtoolsItemSet: NavItemProps[] = [ + { + name: '', + id: 'root', + items: [ + { + name: 'Console', + id: 'console', + href: '/app/dev_tools#/console', + }, + { + name: 'Search profiler', + id: 'search_profiler', + href: '/app/dev_tools#/searchprofiler', + }, + { + name: 'Grok debugger', + id: 'grok_debugger', + href: '/app/dev_tools#/grokdebugger', + }, + { + name: 'Painless lab', + id: 'painless_lab', + href: '/app/dev_tools#/painless_lab', + }, + ], + }, +]; diff --git a/packages/shared-ux/chrome/navigation/src/model/platform_nav/machine_learning.ts b/packages/shared-ux/chrome/navigation/src/model/platform_nav/machine_learning.ts new file mode 100644 index 0000000000000..692bb704dca17 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/model/platform_nav/machine_learning.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { NavItemProps } from '../../../types'; + +export const mlItemSet: NavItemProps[] = [ + { + name: '', + id: 'root', + items: [ + { + name: 'Overview', + id: 'overview', + href: '/app/ml/overview', + }, + { + name: 'Notifications', + id: 'notifications', + href: '/app/ml/notifications', + }, + ], + }, + { + name: 'Anomaly detection', + id: 'anomaly_detection', + items: [ + { + name: 'Jobs', + id: 'jobs', + href: '/app/ml/jobs', + }, + { + name: 'Anomaly explorer', + id: 'explorer', + href: '/app/ml/explorer', + }, + { + name: 'Single metric viewer', + id: 'single_metric_viewer', + href: '/app/ml/timeseriesexplorer', + }, + { + name: 'Settings', + id: 'settings', + href: '/app/ml/settings', + }, + ], + }, + { + name: 'Data frame analytics', + id: 'data_frame_analytics', + items: [ + { + name: 'Jobs', + id: 'jobs', + href: '/app/ml/data_frame_analytics', + }, + { + name: 'Results explorer', + id: 'results_explorer', + href: '/app/ml/data_frame_analytics/exploration', + }, + { + name: 'Analytics map', + id: 'analytics_map', + href: '/app/ml/data_frame_analytics/map', + }, + ], + }, + { + name: 'Model management', + id: 'model_management', + items: [ + { + name: 'Trained models', + id: 'trained_models', + href: '/app/ml/trained_models', + }, + { + name: 'Nodes', + id: 'nodes', + href: '/app/ml/nodes', + }, + ], + }, + { + name: 'Data visualizer', + id: 'data_visualizer', + items: [ + { + name: 'File', + id: 'file', + href: '/app/ml/filedatavisualizer', + }, + { + name: 'Data view', + id: 'data_view', + href: '/app/ml/datavisualizer_index_select', + }, + ], + }, + { + name: 'AIOps labs', + id: 'aiops_labs', + items: [ + { + name: 'Explain log rate spikes', + id: 'explain_log_rate_spikes', + href: '/app/ml/aiops/explain_log_rate_spikes_index_select', + }, + { + name: 'Log pattern analysis', + id: 'log_pattern_analysis', + href: '/app/ml/aiops/log_categorization_index_select', + }, + ], + }, +]; diff --git a/packages/shared-ux/chrome/navigation/src/model/platform_nav/management.ts b/packages/shared-ux/chrome/navigation/src/model/platform_nav/management.ts new file mode 100644 index 0000000000000..c1b09258d1c3b --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/model/platform_nav/management.ts @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { NavItemProps } from '../../../types'; + +export const managementItemSet: NavItemProps[] = [ + { + name: '', + id: 'root', + items: [ + { + name: 'Stack monitoring', + id: 'stack_monitoring', + href: '/app/monitoring', + }, + ], + }, + { + name: 'Integration management', + id: 'integration_management', + items: [ + { + name: 'Integrations', + id: 'integrations', + href: '/app/integrations', + }, + { + name: 'Fleet', + id: 'fleet', + href: '/app/fleet', + }, + { + name: 'Osquery', + id: 'osquery', + href: '/app/osquery', + }, + ], + }, + { + name: 'Stack management', + id: 'stack_management', + items: [ + { + name: 'Ingest', + id: 'ingest', + items: [ + { + name: 'Ingest pipelines', + id: 'ingest_pipelines', + href: '/app/management/ingest/ingest_pipelines', + }, + { + name: 'Logstash pipelines', + id: 'logstash_pipelines', + href: '/app/management/ingest/pipelines', + }, + ], + }, + { + name: 'Data', + id: 'data', + items: [ + { + name: 'Index management', + id: 'index_management', + href: '/app/management/data/index_management', + }, + { + name: 'Index lifecycle policies', + id: 'index_lifecycle_policies', + href: '/app/management/data/index_lifecycle_management', + }, + { + name: 'Snapshot and restore', + id: 'snapshot_and_restore', + href: 'app/management/data/snapshot_restore', + }, + { + name: 'Rollup jobs', + id: 'rollup_jobs', + href: '/app/management/data/rollup_jobs', + }, + { + name: 'Transforms', + id: 'transforms', + href: '/app/management/data/transform', + }, + { + name: 'Cross-cluster replication', + id: 'cross_cluster_replication', + href: '/app/management/data/cross_cluster_replication', + }, + { + name: 'Remote clusters', + id: 'remote_clusters', + href: '/app/management/data/remote_clusters', + }, + ], + }, + { + name: 'Alerts and insights', + id: 'alerts_and_insights', + items: [ + { + name: 'Rules', + id: 'rules', + href: '/app/management/insightsAndAlerting/triggersActions/rules', + }, + { + name: 'Cases', + id: 'cases', + href: '/app/management/insightsAndAlerting/cases', + }, + { + name: 'Connectors', + id: 'connectors', + href: '/app/management/insightsAndAlerting/triggersActionsConnectors/connectors', + }, + { + name: 'Reporting', + id: 'reporting', + href: '/app/management/insightsAndAlerting/reporting', + }, + { + name: 'Machine learning', + id: 'machine_learning', + href: '/app/management/insightsAndAlerting/jobsListLink', + }, + { + name: 'Watcher', + id: 'watcher', + href: '/app/management/insightsAndAlerting/watcher', + }, + ], + }, + { + name: 'Security', + id: 'security', + items: [ + { + name: 'Users', + id: 'users', + href: '/app/management/security/users', + }, + { + name: 'Roles', + id: 'roles', + href: '/app/management/security/roles', + }, + { + name: 'Role mappings', + id: 'role_mappings', + href: '/app/management/security/role_mappings', + }, + { + name: 'API keys', + id: 'api_keys', + href: '/app/management/security/api_keys', + }, + ], + }, + { + name: 'Kibana', + id: 'kibana', + items: [ + { + name: 'Data view', + id: 'data_views', + href: '/app/management/kibana/dataViews', + }, + { + name: 'Saved objects', + id: 'saved_objects', + href: '/app/management/kibana/objects', + }, + { + name: 'Tags', + id: 'tags', + href: '/app/management/kibana/tags', + }, + { + name: 'Search sessions', + id: 'search_sessions', + href: '/app/management/kibana/search_sessions', + }, + { + name: 'Spaces', + id: 'spaces', + href: '/app/management/kibana/spaces', + }, + { + name: 'Advanced settings', + id: 'advanced_settings', + href: '/app/management/kibana/settings', + }, + ], + }, + { + name: 'Upgrade assistant', + id: 'upgrade_assistant', + href: '/app/management/stack/upgrade_assistant', + }, + ], + }, +]; diff --git a/packages/shared-ux/chrome/navigation/src/services.tsx b/packages/shared-ux/chrome/navigation/src/services.tsx new file mode 100644 index 0000000000000..8235963c18681 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/services.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC, useContext } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { NavigationKibanaDependencies, NavigationServices } from '../types'; + +const Context = React.createContext(null); + +/** + * A Context Provider that provides services to the component and its dependencies. + */ +export const NavigationProvider: FC = ({ children, ...services }) => { + return {children}; +}; + +/** + * Kibana-specific Provider that maps dependencies to services. + */ +export const NavigationKibanaProvider: FC = ({ + children, + ...dependencies +}) => { + const { core } = dependencies; + const { http } = core; + const { basePath } = http; + const { navigateToUrl } = core.application; + + const loadingCount = useObservable(http.getLoadingCount$(), 0); + + const value: NavigationServices = { + basePath, + loadingCount, + navigateToUrl, + navIsOpen: true, + }; + + return ( + + {children} + + ); +}; + +/** + * React hook for accessing pre-wired services. + */ +export function useNavigation() { + const context = useContext(Context); + + if (!context) { + throw new Error( + 'Navigation Context is missing. Ensure your component or React root is wrapped with NavigationContext.' + ); + } + + return context; +} diff --git a/packages/shared-ux/chrome/navigation/src/styles.ts b/packages/shared-ux/chrome/navigation/src/styles.ts new file mode 100644 index 0000000000000..72db66e12f7bf --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/styles.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/react'; + +export const navigationStyles = { + euiSideNavItems: css` + padding-left: 45px; + `, +}; diff --git a/packages/shared-ux/chrome/navigation/src/ui/elastic_mark.tsx b/packages/shared-ux/chrome/navigation/src/ui/elastic_mark.tsx new file mode 100644 index 0000000000000..b538eb5d1756c --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/elastic_mark.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { HTMLAttributes } from 'react'; + +export const ElasticMark = ({ ...props }: HTMLAttributes) => ( + + Elastic + + +); diff --git a/packages/shared-ux/chrome/navigation/src/ui/header_logo.scss b/packages/shared-ux/chrome/navigation/src/ui/header_logo.scss new file mode 100644 index 0000000000000..f75fd9cfa2466 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/header_logo.scss @@ -0,0 +1,4 @@ +.chrHeaderLogo__mark { + margin-left: $euiSizeS; + fill: $euiColorGhost; +} diff --git a/packages/shared-ux/chrome/navigation/src/ui/i18n_strings.ts b/packages/shared-ux/chrome/navigation/src/ui/i18n_strings.ts new file mode 100644 index 0000000000000..9f6f3fbadca30 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/i18n_strings.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const getI18nStrings = () => ({ + headerLogoAriaLabel: i18n.translate( + 'sharedUXPackages.chrome.sideNavigation.headerLogo.ariaLabel', + { + defaultMessage: 'Go to home page', + } + ), + linkToCloudProjects: i18n.translate( + 'sharedUXPackages.chrome.sideNavigation.linkToCloud.projects', + { + defaultMessage: 'My projects', + } + ), + linkToCloudDeployments: i18n.translate( + 'sharedUXPackages.chrome.sideNavigation.linkToCloud.deployments', + { + defaultMessage: 'My deployments', + } + ), +}); diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx new file mode 100644 index 0000000000000..20c0c2201acfb --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiCollapsibleNav, + EuiPopover, + EuiThemeProvider, +} from '@elastic/eui'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import React, { useCallback, useState } from 'react'; +import { css } from '@emotion/react'; +import { getSolutionPropertiesMock, NavigationStorybookMock } from '../../mocks'; +import mdx from '../../README.mdx'; +import { NavigationProps, NavigationServices } from '../../types'; +import { Platform } from '../model'; +import { NavigationProvider } from '../services'; +import { Navigation as Component } from './navigation'; + +const storybookMock = new NavigationStorybookMock(); + +const SIZE_OPEN = 248; +const SIZE_CLOSED = 40; + +const Template = (args: NavigationProps & NavigationServices) => { + const services = storybookMock.getServices(args); + const props = storybookMock.getProps(args); + + const [isOpen, setIsOpen] = useState(true); + + const toggleOpen = useCallback(() => { + setIsOpen(!isOpen); + }, [isOpen, setIsOpen]); + + const collabsibleNavCSS = css` + border-inline-end-width: 1, + display: flex, + flex-direction: row, + `; + + const CollapseButton = () => { + const buttonCSS = css` + margin-left: -32px; + position: fixed; + z-index: 1000; + `; + return ( + + + + ); + }; + + return ( + + } + > + {isOpen && ( + + + + )} + + + ); +}; + +export default { + title: 'Chrome/Navigation', + description: 'Navigation container to render items for cross-app linking', + parameters: { + docs: { + page: mdx, + }, + }, + component: Template, +} as ComponentMeta; + +export const SingleExpanded: ComponentStory = Template.bind({}); +SingleExpanded.args = { + activeNavItemId: 'example_project.root.get_started', + solutions: [getSolutionPropertiesMock()], +}; +SingleExpanded.argTypes = storybookMock.getArgumentTypes(); + +export const ReducedPlatformLinks: ComponentStory = Template.bind({}); +ReducedPlatformLinks.args = { + activeNavItemId: 'example_project.root.get_started', + platformConfig: { + [Platform.Analytics]: { enabled: false }, + [Platform.MachineLearning]: { enabled: false }, + [Platform.DevTools]: { enabled: false }, + [Platform.Management]: { + properties: { + root: { + enabled: false, // disables the un-named section that contains only "Stack Monitoring" + }, + integration_management: { + properties: { + integrations: { enabled: false }, // enable only osquery + fleet: { enabled: false }, // enable only osquery + }, + }, + stack_management: { + enabled: false, // disables the stack management section + }, + }, + }, + }, + solutions: [getSolutionPropertiesMock()], +}; +ReducedPlatformLinks.argTypes = storybookMock.getArgumentTypes(); + +export const WithRequestsLoading: ComponentStory = Template.bind({}); +WithRequestsLoading.args = { + activeNavItemId: 'example_project.root.get_started', + loadingCount: 1, + solutions: [getSolutionPropertiesMock()], +}; +WithRequestsLoading.argTypes = storybookMock.getArgumentTypes(); + +export const CustomElements: ComponentStory = Template.bind({}); +CustomElements.args = { + activeNavItemId: 'example_project.custom', + solutions: [ + { + ...getSolutionPropertiesMock(), + items: [ + { + name: ( + + Custom element + + } + isOpen={true} + anchorPosition="rightCenter" + > + Cool popover content + + ), + id: 'custom', + }, + ], + }, + ], +}; +CustomElements.argTypes = storybookMock.getArgumentTypes(); diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx new file mode 100644 index 0000000000000..075925f05de6a --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { getServicesMock } from '../../mocks/src/jest'; +import { PlatformConfigSet, SolutionProperties } from '../../types'; +import { Platform } from '../model'; +import { NavigationProvider } from '../services'; +import { Navigation } from './navigation'; + +describe('', () => { + const services = getServicesMock(); + + const homeHref = '#'; + let platformSections: PlatformConfigSet | undefined; + let solutions: SolutionProperties[]; + + beforeEach(() => { + platformSections = { analytics: {}, ml: {}, devTools: {}, management: {} }; + solutions = [{ id: 'navigation_testing', name: 'Navigation testing', icon: 'gear' }]; + }); + + test('renders the header logo and top-level navigation buckets', async () => { + const { findByTestId, findByText } = render( + + + + ); + + expect(await findByText('Navigation testing')).toBeVisible(); + + expect(await findByTestId('nav-header-logo')).toBeVisible(); + expect(await findByTestId('nav-bucket-navigation_testing')).toBeVisible(); + expect(await findByTestId('nav-bucket-analytics')).toBeVisible(); + expect(await findByTestId('nav-bucket-ml')).toBeVisible(); + expect(await findByTestId('nav-bucket-devTools')).toBeVisible(); + expect(await findByTestId('nav-bucket-management')).toBeVisible(); + }); + + test('includes link to deployments', async () => { + const { findByText } = render( + + + + ); + + expect(await findByText('My deployments')).toBeVisible(); + }); + + test('platform links can be disabled', async () => { + platformSections = { + [Platform.Analytics]: { enabled: false }, + [Platform.MachineLearning]: { enabled: false }, + [Platform.DevTools]: { enabled: false }, + [Platform.Management]: { enabled: false }, + }; + + const { findByTestId, queryByTestId } = render( + + + + ); + + expect(await findByTestId('nav-header-logo')).toBeVisible(); + expect(queryByTestId('nav-bucket-analytics')).not.toBeInTheDocument(); + expect(queryByTestId('nav-bucket-ml')).not.toBeInTheDocument(); + expect(queryByTestId('nav-bucket-devTools')).not.toBeInTheDocument(); + expect(queryByTestId('nav-bucket-management')).not.toBeInTheDocument(); + }); + + test('sets the specified nav item to active', async () => { + solutions[0].items = [ + { + id: 'root', + name: '', + items: [ + { + id: 'city', + name: 'City', + }, + { + id: 'town', + name: 'Town', + }, + ], + }, + ]; + + const { findByTestId } = render( + + + + ); + + const label = await findByTestId('nav-item-navigation_testing.root.city-selected'); + expect(label).toHaveTextContent('City'); + expect(label).toBeVisible(); + }); + + test('shows loading state', async () => { + services.loadingCount = 5; + + const { findByTestId } = render( + + + + ); + + expect(await findByTestId('nav-header-loading-spinner')).toBeVisible(); + }); +}); diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx new file mode 100644 index 0000000000000..60f3af10a6b54 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiCollapsibleNavGroup, + EuiFlexGroup, + EuiFlexItem, + EuiHeaderLogo, + EuiLink, + EuiLoadingSpinner, + EuiSpacer, + useEuiTheme, +} from '@elastic/eui'; +import React from 'react'; +import { getI18nStrings } from './i18n_strings'; +import { NavigationBucketProps, NavigationProps } from '../../types'; +import { NavigationModel } from '../model'; +import { useNavigation } from '../services'; +import { ElasticMark } from './elastic_mark'; +import './header_logo.scss'; +import { NavigationBucket } from './navigation_bucket'; + +export const Navigation = (props: NavigationProps) => { + const { loadingCount, activeNavItemId, ...services } = useNavigation(); + const { euiTheme } = useEuiTheme(); + + const activeNav = activeNavItemId ?? props.activeNavItemId; + + const nav = new NavigationModel(services, props.platformConfig, props.solutions, activeNav); + + const solutions = nav.getSolutions(); + const { analytics, ml, devTools, management } = nav.getPlatform(); + + const strings = getI18nStrings(); + + const NavHeader = () => { + const homeUrl = services.basePath.prepend(props.homeHref); + const navigateHome = (event: React.MouseEvent) => { + event.preventDefault(); + services.navigateToUrl(homeUrl); + }; + const logo = + loadingCount === 0 ? ( + + ) : ( + + + + ); + + return ( + <> + {logo} + {services.navIsOpen ? ( + + ) : null} + + ); + }; + + const LinkToCloud = () => { + switch (props.linkToCloud) { + case 'projects': + return ( + + + + ); + case 'deployments': + return ( + + + + ); + default: + return null; + } + }; + + // higher-order-component to keep the common props DRY + const NavigationBucketHoc = (outerProps: Omit) => ( + + ); + + return ( + + + + + + + + + {solutions.map((solutionBucket, idx) => { + return ; + })} + + {nav.isEnabled('analytics') ? : null} + {nav.isEnabled('ml') ? : null} + + + + + + + + {nav.isEnabled('devTools') ? : null} + {nav.isEnabled('management') ? : null} + + + ); +}; diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation_bucket.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation_bucket.tsx new file mode 100644 index 0000000000000..0b0d6b5b223ab --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation_bucket.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiCollapsibleNavGroup, EuiIcon, EuiSideNav, EuiText } from '@elastic/eui'; +import React from 'react'; +import { NavigationBucketProps } from '../../types'; +import { useNavigation } from '../services'; +import { navigationStyles as styles } from '../styles'; + +export const NavigationBucket = (opts: NavigationBucketProps) => { + const { id, items, activeNavItemId, ...props } = opts; + const { navIsOpen } = useNavigation(); + + if (navIsOpen) { + return ( + + + + + + ); + } + + return ( +
+ +
+
+ ); +}; diff --git a/packages/shared-ux/chrome/navigation/tsconfig.json b/packages/shared-ux/chrome/navigation/tsconfig.json new file mode 100644 index 0000000000000..05fd9d0346421 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@emotion/react/types/css-prop", + "@testing-library/jest-dom", + "@testing-library/react", + "@kbn/ambient-ui-types" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx" + ], + "kbn_references": [ + "@kbn/core-application-browser", + "@kbn/core-http-browser", + "@kbn/shared-ux-storybook-mock", + "@kbn/i18n" + ], + "exclude": [ + "target/**/*" + ] +} diff --git a/packages/shared-ux/chrome/navigation/types/index.ts b/packages/shared-ux/chrome/navigation/types/index.ts new file mode 100644 index 0000000000000..bd7aaddcb67c6 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/types/index.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EuiSideNavItemType, IconType } from '@elastic/eui'; +import { Observable } from 'rxjs'; +import { BasePathService, NavigateToUrlFn, RecentItem } from './internal'; + +/** + * A list of services that are consumed by this component. + * @public + */ +export interface NavigationServices { + activeNavItemId?: string; + basePath: BasePathService; + loadingCount: number; + navIsOpen: boolean; + navigateToUrl: NavigateToUrlFn; +} + +/** + * An interface containing a collection of Kibana dependencies required to + * render this component + * @public + */ +export interface NavigationKibanaDependencies { + core: { + application: { navigateToUrl: NavigateToUrlFn }; + chrome: { + recentlyAccessed: { get$: () => Observable }; + }; + http: { + basePath: BasePathService; + getLoadingCount$(): Observable; + }; + }; +} + +/** + * Props for the `NavItem` component representing the content of a navigational item with optional children. + * @public + */ +export type NavItemProps = Pick, 'id' | 'name'> & { + /** + * Nav Items + */ + items?: Array>; + /** + * Href for a link destination + * Example: /app/fleet + */ + href?: string; +}; + +/** + * @public + */ +export interface PlatformSectionConfig { + enabled?: boolean; + properties?: Record; +} + +/** + * @public + */ +export interface SolutionProperties { + /** + * Solutions' navigation items + */ + items?: NavItemProps[]; + /** + * Solutions' navigation collapsible nav ID + */ + id: string; + /** + * Name to show as title for Solutions' collapsible nav "bucket" + */ + name: React.ReactNode; + /** + * Solution logo, i.e. "logoObservability" + */ + icon: IconType; +} + +/** + * @public + */ +export type PlatformId = 'analytics' | 'ml' | 'devTools' | 'management'; + +/** + * Object that will allow parts of the platform-controlled nav to be hidden + * @public + */ +export type PlatformConfigSet = Record; + +/** + * Props for the `Navigation` component. + * @public + */ +export interface NavigationProps { + /** + * ID of sections to initially open + * Path to the nav item is given with hierarchy expressed in dotted notation. + * Example: `my_project.settings.index_management` + */ + activeNavItemId?: string; + /** + * Configuration for Solutions' section(s) + */ + solutions: SolutionProperties[]; + /** + * Controls over how Platform nav sections appear + */ + platformConfig?: Partial; + /** + * Target for the logo icon + */ + homeHref: string; + /** + * Control of the link that takes the user to their projects or deployments + */ + linkToCloud?: 'projects' | 'deployments'; +} + +export type NavigationBucketProps = (SolutionProperties & + Pick) & { + platformConfig?: PlatformSectionConfig; +}; diff --git a/packages/shared-ux/chrome/navigation/types/internal.ts b/packages/shared-ux/chrome/navigation/types/internal.ts new file mode 100644 index 0000000000000..6808c391073a9 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/types/internal.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { IBasePath } from '@kbn/core-http-browser'; + +/** + * @internal + */ +export type SetActiveNavItemIdFn = (activeNavItemId: string) => void; + +/** + * @internal + */ +export interface RecentItem { + link: string; + label: string; + id: string; +} + +export type NavigateToUrlFn = ApplicationStart['navigateToUrl']; + +export type BasePathService = Pick; diff --git a/tsconfig.base.json b/tsconfig.base.json index 226408426f2e1..f76b33567ae51 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1178,6 +1178,8 @@ "@kbn/shared-ux-card-no-data-mocks/*": ["packages/shared-ux/card/no_data/mocks/*"], "@kbn/shared-ux-card-no-data-types": ["packages/shared-ux/card/no_data/types"], "@kbn/shared-ux-card-no-data-types/*": ["packages/shared-ux/card/no_data/types/*"], + "@kbn/shared-ux-chrome-navigation": ["packages/shared-ux/chrome/navigation"], + "@kbn/shared-ux-chrome-navigation/*": ["packages/shared-ux/chrome/navigation/*"], "@kbn/shared-ux-file-context": ["packages/shared-ux/file/context"], "@kbn/shared-ux-file-context/*": ["packages/shared-ux/file/context/*"], "@kbn/shared-ux-file-image": ["packages/shared-ux/file/image/impl"], diff --git a/yarn.lock b/yarn.lock index b6ef2964ab860..8f14fddb53d9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5093,6 +5093,10 @@ version "0.0.0" uid "" +"@kbn/shared-ux-chrome-navigation@link:packages/shared-ux/chrome/navigation": + version "0.0.0" + uid "" + "@kbn/shared-ux-file-context@link:packages/shared-ux/file/context": version "0.0.0" uid ""