diff --git a/packages/shared-ux/chrome/navigation/index.ts b/packages/shared-ux/chrome/navigation/index.ts index f6070a30a5836..9f9e15bacb207 100644 --- a/packages/shared-ux/chrome/navigation/index.ts +++ b/packages/shared-ux/chrome/navigation/index.ts @@ -8,4 +8,10 @@ export { NavigationKibanaProvider, NavigationProvider } from './src/services'; export { Navigation } from './src/ui/navigation'; -export type { NavigationProps, NavigationServices, NavItemProps } from './types'; +export type { + ChromeNavigation, + ChromeNavigationViewModel, + NavigationServices, + ChromeNavigationNode, + ChromeNavigationNodeViewModel, +} from './types'; diff --git a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts index 166dc73b6290c..1f8356195eadd 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { NavigationServices, SolutionProperties } from '../../types'; +import { NavigationServices, ChromeNavigationNodeViewModel } from '../../types'; export const getServicesMock = (): NavigationServices => { const navigateToUrl = jest.fn().mockResolvedValue(undefined); @@ -21,49 +21,49 @@ export const getServicesMock = (): NavigationServices => { }; }; -export const getSolutionPropertiesMock = (): SolutionProperties => ({ +export const getSolutionPropertiesMock = (): ChromeNavigationNodeViewModel => ({ id: 'example_project', icon: 'logoObservability', - name: 'Example project', + title: 'Example project', items: [ { id: 'root', - name: '', + title: '', items: [ { id: 'get_started', - name: 'Get started', + title: 'Get started', href: '/app/example_project/get_started', }, { id: 'alerts', - name: 'Alerts', + title: 'Alerts', href: '/app/example_project/alerts', }, { id: 'cases', - name: 'Cases', + title: 'Cases', href: '/app/example_project/cases', }, ], }, { id: 'example_settings', - name: 'Settings', + title: 'Settings', items: [ { id: 'logs', - name: 'Logs', + title: 'Logs', href: '/app/management/logs', }, { id: 'signals', - name: 'Signals', + title: 'Signals', href: '/app/management/signals', }, { id: 'tracing', - name: 'Tracing', + title: '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 index 27e27393dc5bb..d269f5ca56ae5 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts @@ -8,15 +8,18 @@ import { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock'; import { action } from '@storybook/addon-actions'; -import { NavigationProps, NavigationServices } from '../../types'; +import { ChromeNavigationViewModel, NavigationServices } from '../../types'; -type Arguments = NavigationProps & NavigationServices; +type Arguments = ChromeNavigationViewModel & NavigationServices; export type Params = Pick< Arguments, - 'activeNavItemId' | 'loadingCount' | 'navIsOpen' | 'platformConfig' | 'solutions' + 'activeNavItemId' | 'loadingCount' | 'navIsOpen' | 'platformConfig' | 'navigationTree' >; -export class StorybookMock extends AbstractStorybookMock { +export class StorybookMock extends AbstractStorybookMock< + ChromeNavigationViewModel, + NavigationServices +> { propArguments = {}; serviceArguments = { @@ -49,7 +52,7 @@ export class StorybookMock extends AbstractStorybookMock; -type OnClickFn = MyEuiSideNavItem['onClick']; +import type { ChromeNavigationNodeViewModel, PlatformSectionConfig } from '../../types'; /** - * 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. + * Navigation node parser. It filers out the nodes disabled through config and + * sets the `path` of each of the nodes. + * + * @param items Navigation nodes + * @param platformConfig Configuration with flags to disable nodes in the navigation tree + * + * @returns The navigation tree filtered */ -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; +export const parseNavItems = ( + parentIds: string[] = [], + navItems?: ChromeNavigationNodeViewModel[], + platformSectionConfig?: PlatformSectionConfig +): ChromeNavigationNodeViewModel[] | undefined => { + if (!navItems) { + return undefined; + } + + return navItems.reduce((accum, item) => { + const config = platformSectionConfig?.properties?.[item.id]; + if (config?.enabled === false) { + // return accumulated set without the item that is not enabled + return accum; + } + + const path = [...parentIds, item.id].filter(Boolean).join('.'); + + let filteredItems: ChromeNavigationNodeViewModel[] | undefined; + if (item.items) { + // recursion + const nextPlatformSectionConfig = platformSectionConfig?.properties?.[item.id]; + filteredItems = parseNavItems([...parentIds, item.id], item.items, nextPlatformSectionConfig); + } + + return [...accum, { ...item, path, items: filteredItems }]; + }, []); }; diff --git a/packages/shared-ux/chrome/navigation/src/model/model.ts b/packages/shared-ux/chrome/navigation/src/model/model.ts index 0d3e3266be780..6ed5f2dc795e7 100644 --- a/packages/shared-ux/chrome/navigation/src/model/model.ts +++ b/packages/shared-ux/chrome/navigation/src/model/model.ts @@ -6,54 +6,27 @@ * 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'; +import type { ChromeNavigationNodeViewModel, PlatformId, PlatformConfigSet } from '../../types'; +import { parseNavItems } 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; - } + private platformConfig: Partial | undefined, + private solutions: ChromeNavigationNodeViewModel[] + ) {} - public getPlatform(): Record { + public getPlatform(): Record { return { [Platform.Analytics]: { id: Platform.Analytics, icon: 'stats', - name: 'Data exploration', - items: this.convertToSideNavItems( - Platform.Analytics, + title: 'Data exploration', + items: parseNavItems( + [Platform.Analytics], navItemSet[Platform.Analytics], this.platformConfig?.[Platform.Analytics] ), @@ -61,9 +34,9 @@ export class NavigationModel { [Platform.MachineLearning]: { id: Platform.MachineLearning, icon: 'indexMapping', - name: 'Machine learning', - items: this.convertToSideNavItems( - Platform.MachineLearning, + title: 'Machine learning', + items: parseNavItems( + [Platform.MachineLearning], navItemSet[Platform.MachineLearning], this.platformConfig?.[Platform.MachineLearning] ), @@ -71,9 +44,9 @@ export class NavigationModel { [Platform.DevTools]: { id: Platform.DevTools, icon: 'editorCodeBlock', - name: 'Developer tools', - items: this.convertToSideNavItems( - Platform.DevTools, + title: 'Developer tools', + items: parseNavItems( + [Platform.DevTools], navItemSet[Platform.DevTools], this.platformConfig?.[Platform.DevTools] ), @@ -81,9 +54,9 @@ export class NavigationModel { [Platform.Management]: { id: Platform.Management, icon: 'gear', - name: 'Management', - items: this.convertToSideNavItems( - Platform.Management, + title: 'Management', + items: parseNavItems( + [Platform.Management], navItemSet[Platform.Management], this.platformConfig?.[Platform.Management] ), @@ -91,13 +64,13 @@ export class NavigationModel { }; } - public getSolutions(): NavigationBucketProps[] { + public getSolutions(): ChromeNavigationNodeViewModel[] { // Allow multiple solutions' collapsible nav buckets side-by-side return this.solutions.map((s) => ({ id: s.id, - name: s.name, + title: s.title, icon: s.icon, - items: this.convertToSideNavItems(s.id, s.items), + items: parseNavItems([s.id], s.items), })); } 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 index 4e52e8c161bbd..7722a5981ba3b 100644 --- a/packages/shared-ux/chrome/navigation/src/model/platform_nav/analytics.ts +++ b/packages/shared-ux/chrome/navigation/src/model/platform_nav/analytics.ts @@ -6,25 +6,29 @@ * Side Public License, v 1. */ -import { NavItemProps } from '../../../types'; +import { ChromeNavigationNodeViewModel } from '../../../types'; -export const analyticsItemSet: NavItemProps[] = [ +// TODO: Declare ChromeNavigationNode[] (with "link" to app id or deeplink id) +// and then call an api on the Chrome service to convert to ChromeNavigationNodeViewModel +// with its "href", "isActive"... metadata + +export const analyticsItemSet: ChromeNavigationNodeViewModel[] = [ { - name: '', + title: '', id: 'root', items: [ { - name: 'Discover', + title: 'Discover', id: 'discover', href: '/app/discover', }, { - name: 'Dashboard', + title: 'Dashboard', id: 'dashboard', href: '/app/dashboards', }, { - name: 'Visualize Library', + title: '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 index 049a537c96cc6..64ef0aa26571e 100644 --- a/packages/shared-ux/chrome/navigation/src/model/platform_nav/devtools.ts +++ b/packages/shared-ux/chrome/navigation/src/model/platform_nav/devtools.ts @@ -6,30 +6,34 @@ * Side Public License, v 1. */ -import { NavItemProps } from '../../../types'; +import { ChromeNavigationNodeViewModel } from '../../../types'; -export const devtoolsItemSet: NavItemProps[] = [ +// TODO: Declare ChromeNavigationNode[] (with "link" to app id or deeplink id) +// and then call an api on the Chrome service to convert to ChromeNavigationNodeViewModel +// with its "href", "isActive"... metadata + +export const devtoolsItemSet: ChromeNavigationNodeViewModel[] = [ { - name: '', + title: '', id: 'root', items: [ { - name: 'Console', + title: 'Console', id: 'console', href: '/app/dev_tools#/console', }, { - name: 'Search profiler', + title: 'Search profiler', id: 'search_profiler', href: '/app/dev_tools#/searchprofiler', }, { - name: 'Grok debugger', + title: 'Grok debugger', id: 'grok_debugger', href: '/app/dev_tools#/grokdebugger', }, { - name: 'Painless lab', + title: '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 index 692bb704dca17..2c205c5784632 100644 --- 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 @@ -6,115 +6,119 @@ * Side Public License, v 1. */ -import { NavItemProps } from '../../../types'; +import { ChromeNavigationNodeViewModel } from '../../../types'; -export const mlItemSet: NavItemProps[] = [ +// TODO: Declare ChromeNavigationNode[] (with "link" to app id or deeplink id) +// and then call an api on the Chrome service to convert to ChromeNavigationNodeViewModel +// with its "href", "isActive"... metadata + +export const mlItemSet: ChromeNavigationNodeViewModel[] = [ { - name: '', + title: '', id: 'root', items: [ { - name: 'Overview', + title: 'Overview', id: 'overview', href: '/app/ml/overview', }, { - name: 'Notifications', + title: 'Notifications', id: 'notifications', href: '/app/ml/notifications', }, ], }, { - name: 'Anomaly detection', + title: 'Anomaly detection', id: 'anomaly_detection', items: [ { - name: 'Jobs', + title: 'Jobs', id: 'jobs', href: '/app/ml/jobs', }, { - name: 'Anomaly explorer', + title: 'Anomaly explorer', id: 'explorer', href: '/app/ml/explorer', }, { - name: 'Single metric viewer', + title: 'Single metric viewer', id: 'single_metric_viewer', href: '/app/ml/timeseriesexplorer', }, { - name: 'Settings', + title: 'Settings', id: 'settings', href: '/app/ml/settings', }, ], }, { - name: 'Data frame analytics', + title: 'Data frame analytics', id: 'data_frame_analytics', items: [ { - name: 'Jobs', + title: 'Jobs', id: 'jobs', href: '/app/ml/data_frame_analytics', }, { - name: 'Results explorer', + title: 'Results explorer', id: 'results_explorer', href: '/app/ml/data_frame_analytics/exploration', }, { - name: 'Analytics map', + title: 'Analytics map', id: 'analytics_map', href: '/app/ml/data_frame_analytics/map', }, ], }, { - name: 'Model management', + title: 'Model management', id: 'model_management', items: [ { - name: 'Trained models', + title: 'Trained models', id: 'trained_models', href: '/app/ml/trained_models', }, { - name: 'Nodes', + title: 'Nodes', id: 'nodes', href: '/app/ml/nodes', }, ], }, { - name: 'Data visualizer', + title: 'Data visualizer', id: 'data_visualizer', items: [ { - name: 'File', + title: 'File', id: 'file', href: '/app/ml/filedatavisualizer', }, { - name: 'Data view', + title: 'Data view', id: 'data_view', href: '/app/ml/datavisualizer_index_select', }, ], }, { - name: 'AIOps labs', + title: 'AIOps labs', id: 'aiops_labs', items: [ { - name: 'Explain log rate spikes', + title: 'Explain log rate spikes', id: 'explain_log_rate_spikes', href: '/app/ml/aiops/explain_log_rate_spikes_index_select', }, { - name: 'Log pattern analysis', + title: '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 index c1b09258d1c3b..a481c5458c693 100644 --- a/packages/shared-ux/chrome/navigation/src/model/platform_nav/management.ts +++ b/packages/shared-ux/chrome/navigation/src/model/platform_nav/management.ts @@ -6,202 +6,206 @@ * Side Public License, v 1. */ -import { NavItemProps } from '../../../types'; +import { ChromeNavigationNodeViewModel } from '../../../types'; -export const managementItemSet: NavItemProps[] = [ +// TODO: Declare ChromeNavigationNode[] (with "link" to app id or deeplink id) +// and then call an api on the Chrome service to convert to ChromeNavigationNodeViewModel +// with its "href", "isActive"... metadata + +export const managementItemSet: ChromeNavigationNodeViewModel[] = [ { - name: '', + title: '', id: 'root', items: [ { - name: 'Stack monitoring', + title: 'Stack monitoring', id: 'stack_monitoring', href: '/app/monitoring', }, ], }, { - name: 'Integration management', + title: 'Integration management', id: 'integration_management', items: [ { - name: 'Integrations', + title: 'Integrations', id: 'integrations', href: '/app/integrations', }, { - name: 'Fleet', + title: 'Fleet', id: 'fleet', href: '/app/fleet', }, { - name: 'Osquery', + title: 'Osquery', id: 'osquery', href: '/app/osquery', }, ], }, { - name: 'Stack management', + title: 'Stack management', id: 'stack_management', items: [ { - name: 'Ingest', + title: 'Ingest', id: 'ingest', items: [ { - name: 'Ingest pipelines', + title: 'Ingest pipelines', id: 'ingest_pipelines', href: '/app/management/ingest/ingest_pipelines', }, { - name: 'Logstash pipelines', + title: 'Logstash pipelines', id: 'logstash_pipelines', href: '/app/management/ingest/pipelines', }, ], }, { - name: 'Data', + title: 'Data', id: 'data', items: [ { - name: 'Index management', + title: 'Index management', id: 'index_management', href: '/app/management/data/index_management', }, { - name: 'Index lifecycle policies', + title: 'Index lifecycle policies', id: 'index_lifecycle_policies', href: '/app/management/data/index_lifecycle_management', }, { - name: 'Snapshot and restore', + title: 'Snapshot and restore', id: 'snapshot_and_restore', href: 'app/management/data/snapshot_restore', }, { - name: 'Rollup jobs', + title: 'Rollup jobs', id: 'rollup_jobs', href: '/app/management/data/rollup_jobs', }, { - name: 'Transforms', + title: 'Transforms', id: 'transforms', href: '/app/management/data/transform', }, { - name: 'Cross-cluster replication', + title: 'Cross-cluster replication', id: 'cross_cluster_replication', href: '/app/management/data/cross_cluster_replication', }, { - name: 'Remote clusters', + title: 'Remote clusters', id: 'remote_clusters', href: '/app/management/data/remote_clusters', }, ], }, { - name: 'Alerts and insights', + title: 'Alerts and insights', id: 'alerts_and_insights', items: [ { - name: 'Rules', + title: 'Rules', id: 'rules', href: '/app/management/insightsAndAlerting/triggersActions/rules', }, { - name: 'Cases', + title: 'Cases', id: 'cases', href: '/app/management/insightsAndAlerting/cases', }, { - name: 'Connectors', + title: 'Connectors', id: 'connectors', href: '/app/management/insightsAndAlerting/triggersActionsConnectors/connectors', }, { - name: 'Reporting', + title: 'Reporting', id: 'reporting', href: '/app/management/insightsAndAlerting/reporting', }, { - name: 'Machine learning', + title: 'Machine learning', id: 'machine_learning', href: '/app/management/insightsAndAlerting/jobsListLink', }, { - name: 'Watcher', + title: 'Watcher', id: 'watcher', href: '/app/management/insightsAndAlerting/watcher', }, ], }, { - name: 'Security', + title: 'Security', id: 'security', items: [ { - name: 'Users', + title: 'Users', id: 'users', href: '/app/management/security/users', }, { - name: 'Roles', + title: 'Roles', id: 'roles', href: '/app/management/security/roles', }, { - name: 'Role mappings', + title: 'Role mappings', id: 'role_mappings', href: '/app/management/security/role_mappings', }, { - name: 'API keys', + title: 'API keys', id: 'api_keys', href: '/app/management/security/api_keys', }, ], }, { - name: 'Kibana', + title: 'Kibana', id: 'kibana', items: [ { - name: 'Data view', + title: 'Data view', id: 'data_views', href: '/app/management/kibana/dataViews', }, { - name: 'Saved objects', + title: 'Saved objects', id: 'saved_objects', href: '/app/management/kibana/objects', }, { - name: 'Tags', + title: 'Tags', id: 'tags', href: '/app/management/kibana/tags', }, { - name: 'Search sessions', + title: 'Search sessions', id: 'search_sessions', href: '/app/management/kibana/search_sessions', }, { - name: 'Spaces', + title: 'Spaces', id: 'spaces', href: '/app/management/kibana/spaces', }, { - name: 'Advanced settings', + title: 'Advanced settings', id: 'advanced_settings', href: '/app/management/kibana/settings', }, ], }, { - name: 'Upgrade assistant', + title: 'Upgrade assistant', id: 'upgrade_assistant', href: '/app/management/stack/upgrade_assistant', }, diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx index 20c0c2201acfb..9652d83597119 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx @@ -18,7 +18,7 @@ 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 { ChromeNavigationViewModel, NavigationServices } from '../../types'; import { Platform } from '../model'; import { NavigationProvider } from '../services'; import { Navigation as Component } from './navigation'; @@ -28,7 +28,7 @@ const storybookMock = new NavigationStorybookMock(); const SIZE_OPEN = 248; const SIZE_CLOSED = 40; -const Template = (args: NavigationProps & NavigationServices) => { +const Template = (args: ChromeNavigationViewModel & NavigationServices) => { const services = storybookMock.getServices(args); const props = storybookMock.getProps(args); @@ -97,7 +97,7 @@ export default { export const SingleExpanded: ComponentStory = Template.bind({}); SingleExpanded.args = { activeNavItemId: 'example_project.root.get_started', - solutions: [getSolutionPropertiesMock()], + navigationTree: [getSolutionPropertiesMock()], }; SingleExpanded.argTypes = storybookMock.getArgumentTypes(); @@ -125,7 +125,7 @@ ReducedPlatformLinks.args = { }, }, }, - solutions: [getSolutionPropertiesMock()], + navigationTree: [getSolutionPropertiesMock()], }; ReducedPlatformLinks.argTypes = storybookMock.getArgumentTypes(); @@ -133,19 +133,19 @@ export const WithRequestsLoading: ComponentStory = Template.bin WithRequestsLoading.args = { activeNavItemId: 'example_project.root.get_started', loadingCount: 1, - solutions: [getSolutionPropertiesMock()], + navigationTree: [getSolutionPropertiesMock()], }; WithRequestsLoading.argTypes = storybookMock.getArgumentTypes(); export const CustomElements: ComponentStory = Template.bind({}); CustomElements.args = { activeNavItemId: 'example_project.custom', - solutions: [ + navigationTree: [ { ...getSolutionPropertiesMock(), items: [ { - name: ( + title: ( diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx index 075925f05de6a..653b66887054b 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.test.tsx @@ -9,7 +9,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { getServicesMock } from '../../mocks/src/jest'; -import { PlatformConfigSet, SolutionProperties } from '../../types'; +import { PlatformConfigSet, ChromeNavigationNodeViewModel } from '../../types'; import { Platform } from '../model'; import { NavigationProvider } from '../services'; import { Navigation } from './navigation'; @@ -19,17 +19,21 @@ describe('', () => { const homeHref = '#'; let platformSections: PlatformConfigSet | undefined; - let solutions: SolutionProperties[]; + let solutions: ChromeNavigationNodeViewModel[]; beforeEach(() => { platformSections = { analytics: {}, ml: {}, devTools: {}, management: {} }; - solutions = [{ id: 'navigation_testing', name: 'Navigation testing', icon: 'gear' }]; + solutions = [{ id: 'navigation_testing', title: 'Navigation testing', icon: 'gear' }]; }); test('renders the header logo and top-level navigation buckets', async () => { const { findByTestId, findByText } = render( - + ); @@ -48,7 +52,7 @@ describe('', () => { @@ -68,7 +72,11 @@ describe('', () => { const { findByTestId, queryByTestId } = render( - + ); @@ -83,15 +91,15 @@ describe('', () => { solutions[0].items = [ { id: 'root', - name: '', + title: '', items: [ { id: 'city', - name: 'City', + title: 'City', }, { id: 'town', - name: 'Town', + title: 'Town', }, ], }, @@ -101,7 +109,7 @@ describe('', () => { @@ -118,7 +126,11 @@ describe('', () => { const { findByTestId } = render( - + ); diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx index 60f3af10a6b54..ed58e9f57c4e5 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx @@ -18,20 +18,33 @@ import { } from '@elastic/eui'; import React from 'react'; import { getI18nStrings } from './i18n_strings'; -import { NavigationBucketProps, NavigationProps } from '../../types'; +import type { ChromeNavigationViewModel } 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(); +import { NavigationBucket, type Props as NavigationBucketProps } from './navigation_bucket'; + +interface Props extends ChromeNavigationViewModel { + /** + * ID of sections to highlight + */ + activeNavItemId?: string; +} + +export const Navigation = ({ + platformConfig, + navigationTree, + homeHref, + linkToCloud, + activeNavItemId: activeNavItemIdProps, +}: Props) => { + const { loadingCount, activeNavItemId, basePath, navIsOpen, navigateToUrl } = useNavigation(); const { euiTheme } = useEuiTheme(); - const activeNav = activeNavItemId ?? props.activeNavItemId; + const activeNav = activeNavItemId ?? activeNavItemIdProps; - const nav = new NavigationModel(services, props.platformConfig, props.solutions, activeNav); + const nav = new NavigationModel(platformConfig, navigationTree); const solutions = nav.getSolutions(); const { analytics, ml, devTools, management } = nav.getPlatform(); @@ -39,10 +52,10 @@ export const Navigation = (props: NavigationProps) => { const strings = getI18nStrings(); const NavHeader = () => { - const homeUrl = services.basePath.prepend(props.homeHref); + const homeUrl = basePath.prepend(homeHref); const navigateHome = (event: React.MouseEvent) => { event.preventDefault(); - services.navigateToUrl(homeUrl); + navigateToUrl(homeUrl); }; const logo = loadingCount === 0 ? ( @@ -66,15 +79,13 @@ export const Navigation = (props: NavigationProps) => { return ( <> {logo} - {services.navIsOpen ? ( - - ) : null} + {navIsOpen ? : null} ); }; const LinkToCloud = () => { - switch (props.linkToCloud) { + switch (linkToCloud) { case 'projects': return ( { - {solutions.map((solutionBucket, idx) => { - return ; + {solutions.map((navTree, idx) => { + return ; })} - {nav.isEnabled('analytics') ? : null} - {nav.isEnabled('ml') ? : null} + {nav.isEnabled('analytics') ? : null} + {nav.isEnabled('ml') ? : null} @@ -127,8 +138,8 @@ export const Navigation = (props: NavigationProps) => { - {nav.isEnabled('devTools') ? : null} - {nav.isEnabled('management') ? : 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 index 0b0d6b5b223ab..fa3142773bb0e 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation_bucket.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation_bucket.tsx @@ -6,28 +6,84 @@ * Side Public License, v 1. */ -import { EuiCollapsibleNavGroup, EuiIcon, EuiSideNav, EuiText } from '@elastic/eui'; +import { + EuiCollapsibleNavGroup, + EuiIcon, + EuiSideNav, + EuiSideNavItemType, + EuiText, +} from '@elastic/eui'; import React from 'react'; -import { NavigationBucketProps } from '../../types'; +import { ChromeNavigationNodeViewModel } from '../../types'; +import type { BasePathService, NavigateToUrlFn } from '../../types/internal'; import { useNavigation } from '../services'; import { navigationStyles as styles } from '../styles'; -export const NavigationBucket = (opts: NavigationBucketProps) => { - const { id, items, activeNavItemId, ...props } = opts; - const { navIsOpen } = useNavigation(); +const navigationNodeToEuiItem = ( + item: ChromeNavigationNodeViewModel, + { + navigateToUrl, + basePath, + activeNavItemId, + }: { activeNavItemId?: string; navigateToUrl: NavigateToUrlFn; basePath: BasePathService } +): EuiSideNavItemType => { + const path = item.path ?? item.id; + + let subjId = path; + let isSelected: boolean = false; + + if (!item.items && path === activeNavItemId) { + // if there are no subnav items and ID is current, mark the item as selected + isSelected = true; + subjId += '-selected'; + } + + return { + id: path, + name: item.title, + isSelected, + onClick: + item.href !== undefined + ? (event: React.MouseEvent) => { + event.preventDefault(); + navigateToUrl(basePath.prepend(item.href!)); + } + : undefined, + href: item.href, + items: item.items?.map((_item) => + navigationNodeToEuiItem(_item, { navigateToUrl, basePath, activeNavItemId }) + ), + ['data-test-subj']: `nav-item-${subjId}`, + }; +}; + +export interface Props { + navigationTree: ChromeNavigationNodeViewModel; + activeNavItemId?: string; +} + +export const NavigationBucket = (props: Props) => { + const { navigationTree, activeNavItemId } = props; + const { navIsOpen, navigateToUrl, basePath } = useNavigation(); + const { id, title, icon, items } = navigationTree; if (navIsOpen) { return ( - + + navigationNodeToEuiItem(item, { navigateToUrl, basePath, activeNavItemId }) + )} + css={styles.euiSideNavItems} + /> ); @@ -35,7 +91,7 @@ export const NavigationBucket = (opts: NavigationBucketProps) => { return (
- +
); diff --git a/packages/shared-ux/chrome/navigation/types/index.ts b/packages/shared-ux/chrome/navigation/types/index.ts index bd7aaddcb67c6..f58c9dc6836d1 100644 --- a/packages/shared-ux/chrome/navigation/types/index.ts +++ b/packages/shared-ux/chrome/navigation/types/index.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import type { EuiSideNavItemType, IconType } from '@elastic/eui'; -import { Observable } from 'rxjs'; -import { BasePathService, NavigateToUrlFn, RecentItem } from './internal'; +import type { ReactNode } from 'react'; +import type { Observable } from 'rxjs'; +import type { BasePathService, NavigateToUrlFn, RecentItem } from './internal'; /** * A list of services that are consumed by this component. @@ -40,93 +40,111 @@ export interface NavigationKibanaDependencies { }; } +/** @public */ +export type ChromeNavigationLink = string; + /** - * Props for the `NavItem` component representing the content of a navigational item with optional children. + * Chrome navigatioin node definition. + * * @public */ -export type NavItemProps = Pick, 'id' | 'name'> & { - /** - * Nav Items - */ - items?: Array>; - /** - * Href for a link destination - * Example: /app/fleet - */ - href?: string; -}; +export interface ChromeNavigationNode { + /** An optional id. If not provided a link must be passed */ + id?: string; + /** An optional title for the node */ + title?: string | ReactNode; + /** An optional eui icon */ + icon?: string; + /** An app id or a deeplink id */ + link?: ChromeNavigationLink; + /** Sub navigation item for this node */ + items?: ChromeNavigationNode[]; +} /** + * Chrome navigation definition used internally in the components. + * Each "link" (if present) has been converted to a propert href. Additional metadata has been added + * like the "isActive" flag or the "path" (indicating the full path of the node in the nav tree). + * * @public */ -export interface PlatformSectionConfig { - enabled?: boolean; - properties?: Record; +export interface ChromeNavigationNodeViewModel extends Omit { + id: string; + /** + * Full path that points to this node (includes all parent ids). If not set + * the path is the id + */ + path?: string; + isActive?: boolean; + href?: string; + items?: ChromeNavigationNodeViewModel[]; } /** + * External definition of the side navigation. + * * @public */ -export interface SolutionProperties { +export interface ChromeNavigation { /** - * Solutions' navigation items + * Target for the logo icon. Must be an app id or a deeplink id. */ - items?: NavItemProps[]; + homeLink: ChromeNavigationLink; /** - * Solutions' navigation collapsible nav ID + * Control of the link that takes the user to their projects or deployments */ - id: string; + linkToCloud?: 'projects' | 'deployments'; /** - * Name to show as title for Solutions' collapsible nav "bucket" + * The navigation tree definition. + * + * NOTE: For now this tree will _only_ contain the solution tree and we will concatenate + * the different platform trees inside the component. + * In a following work we will build the full navigation tree inside a "buildNavigationTree()" + * helper exposed from this package. This helper will allow an array of PlatformId to be disabled + * + * e.g. buildNavigationTree({ solutionTree: [...], disable: ['devTools'] }) */ - name: React.ReactNode; + navigationTree: ChromeNavigationNode[]; /** - * Solution logo, i.e. "logoObservability" + * Controls over which Platform nav sections is enabled or disabled. + * NOTE: this is a temporary solution until we have the buildNavigationTree() helper mentioned + * above. */ - icon: IconType; + platformConfig?: Partial; } /** - * @public + * Internal definition of the side navigation. + * + * @internal */ -export type PlatformId = 'analytics' | 'ml' | 'devTools' | 'management'; +export interface ChromeNavigationViewModel + extends Pick { + /** + * Target for the logo icon + */ + homeHref: string; + /** + * The navigation tree definition + */ + navigationTree: ChromeNavigationNodeViewModel[]; +} /** - * Object that will allow parts of the platform-controlled nav to be hidden * @public */ -export type PlatformConfigSet = Record; +export interface PlatformSectionConfig { + enabled?: boolean; + properties?: 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 PlatformId = 'analytics' | 'ml' | 'devTools' | 'management'; -export type NavigationBucketProps = (SolutionProperties & - Pick) & { - platformConfig?: PlatformSectionConfig; -}; +/** + * Object that will allow parts of the platform-controlled nav to be hidden + * @public + */ +export type PlatformConfigSet = Record;