From a11947cb6bdeb70727505dcd2bf47db27d07a2df Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Fri, 4 Apr 2025 00:34:23 +0200 Subject: [PATCH] [Solution Side Nav] Add back external link indicator to sidenav (#215946) ## Summary This PR adds back external link indicator to sidenav items. ![Screenshot 2025-04-01 at 20 35 14](https://github.com/user-attachments/assets/c82def72-780e-4bb6-812d-d51244e90685) (cherry picked from commit e21739b657d57a884b7a8da713a34f4f5c8273a2) # Conflicts: # src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx --- .../project_navigation_service.test.ts | 38 +++++++++---------- .../src/project_navigation/utils.ts | 6 +-- .../chrome/browser/src/project_navigation.ts | 2 +- .../ui/components/navigation_section_ui.tsx | 9 +++-- .../ui/components/panel/panel_nav_item.tsx | 7 +++- .../navigation/src/ui/navigation.stories.tsx | 3 ++ 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/core/packages/chrome/browser-internal/src/project_navigation/project_navigation_service.test.ts b/src/core/packages/chrome/browser-internal/src/project_navigation/project_navigation_service.test.ts index 124b44e80e30f..29428a55a289d 100644 --- a/src/core/packages/chrome/browser-internal/src/project_navigation/project_navigation_service.test.ts +++ b/src/core/packages/chrome/browser-internal/src/project_navigation/project_navigation_service.test.ts @@ -175,7 +175,7 @@ describe('initNavigation()', () => { path: 'group1.foo', href: '/app/foo', deepLink: getNavLink({ id: 'foo', title: 'FOO' }), - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', }); expect(node.children![0].href).toBe(node.children![0]!.deepLink!.href); @@ -240,14 +240,14 @@ describe('initNavigation()', () => { title: '', path: 'node-1', type: 'navGroup', - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', children: [ { id: 'node-0', // auto generated path: 'node-1.node-0', title: '', - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', children: [ { @@ -261,7 +261,7 @@ describe('initNavigation()', () => { }, href: '/app/foo', id: 'foo', - isElasticInternalLink: false, + isExternalLink: false, path: 'node-1.node-0.foo', sideNavStatus: 'visible', title: 'FOO', @@ -277,21 +277,21 @@ describe('initNavigation()', () => { title: 'Footer group', path: 'node-4', type: 'navGroup', - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', children: [ { id: 'node-0', // auto generated path: 'node-4.node-0', title: '', - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', children: [ { deepLink: expect.any(Object), // we are not testing the deepLink here href: '/app/foo', id: 'foo', - isElasticInternalLink: false, + isExternalLink: false, path: 'node-4.node-0.foo', sideNavStatus: 'visible', title: 'FOO', @@ -330,7 +330,7 @@ describe('initNavigation()', () => { }, "href": "/app/discover", "id": "discover", - "isElasticInternalLink": false, + "isExternalLink": false, "onClick": undefined, "path": "rootNav:analytics.discover", "sideNavStatus": "visible", @@ -349,7 +349,7 @@ describe('initNavigation()', () => { }, "href": "/app/dashboards", "id": "dashboards", - "isElasticInternalLink": false, + "isExternalLink": false, "onClick": undefined, "path": "rootNav:analytics.dashboards", "sideNavStatus": "visible", @@ -368,7 +368,7 @@ describe('initNavigation()', () => { }, "href": "/app/visualize", "id": "visualize", - "isElasticInternalLink": false, + "isExternalLink": false, "onClick": undefined, "path": "rootNav:analytics.visualize", "sideNavStatus": "visible", @@ -379,7 +379,7 @@ describe('initNavigation()', () => { "href": undefined, "icon": "stats", "id": "rootNav:analytics", - "isElasticInternalLink": false, + "isExternalLink": false, "onClick": undefined, "path": "rootNav:analytics", "renderAs": "accordion", @@ -457,7 +457,7 @@ describe('initNavigation()', () => { deepLink: undefined, href: 'https://cloud.elastic.co/userAndRoles', id: 'node-0', - isElasticInternalLink: true, + isExternalLink: true, path: 'group1.node-0', sideNavStatus: 'visible', title: 'Users and roles', @@ -468,7 +468,7 @@ describe('initNavigation()', () => { deepLink: undefined, href: 'https://cloud.elastic.co/performance', id: 'node-1', - isElasticInternalLink: true, + isExternalLink: true, path: 'group1.node-1', sideNavStatus: 'visible', title: 'Performance', @@ -479,7 +479,7 @@ describe('initNavigation()', () => { deepLink: undefined, href: 'https://cloud.elastic.co/billing', id: 'node-2', - isElasticInternalLink: true, + isExternalLink: true, path: 'group1.node-2', sideNavStatus: 'visible', title: 'Billing and subscription', @@ -490,7 +490,7 @@ describe('initNavigation()', () => { deepLink: undefined, href: 'https://cloud.elastic.co/deployment', id: 'node-3', - isElasticInternalLink: true, + isExternalLink: true, path: 'group1.node-3', sideNavStatus: 'visible', title: 'Project', @@ -804,7 +804,7 @@ describe('getActiveNodes$()', () => { id: 'root', title: 'Root', path: 'root', - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', type: 'navGroup', }, @@ -812,7 +812,7 @@ describe('getActiveNodes$()', () => { id: 'item1', title: 'ITEM1', path: 'root.item1', - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', href: '/app/item1', deepLink: { @@ -861,7 +861,7 @@ describe('getActiveNodes$()', () => { id: 'root', title: 'Root', path: 'root', - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', type: 'navGroup', }, @@ -869,7 +869,7 @@ describe('getActiveNodes$()', () => { id: 'item1', title: 'ITEM1', path: 'root.item1', - isElasticInternalLink: false, + isExternalLink: false, sideNavStatus: 'visible', href: '/app/item1', deepLink: { diff --git a/src/core/packages/chrome/browser-internal/src/project_navigation/utils.ts b/src/core/packages/chrome/browser-internal/src/project_navigation/utils.ts index bdf3929c464dc..be7292cbc2585 100644 --- a/src/core/packages/chrome/browser-internal/src/project_navigation/utils.ts +++ b/src/core/packages/chrome/browser-internal/src/project_navigation/utils.ts @@ -321,8 +321,8 @@ const initNavNode = < const id = getNavigationNodeId(node, () => `node-${index}`) as Id; const title = getTitleForNode(node, { deepLink, cloudLinks }); - const isElasticInternalLink = cloudLink != null; - const href = isElasticInternalLink ? cloudLinks[cloudLink]?.href : node.href; + const isExternalLink = cloudLink != null; + const href = isExternalLink ? cloudLinks[cloudLink]?.href : node.href; const path = parentNodePath ? `${parentNodePath}.${id}` : id; if (href && !isAbsoluteLink(href) && !onClick) { @@ -337,7 +337,7 @@ const initNavNode = < path, title, deepLink, - isElasticInternalLink, + isExternalLink, sideNavStatus, }; diff --git a/src/core/packages/chrome/browser/src/project_navigation.ts b/src/core/packages/chrome/browser/src/project_navigation.ts index f4a5af26c4176..858cb2d3d84f1 100644 --- a/src/core/packages/chrome/browser/src/project_navigation.ts +++ b/src/core/packages/chrome/browser/src/project_navigation.ts @@ -245,7 +245,7 @@ export interface ChromeProjectNavigationNode extends NodeDefinitionBase { /** * Flag to indicate if the node is an "external" cloud link */ - isElasticInternalLink?: boolean; + isExternalLink?: boolean; } export type PanelSelectedNode = Pick< diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx index 1ae5fdbd8d8c5..723e8cade8835 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx @@ -117,7 +117,6 @@ const serializeNavNode = ( const serialized: ChromeProjectNavigationNode = { ...navNode, }; - serialized.renderAs = getRenderAs(serialized, { isSideNavCollapsed }); serialized.spaceBefore = getSpaceBefore(serialized, { isSideNavCollapsed, @@ -257,7 +256,7 @@ const getEuiProps = ( // if it is the highest match in the URL, not if one of its children is also active. const onlyIfHighestMatch = isAccordion && !isCollapsible; const isActive = isActiveFromUrl(navNode.path, activeNodes, onlyIfHighestMatch); - const isExternal = Boolean(href) && !navNode.isElasticInternalLink && isAbsoluteLink(href!); + const isExternal = Boolean(href) && navNode.isExternalLink && isAbsoluteLink(href!); const isAccordionExpanded = !getIsCollapsed(path); let isSelected = isActive; @@ -386,7 +385,7 @@ function nodeToEuiCollapsibleNavProps( _navNode, deps ); - const { id, path, href, renderAs, isCollapsible, spaceBefore } = navNode; + const { id, path, href, renderAs, isCollapsible, spaceBefore, isExternalLink } = navNode; if (navNode.renderItem) { // Leave the rendering to the consumer @@ -427,7 +426,9 @@ function nodeToEuiCollapsibleNavProps( // Render as an accordion or a link (handled by EUI) depending if // "items" is undefined or not. If it is undefined --> a link, otherwise an // accordion is rendered. - ...(subItems ? { items: subItems, isCollapsible } : { href, linkProps }), + ...(subItems + ? { items: subItems, isCollapsible } + : { href, ...linkProps, external: isExternalLink }), }, ]; diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_nav_item.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_nav_item.tsx index ddbdc4ac45252..b0788ed437c91 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_nav_item.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_nav_item.tsx @@ -24,7 +24,8 @@ interface Props { export const PanelNavItem: FC = ({ item }) => { const { navigateToUrl } = useServices(); const { close: closePanel } = usePanel(); - const { id, icon, deepLink, openInNewTab, renderItem } = item; + const { id, icon, deepLink, openInNewTab, isExternalLink, renderItem } = item; + const href = deepLink?.url ?? item.href; const { euiTheme } = useEuiTheme(); @@ -52,6 +53,9 @@ export const PanelNavItem: FC = ({ item }) => { &.sideNavPanelLink:hover { background-color: ${transparentize(euiTheme.colors.lightShade, 0.5)}; } + & svg[class*='EuiExternalLinkIcon'] { + margin-left: auto; + } ` )} size="s" @@ -59,6 +63,7 @@ export const PanelNavItem: FC = ({ item }) => { href={href} iconType={icon} onClick={onClick} + external={isExternalLink} target={openInNewTab ? '_blank' : undefined} /> ); diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx index e4885bce172da..5104182ede9f1 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx @@ -111,6 +111,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = { title: 'Item 01', href: '/app/kibana', icon: 'iInCircle', + isExternalLink: true, }, { id: 'item02', @@ -203,6 +204,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = { title: 'Item 17', href: '/app/kibana', icon: 'iInCircle', + isExternalLink: true, }, { id: 'sub2', @@ -403,6 +405,7 @@ const generalLayoutNavTree: NavigationTreeDefinitionUI = { title: 'Item-Beta', href: '/app/kibana', withBadge: true, + isExternalLink: true, }, { id: 'item-labs',