Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/shared-ux/chrome/navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
22 changes: 11 additions & 11 deletions packages/shared-ux/chrome/navigation/mocks/src/jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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',
},
],
Expand Down
13 changes: 8 additions & 5 deletions packages/shared-ux/chrome/navigation/mocks/src/storybook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NavigationProps, NavigationServices> {
export class StorybookMock extends AbstractStorybookMock<
ChromeNavigationViewModel,
NavigationServices
> {
propArguments = {};

serviceArguments = {
Expand Down Expand Up @@ -49,7 +52,7 @@ export class StorybookMock extends AbstractStorybookMock<NavigationProps, Naviga
};
}

getProps(params: Params): NavigationProps {
getProps(params: Params): ChromeNavigationViewModel {
return {
...params,
homeHref: '#',
Expand Down
100 changes: 35 additions & 65 deletions packages/shared-ux/chrome/navigation/src/model/create_side_nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,72 +6,42 @@
* Side Public License, v 1.
*/

import type { EuiSideNavItemType } from '@elastic/eui';
import type { NavigationModelDeps } from '.';
import type { NavItemProps, PlatformSectionConfig } from '../../types';

type MyEuiSideNavItem = EuiSideNavItemType<unknown>;
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<EuiSideNavItemType<unknown>> =>
navItems.reduce<MyEuiSideNavItem[]>((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 = (
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This utility handler is only in charge of building the path of each node and to remove any node that is not enabled.

The conversion to a MyEuiSideNavItem will occur at the component level (inside navigation_bucket.tsx)

parentIds: string[] = [],
navItems?: ChromeNavigationNodeViewModel[],
platformSectionConfig?: PlatformSectionConfig
): ChromeNavigationNodeViewModel[] | undefined => {
if (!navItems) {
return undefined;
}

return navItems.reduce<ChromeNavigationNodeViewModel[]>((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 }];
}, []);
};
69 changes: 21 additions & 48 deletions packages/shared-ux/chrome/navigation/src/model/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,98 +6,71 @@
* 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<NavItemProps<unknown>>,
config?: PlatformSectionConfig | undefined
) => Array<EuiSideNavItemType<unknown>>;

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<PlatformConfigSet> | undefined,
private solutions: ChromeNavigationNodeViewModel[]
) {}

public getPlatform(): Record<PlatformId, NavigationBucketProps> {
public getPlatform(): Record<PlatformId, ChromeNavigationNodeViewModel> {
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]
),
},
[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]
),
},
[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]
),
},
[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]
),
},
};
}

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),
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Contributor Author

@sebelga sebelga May 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to not use href here but link. The same way links will be defined when calling the chrome.project.setNavigation(/*<config-with-nodes-and-links>*/).

This can be done as a second chunk of work to limit the changes in this PR.

// 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',
},
Expand Down
Loading