Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Serverless] Add panels to side nav #167774

Merged
merged 87 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from 76 commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
5039c90
Add panel provider and context
sebelga Sep 28, 2023
0a7f27d
Fix story to render example project
sebelga Sep 28, 2023
cf4f5b3
Update PanelProvider and navigation UI to open panel
sebelga Sep 29, 2023
5a7bb52
Add story to test panel
sebelga Sep 29, 2023
ab1545a
Update DefaultContent component
sebelga Sep 29, 2023
3f66dd4
Handle custom content to be rendered in panel
sebelga Sep 29, 2023
8ccf19c
Provide collapsible state from ChromeService
sebelga Sep 29, 2023
f15423d
Add isSideNavCollapsed prop
sebelga Sep 29, 2023
e203a8e
Refactor types to extend NodeDefinitionBase with common props
sebelga Oct 2, 2023
9b284bf
Remove unnecessary basePath prop
sebelga Oct 2, 2023
8bb93dc
Add default content for group with title
sebelga Oct 2, 2023
d1a19cb
Add default content for groups with no title
sebelga Oct 2, 2023
af4b85b
Add isCollapsible and appendHorizontalRule props
sebelga Oct 3, 2023
1e6a507
Add panel example with components UI
sebelga Oct 3, 2023
60caa5e
Add badge example with component UI
sebelga Oct 3, 2023
62fb844
Fix z-index for side nav panel
sebelga Oct 3, 2023
82693a0
Fix jest tests
sebelga Oct 3, 2023
72872d1
Fix TS issue
sebelga Oct 3, 2023
91e9e49
Fix TS issue
sebelga Oct 3, 2023
bf8a2a8
WIP button to open panel in main nav
sebelga Oct 9, 2023
cb0e94b
WIP component integration tests
sebelga Oct 9, 2023
814fdea
Update type and cleanup navigation section UI
sebelga Oct 9, 2023
65c6c17
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Oct 9, 2023
5a5eec4
Fix render custom component
sebelga Oct 9, 2023
549f701
Merge branch 'serverless-chrome/add-panel-to-side-nav' of github.com:…
sebelga Oct 9, 2023
6a14d71
Automatically wrap panel children items into root group
sebelga Oct 9, 2023
529f317
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Oct 9, 2023
7ad62ac
Generate unique node id if not present
sebelga Oct 9, 2023
b1ac7b5
Merge branch 'serverless-chrome/add-panel-to-side-nav' of github.com:…
sebelga Oct 9, 2023
b5a9f7a
Add support for "renderAsItem" status in side nav panel
sebelga Oct 10, 2023
86eeeb8
Add support for "hidden" status in side nav panel
sebelga Oct 10, 2023
e38c5c0
Add support for "hidden" status in main nav panel
sebelga Oct 10, 2023
c3f7d8c
Don't render open panel button when all children are hidden
sebelga Oct 11, 2023
e02880c
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Oct 11, 2023
55c7f6e
Use "renderAs" instead of "renderAsItem"
sebelga Oct 11, 2023
6eaa626
Add icon in panel examples
sebelga Oct 11, 2023
5c1dd5b
Read title from children if it's a string
sebelga Oct 11, 2023
561430d
Merge branch 'serverless-chrome/add-panel-to-side-nav' of github.com:…
sebelga Oct 11, 2023
4cc0ff9
Pass additional props to custom content component
sebelga Oct 12, 2023
ffb3c4f
Refactor node path retriever
sebelga Oct 12, 2023
34dfb79
Add support for "navItem" inside the root definition
sebelga Oct 12, 2023
59c1e24
Generate a fix ID based on the node position in the tree
sebelga Oct 12, 2023
a6b8c79
Add example of renderAs item in the main nav
sebelga Oct 12, 2023
85771f0
Fix jest tests
sebelga Oct 12, 2023
af87c94
Set fix key for recently accessed items
sebelga Oct 12, 2023
03a3f35
Add comment to story
sebelga Oct 12, 2023
2e4dd19
Move EuiCollapsibleNavItem for top nav item in NavigationItem comp
sebelga Oct 12, 2023
77d8fcb
Merge remote-tracking branch 'upstream/main' into serverless-chrome/a…
sebelga Oct 12, 2023
b741114
Merge branch 'main' into serverless-chrome/add-panel-to-side-nav
sebelga Oct 13, 2023
6721b9a
Unify how node id are generated
sebelga Oct 13, 2023
634e05f
Update jest test + add example for links at root level
sebelga Oct 13, 2023
5b0586f
Export util to get node href value
sebelga Oct 13, 2023
49d7511
Refactor NavigationSectionUI to no repeat props definition
sebelga Oct 13, 2023
0159b2b
Add story example of group with link and no children
sebelga Oct 13, 2023
771e3fe
Clean up
sebelga Oct 13, 2023
92e4c88
Improve typing
sebelga Oct 13, 2023
ac158a1
Revert jest tests
sebelga Oct 13, 2023
b187124
Use Array instead of NonEmptyArray
sebelga Oct 13, 2023
56bd4f6
Update jest snapshot
sebelga Oct 13, 2023
a0732b2
Merge branch 'main' into serverless-chrome/add-panel-to-side-nav
sebelga Oct 13, 2023
e2c8ab1
Remove NodeDefinition cast in story
sebelga Oct 13, 2023
1926322
Fix TS issue
sebelga Oct 13, 2023
424b7a7
Fix functional test helper
sebelga Oct 13, 2023
4420bca
Fix group node visibility
sebelga Oct 13, 2023
cf957e7
Merge remote-tracking branch 'upstream/main' into serverless-chrome/a…
sebelga Oct 13, 2023
e45e7c7
Tiny refactor
sebelga Oct 13, 2023
62ea1cf
Own CR
sebelga Oct 13, 2023
83d5ea3
Fix functional test helper
sebelga Oct 13, 2023
704f86e
Fix functional test helper (2)
sebelga Oct 14, 2023
4fd1ce0
Merge remote-tracking branch 'upstream/main' into serverless-chrome/a…
sebelga Oct 14, 2023
a2ec089
Another try...
sebelga Oct 14, 2023
9754850
More refactors
sebelga Oct 16, 2023
f907f6e
Fix initialOpen + functional test
sebelga Oct 16, 2023
138536a
Merge remote-tracking branch 'upstream/main' into serverless-chrome/a…
sebelga Oct 16, 2023
b78e7cd
Fix TS issue
sebelga Oct 16, 2023
35fdfa6
Skip functional test for auto expand accordion
sebelga Oct 16, 2023
186755c
Fix functional test
sebelga Oct 16, 2023
cbf154d
Merge remote-tracking branch 'upstream/main' into serverless-chrome/a…
sebelga Oct 16, 2023
66e15cc
Fix testSubj selector
sebelga Oct 16, 2023
b914201
Merge remote-tracking branch 'upstream/main' into serverless-chrome/a…
sebelga Oct 16, 2023
97cb14e
Merge branch 'main' into serverless-chrome/add-panel-to-side-nav
kibanamachine Oct 16, 2023
85587fd
Update test selectors
sebelga Oct 17, 2023
e959033
Merge remote-tracking branch 'upstream/main' into serverless-chrome/a…
sebelga Oct 17, 2023
9d5e9e7
Merge branch 'serverless-chrome/add-panel-to-side-nav' of github.com:…
sebelga Oct 17, 2023
6d38a3a
Update test selectors
sebelga Oct 17, 2023
daeb4ac
Merge branch 'main' into serverless-chrome/add-panel-to-side-nav
sebelga Oct 17, 2023
90d0566
Merge branch 'main' into serverless-chrome/add-panel-to-side-nav
sebelga Oct 18, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export class ChromeService {
private readonly docTitle = new DocTitleService();
private readonly projectNavigation = new ProjectNavigationService();
private mutationObserver: MutationObserver | undefined;
private readonly isSideNavCollapsed$ = new BehaviorSubject<boolean>(true);

constructor(private readonly params: ConstructorParams) {}

Expand Down Expand Up @@ -386,6 +387,9 @@ export class ChromeService {
docLinks={docLinks}
kibanaVersion={injectedMetadata.getKibanaVersion()}
prependBasePath={http.basePath.prepend}
toggleSideNav={(isCollapsed) => {
this.isSideNavCollapsed$.next(isCollapsed);
}}
>
<SideNavComponent activeNodes={activeNodes} />
</ProjectHeader>
Expand Down Expand Up @@ -508,6 +512,7 @@ export class ChromeService {
getBodyClasses$: () => bodyClasses$.pipe(takeUntil(this.stop$)),
setChromeStyle,
getChromeStyle$: () => chromeStyle$.pipe(takeUntil(this.stop$)),
getIsSideNavCollapsed$: () => this.isSideNavCollapsed$.asObservable(),
project: {
setHome: setProjectHome,
setProjectsUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ interface AppMenuBarProps {
}
export const AppMenuBar = ({ headerActionMenuMounter }: AppMenuBarProps) => {
const { euiTheme } = useEuiTheme();
const zIndex =
typeof euiTheme.levels.header === 'number'
? euiTheme.levels.header - 1 // We want it to appear right below the header
: euiTheme.levels.header;

return (
<div
className="header__actionMenu"
data-test-subj="kibanaProjectHeaderActionMenu"
css={css`
z-index: ${euiTheme.levels.header};
z-index: ${zIndex};
background: ${euiTheme.colors.emptyShade};
border-bottom: ${euiTheme.border.thin};
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('Header', () => {
navControlsCenter$: Rx.of([]),
navControlsRight$: Rx.of([]),
prependBasePath: (str) => `hello/world/${str}`,
toggleSideNav: jest.fn(),
};

it('renders', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface Props {
navControlsCenter$: Observable<ChromeNavControl[]>;
navControlsRight$: Observable<ChromeNavControl[]>;
prependBasePath: (url: string) => string;
toggleSideNav: (isCollapsed: boolean) => void;
}

const LOADING_DEBOUNCE_TIME = 80;
Expand Down Expand Up @@ -172,6 +173,7 @@ export const ProjectHeader = ({
children,
prependBasePath,
docLinks,
toggleSideNav,
...observables
}: Props) => {
const headerActionMenuMounter = useHeaderActionMenuMounter(observables.actionMenu$);
Expand All @@ -196,7 +198,7 @@ export const ProjectHeader = ({
<EuiHeader position="fixed" className="header__firstBar">
<EuiHeaderSection grow={false}>
<Router history={application.history}>
<ProjectNavigation>{children}</ProjectNavigation>
<ProjectNavigation toggleSideNav={toggleSideNav}>{children}</ProjectNavigation>
</Router>

<EuiHeaderSectionItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,39 @@
* Side Public License, v 1.
*/

import React, { useEffect, useRef } from 'react';
import { EuiCollapsibleNavBeta } from '@elastic/eui';
import React from 'react';
import useLocalStorage from 'react-use/lib/useLocalStorage';

const LOCAL_STORAGE_IS_COLLAPSED_KEY = 'PROJECT_NAVIGATION_COLLAPSED' as const;

export const ProjectNavigation: React.FC = ({ children }) => {
export const ProjectNavigation: React.FC<{
toggleSideNav: (isVisible: boolean) => void;
}> = ({ children, toggleSideNav }) => {
const isMounted = useRef(false);
const [isCollapsed, setIsCollapsed] = useLocalStorage(LOCAL_STORAGE_IS_COLLAPSED_KEY, false);
const onCollapseToggle = (nextIsCollapsed: boolean) => {
setIsCollapsed(nextIsCollapsed);
toggleSideNav(nextIsCollapsed);
};

useEffect(() => {
if (!isMounted.current && isCollapsed !== undefined) {
toggleSideNav(isCollapsed);
}
isMounted.current = true;
}, [isCollapsed, toggleSideNav]);

return (
<EuiCollapsibleNavBeta
data-test-subj="projectLayoutSideNav"
initialIsCollapsed={isCollapsed}
onCollapseToggle={onCollapseToggle}
css={isCollapsed ? { display: 'none;' } : {}}
css={
isCollapsed
? { display: 'none;' }
: { overflow: 'visible', clipPath: 'polygon(0 0, 300% 0, 300% 100%, 0 100%)' }
}
>
{children}
</EuiCollapsibleNavBeta>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const createStartContractMock = () => {
setBadge: jest.fn(),
getBreadcrumbs$: jest.fn(),
setBreadcrumbs: jest.fn(),
getIsSideNavCollapsed$: jest.fn(),
getBreadcrumbsAppendExtension$: jest.fn(),
setBreadcrumbsAppendExtension: jest.fn(),
getGlobalHelpExtensionMenuLinks$: jest.fn(),
Expand Down
2 changes: 2 additions & 0 deletions packages/core/chrome/core-chrome-browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ export type {
CloudLinkId,
SideNavCompProps,
SideNavComponent,
SideNavNodeStatus,
ChromeProjectBreadcrumb,
ChromeSetProjectBreadcrumbsParams,
NodeDefinition,
NodeDefinitionWithChildren,
NodeRenderAs,
} from './src';
5 changes: 5 additions & 0 deletions packages/core/chrome/core-chrome-browser/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,9 @@ export interface ChromeStart {
* Get an observable of the current style type of the chrome.
*/
getChromeStyle$(): Observable<ChromeStyle>;

/**
* Get an observable of the current collapsed state of the side nav.
*/
getIsSideNavCollapsed$(): Observable<boolean>;
}
2 changes: 2 additions & 0 deletions packages/core/chrome/core-chrome-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ export type {
CloudLinkId,
SideNavCompProps,
SideNavComponent,
SideNavNodeStatus,
ChromeSetProjectBreadcrumbsParams,
ChromeProjectBreadcrumb,
NodeDefinition,
NodeDefinitionWithChildren,
RenderAs as NodeRenderAs,
} from './project_navigation';
152 changes: 104 additions & 48 deletions packages/core/chrome/core-chrome-browser/src/project_navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import type { ComponentType } from 'react';
import type { Location } from 'history';
import { EuiAccordionProps } from '@elastic/eui';
import { EuiAccordionProps, IconType } from '@elastic/eui';
import type { AppId as DevToolsApp, DeepLinkId as DevToolsLink } from '@kbn/deeplinks-devtools';
import type {
AppId as AnalyticsApp,
Expand Down Expand Up @@ -49,6 +49,10 @@ export type AppDeepLinkId =
/** @public */
export type CloudLinkId = 'userAndRoles' | 'performance' | 'billingAndSub' | 'deployment';

export type SideNavNodeStatus = 'hidden' | 'visible';

export type RenderAs = 'block' | 'accordion' | 'panelOpener' | 'item';

export type GetIsActiveFn = (params: {
/** The current path name including the basePath + hash value but **without** any query params */
pathNameSerialized: string;
Expand All @@ -58,8 +62,99 @@ export type GetIsActiveFn = (params: {
prepend: (path: string) => string;
}) => boolean;

/**
* Base definition of navigation nodes. A node can either be a "group" or an "item".
* Each have commmon properties and specific properties.
*/
interface NodeDefinitionBase {
/**
* Optional icon for the navigation node. Note: not all navigation depth will render the icon
*/
icon?: IconType;
/**
* href for absolute links only. Internal links should use "link".
*/
href?: string;
/**
* Optional status to indicate if the breadcrumb should be hidden when this node is active.
* @default 'visible'
*/
breadcrumbStatus?: 'hidden' | 'visible';
/**
* Optional status to for the side navigation. "hidden" and "visible" are self explanatory.
* The `renderAsItem` status is _only_ for group nodes (nodes with children declared or with
* the "nodeType" set to `group`) and allow to render the node as an "item" instead of the head of
* a group. This is usefull to have sub-pages declared in the tree that will correctly be mapped
* in the Breadcrumbs, but are not rendered in the side navigation.
* @default 'visible'
*/
sideNavStatus?: SideNavNodeStatus;
/**
* Optional function to get the active state. This function is called whenever the location changes.
*/
getIsActive?: GetIsActiveFn;
/**
* ----------------------------------------------------------------------------------------------
* ------------------------------- GROUP NODES ONLY PROPS ---------------------------------------
* ----------------------------------------------------------------------------------------------
*/
/**
* ["group" nodes only] Optional flag to indicate if the node must be treated as a group title.
* Can not be used with `children`
*/
isGroupTitle?: boolean;
/**
* ["group" nodes only] Property to indicate how the group should be rendered.
* - Accordion: wraps the items in an EuiAccordion
* - PanelOpener: renders a button to open a panel on the right of the side nav
* - item: renders the group as an item in the side nav
* @default 'block'
*/
renderAs?: RenderAs;
/**
* ["group" nodes only] Optional flag to indicate if a horizontal rule should be rendered after the node.
* Note: this property is currently only used for (1) "group" nodes and (2) in the navigation
* panel opening on the right of the side nav.
*/
appendHorizontalRule?: boolean;
/**
* ["group" nodes only] Temp prop. Will be removed once the new navigation is fully implemented.
*/
accordionProps?: Partial<EuiAccordionProps>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In a next PR, working on #167323 I will remove this prop and use the renderAs: 'accordion' instead.

/**
* ----------------------------------------------------------------------------------------------
* -------------------------------- ITEM NODES ONLY PROPS ---------------------------------------
* ----------------------------------------------------------------------------------------------
*/
/**
* ["item" nodes only] Optional flag to indicate if the target page should be opened in a new Browser tab.
* Note: this property is currently only used in the navigation panel opening on the right of the side nav.
*/
openInNewTab?: boolean;
/**
* ["item" nodes only] Optional flag to indicate if a badge should be rendered next to the text.
* Note: this property is currently only used in the navigation panel opening on the right of the side nav.
*/
withBadge?: boolean;
/**
* ["item" nodes only] If `withBadge` is true, this object can be used to customize the badge.
*/
badgeOptions?: {
/** The text of the badge. Default: "Beta" */
text?: string;
};
}

/** @public */
export interface ChromeProjectNavigationNode {
/**
* Chrome project navigation node. This is the tree definition stored in the Chrome service
* that is generated based on the NodeDefinition below.
* Some of the process that occurs between the 2 are:
* - "link" prop get converted to existing ChromNavLink
* - "path" is added to each node based on where it is located in the tree
* - "isActive" state is set for each node if its URL matches the current location
*/
export interface ChromeProjectNavigationNode extends NodeDefinitionBase {
/** Optional id, if not passed a "link" must be provided. */
id: string;
/** Optional title. If not provided and a "link" is provided the title will be the Deep link title */
Expand All @@ -68,32 +163,15 @@ export interface ChromeProjectNavigationNode {
path: string[];
/** App id or deeplink id */
deepLink?: ChromeNavLink;
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
icon?: string;
/** Optional flag to indicate if the node must be treated as a group title */
isGroupTitle?: boolean;
/** Optional children of the navigation node */
children?: ChromeProjectNavigationNode[];
/**
* href for absolute links only. Internal links should use "link".
* Optional children of the navigation node. Once a node has "children" defined it is
* considered a "group" node.
*/
href?: string;
children?: ChromeProjectNavigationNode[];
/**
* Flag to indicate if the node is currently active.
*/
isActive?: boolean;
/**
* Optional function to get the active state. This function is called whenever the location changes.
*/
getIsActive?: GetIsActiveFn;

/**
* Optional flag to indicate if the breadcrumb should be hidden when this node is active.
* @default 'visible'
*/
breadcrumbStatus?: 'hidden' | 'visible';

accordionProps?: Partial<EuiAccordionProps>;
}

/** @public */
Expand All @@ -120,7 +198,8 @@ export interface ChromeSetProjectBreadcrumbsParams {
absolute: boolean;
}

type NonEmptyArray<T> = [T, ...T[]];
// --- NOTE: The following types are the ones that the consumer uses to configure their navigation.
// --- They are converted to the ChromeProjectNavigationNode type above.

/**
* @public
Expand All @@ -134,7 +213,7 @@ export interface NodeDefinition<
LinkId extends AppDeepLinkId = AppDeepLinkId,
Id extends string = string,
ChildrenId extends string = Id
> {
> extends NodeDefinitionBase {
/** Optional id, if not passed a "link" must be provided. */
id?: Id;
/** Optional title. If not provided and a "link" is provided the title will be the Deep link title */
Expand All @@ -143,31 +222,8 @@ export interface NodeDefinition<
link?: LinkId;
/** Cloud link id */
cloudLink?: CloudLinkId;
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
icon?: string;
/**
* Optional flag to indicate if the node must be treated as a group title.
* Can not be used with `children`
*/
isGroupTitle?: boolean;
/** Optional children of the navigation node. Can not be used with `isGroupTitle` */
children?: NonEmptyArray<NodeDefinition<LinkId, Id, ChildrenId>>;
/**
* Use href for absolute links only. Internal links should use "link".
*/
href?: string;
/**
* Optional function to get the active state. This function is called whenever the location changes.
*/
getIsActive?: GetIsActiveFn;

/**
* Optional flag to indicate if the breadcrumb should be hidden when this node is active.
* @default 'visible'
*/
breadcrumbStatus?: 'hidden' | 'visible';

accordionProps?: Partial<EuiAccordionProps>;
children?: Array<NodeDefinition<LinkId, Id, ChildrenId>>;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/shared-ux/chrome/navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ export { DefaultNavigation, getPresets, Navigation } from './src/ui';

export type {
GroupDefinition,
PresetDefinition,
ItemDefinition,
NavigationGroupPreset,
NavigationTreeDefinition,
ProjectNavigationDefinition,
RecentlyAccessedDefinition,
RootNavigationItemDefinition,
PanelComponentProps,
PanelContent,
} from './src/ui';

export type { NavigationServices } from './types';
1 change: 1 addition & 0 deletions packages/shared-ux/chrome/navigation/mocks/src/jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const getServicesMock = ({
navigateToUrl,
onProjectNavigationChange: jest.fn(),
activeNodes$: of(activeNodes),
isSideNavCollapsed: false,
cloudLinks: {
billingAndSub: {
title: 'Mock Billing & Subscriptions',
Expand Down
Loading