From 23d765b01b99dc34bb97cf84cb8575de183a80ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 11 Jul 2023 16:46:51 +0100 Subject: [PATCH 01/25] Allow cloudLink to be passed in nav node --- .../core/chrome/core-chrome-browser/index.ts | 1 + .../chrome/core-chrome-browser/src/index.ts | 1 + .../src/project_navigation.ts | 5 + .../chrome/navigation/src/cloud_links.tsx | 58 ++++++++++++ .../chrome/navigation/src/services.tsx | 6 +- .../src/ui/components/navigation_group.tsx | 6 +- .../src/ui/components/navigation_item.tsx | 4 +- .../src/ui/hooks/use_init_navnode.ts | 94 +++++++++++++++++-- .../chrome/navigation/types/index.ts | 4 + 9 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 packages/shared-ux/chrome/navigation/src/cloud_links.tsx diff --git a/packages/core/chrome/core-chrome-browser/index.ts b/packages/core/chrome/core-chrome-browser/index.ts index d42d859cc6f3b..2e587cbcda497 100644 --- a/packages/core/chrome/core-chrome-browser/index.ts +++ b/packages/core/chrome/core-chrome-browser/index.ts @@ -34,6 +34,7 @@ export type { ChromeUserBanner, ChromeProjectNavigation, ChromeProjectNavigationNode, + CloudLinkId, SideNavCompProps, SideNavComponent, ChromeProjectBreadcrumb, diff --git a/packages/core/chrome/core-chrome-browser/src/index.ts b/packages/core/chrome/core-chrome-browser/src/index.ts index 7a414fc87164e..b931f05ecd327 100644 --- a/packages/core/chrome/core-chrome-browser/src/index.ts +++ b/packages/core/chrome/core-chrome-browser/src/index.ts @@ -33,6 +33,7 @@ export type { ChromeProjectNavigationNode, AppDeepLinkId, AppId, + CloudLinkId, SideNavCompProps, SideNavComponent, ChromeSetProjectBreadcrumbsParams, diff --git a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts b/packages/core/chrome/core-chrome-browser/src/project_navigation.ts index 5be1d505f2ff9..f41ceb54066bf 100644 --- a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts +++ b/packages/core/chrome/core-chrome-browser/src/project_navigation.ts @@ -44,6 +44,9 @@ export type AppDeepLinkId = | SearchLink | ObservabilityLink; +/** @public */ +export type CloudLinkId = 'userAndRoles' | 'performance' | 'billingAndSub'; + /** @public */ export interface ChromeProjectNavigationNode { /** Optional id, if not passed a "link" must be provided. */ @@ -123,6 +126,8 @@ export interface NodeDefinition< title?: string; /** App id or deeplink id */ 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 children of the navigation node */ diff --git a/packages/shared-ux/chrome/navigation/src/cloud_links.tsx b/packages/shared-ux/chrome/navigation/src/cloud_links.tsx new file mode 100644 index 0000000000000..a0c2377fef7fd --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/cloud_links.tsx @@ -0,0 +1,58 @@ +/* + * 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'; +import type { CloudLinkId } from '@kbn/core-chrome-browser'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; + +export type CloudLinks = { + [id in CloudLinkId]?: { + title: string; + href: string; + }; +}; + +export const getCloudLinks = (cloud: CloudStart): CloudLinks => { + const { billingUrl, performanceUrl, usersAndRolesUrl } = cloud; + + const links: CloudLinks = {}; + + if (usersAndRolesUrl) { + links.billingAndSub = { + title: i18n.translate( + 'sharedUXPackages.chrome.sideNavigation.cloudLinks.usersAndRolesLinkText', + { + defaultMessage: 'Users and roles', + } + ), + href: usersAndRolesUrl, + }; + } + + if (performanceUrl) { + links.billingAndSub = { + title: i18n.translate( + 'sharedUXPackages.chrome.sideNavigation.cloudLinks.performanceLinkText', + { + defaultMessage: 'Performance', + } + ), + href: performanceUrl, + }; + } + + if (billingUrl) { + links.billingAndSub = { + title: i18n.translate('sharedUXPackages.chrome.sideNavigation.cloudLinks.billingLinkText', { + defaultMessage: 'Billing and subscription', + }), + href: billingUrl, + }; + } + + return links; +}; diff --git a/packages/shared-ux/chrome/navigation/src/services.tsx b/packages/shared-ux/chrome/navigation/src/services.tsx index f59365b21ead3..079ae21b6d39d 100644 --- a/packages/shared-ux/chrome/navigation/src/services.tsx +++ b/packages/shared-ux/chrome/navigation/src/services.tsx @@ -8,6 +8,7 @@ import React, { FC, useContext } from 'react'; import { NavigationKibanaDependencies, NavigationServices } from '../types'; +import { CloudLinks, getCloudLinks } from './cloud_links'; const Context = React.createContext(null); @@ -25,11 +26,13 @@ export const NavigationKibanaProvider: FC = ({ children, ...dependencies }) => { - const { core, serverless } = dependencies; + const { core, serverless, cloud } = dependencies; const { chrome, http } = core; const { basePath } = http; const { navigateToUrl } = core.application; + const cloudLinks: CloudLinks = cloud ? getCloudLinks(cloud) : {}; + const value: NavigationServices = { basePath, recentlyAccessed$: chrome.recentlyAccessed.get$(), @@ -38,6 +41,7 @@ export const NavigationKibanaProvider: FC = ({ navIsOpen: true, onProjectNavigationChange: serverless.setNavigation, activeNodes$: serverless.getActiveNavigationNodes$(), + cloudLinks, }; return {children}; diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_group.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_group.tsx index eda309b3635b2..76180b799991a 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_group.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_group.tsx @@ -7,8 +7,9 @@ */ import React, { createContext, useCallback, useMemo, useContext } from 'react'; - import type { AppDeepLinkId } from '@kbn/core-chrome-browser'; + +import { useNavigation as useNavigationServices } from '../../services'; import { useInitNavNode } from '../hooks'; import type { NodeProps, RegisterFunction } from '../types'; import { NavigationSectionUI } from './navigation_section_ui'; @@ -45,6 +46,7 @@ function NavigationGroupInternalComp< Id extends string = string, ChildrenId extends string = Id >(props: Props) { + const { cloudLinks } = useNavigationServices(); const navigationContext = useNavigation(); const { children, node } = useMemo(() => { @@ -58,7 +60,7 @@ function NavigationGroupInternalComp< }; }, [props]); - const { navNode, registerChildNode, path, childrenNodes } = useInitNavNode(node); + const { navNode, registerChildNode, path, childrenNodes } = useInitNavNode(node, { cloudLinks }); const unstyled = props.unstyled ?? navigationContext.unstyled; diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_item.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_item.tsx index 4333db7c9844c..ca9844db7b470 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_item.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_item.tsx @@ -9,6 +9,7 @@ import React, { Fragment, ReactElement, ReactNode, useEffect, useMemo } from 'react'; import type { AppDeepLinkId } from '@kbn/core-chrome-browser'; +import { useNavigation as useNavigationServices } from '../../services'; import type { ChromeProjectNavigationNodeEnhanced, NodeProps } from '../types'; import { useInitNavNode } from '../hooks'; import { useNavigation } from './navigation'; @@ -31,6 +32,7 @@ function NavigationItemComp< Id extends string = string, ChildrenId extends string = Id >(props: Props) { + const { cloudLinks } = useNavigationServices(); const navigationContext = useNavigation(); const navNodeRef = React.useRef(null); @@ -51,7 +53,7 @@ function NavigationItemComp< typeof children === 'function' ? () => children(navNodeRef.current) : () => children; } - const { navNode } = useInitNavNode({ ...node, children, renderItem }); + const { navNode } = useInitNavNode({ ...node, children, renderItem }, { cloudLinks }); useEffect(() => { navNodeRef.current = navNode; diff --git a/packages/shared-ux/chrome/navigation/src/ui/hooks/use_init_navnode.ts b/packages/shared-ux/chrome/navigation/src/ui/hooks/use_init_navnode.ts index 8f805c4b5d8ed..f569115250e11 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/hooks/use_init_navnode.ts +++ b/packages/shared-ux/chrome/navigation/src/ui/hooks/use_init_navnode.ts @@ -10,9 +10,11 @@ import { AppDeepLinkId, ChromeNavLink, ChromeProjectNavigationNode, + CloudLinkId, } from '@kbn/core-chrome-browser'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; +import { CloudLinks } from '../../cloud_links'; import { useNavigation as useNavigationServices } from '../../services'; import { isAbsoluteLink } from '../../utils'; @@ -40,12 +42,40 @@ function getIdFromNavigationNode< return id; } -function isNodeVisible({ link, deepLink }: { link?: string; deepLink?: ChromeNavLink }) { +/** + * We don't have currently a way to know if a user has access to a Cloud section. + * TODO: This function will have to be revisited once we have an API from Cloud to know the user + * permissions. + */ +function hasUserAccessToCloudLink(): boolean { + return true; +} + +function isNodeVisible( + { + link, + deepLink, + cloudLink, + }: { + link?: string; + deepLink?: ChromeNavLink; + cloudLink?: CloudLinkId; + }, + { cloudLinks }: { cloudLinks: CloudLinks } +) { if (link && !deepLink) { // If a link is provided, but no deepLink is found, don't render anything return false; } + if (cloudLink) { + if (!cloudLinks[cloudLink]) { + // Invalid cloudLinkId or link url has not been set in kibana.yml + return false; + } + return hasUserAccessToCloudLink(); + } + if (deepLink) { return !deepLink.hidden; } @@ -53,6 +83,47 @@ function isNodeVisible({ link, deepLink }: { link?: string; deepLink?: ChromeNav return true; } +function getTitleForNode< + LinkId extends AppDeepLinkId = AppDeepLinkId, + Id extends string = string, + ChildrenId extends string = Id +>( + navNode: NodePropsEnhanced, + { deepLink, cloudLinks }: { deepLink?: ChromeNavLink; cloudLinks: CloudLinks } +): string { + const { children } = navNode; + if (navNode.title) { + return navNode.title; + } + + if (typeof children === 'string') { + return children; + } + + if (deepLink?.title) { + return deepLink.title; + } + + if (navNode.cloudLink) { + return cloudLinks[navNode.cloudLink]?.title ?? ''; + } + + return ''; +} + +function validateNodeProps< + LinkId extends AppDeepLinkId = AppDeepLinkId, + Id extends string = string, + ChildrenId extends string = Id +>({ link, href, cloudLink }: NodePropsEnhanced) { + if (link && cloudLink) { + throw new Error(`Only one of "link" or "cloudLink" can be provided.`); + } + if (href && cloudLink) { + throw new Error(`Only one of "href" or "cloudLink" can be provided.`); + } +} + function createInternalNavNode< LinkId extends AppDeepLinkId = AppDeepLinkId, Id extends string = string, @@ -62,14 +133,16 @@ function createInternalNavNode< _navNode: NodePropsEnhanced, deepLinks: Readonly, path: string[] | null, - isActive: boolean + isActive: boolean, + { cloudLinks }: { cloudLinks: CloudLinks } ): ChromeProjectNavigationNodeEnhanced | null { - const { children, link, href, ...navNode } = _navNode; - const deepLink = deepLinks.find((dl) => dl.id === link); - const isVisible = isNodeVisible({ link, deepLink }); + validateNodeProps(_navNode); - const titleFromDeepLinkOrChildren = typeof children === 'string' ? children : deepLink?.title; - const title = navNode.title ?? titleFromDeepLinkOrChildren; + const { children, link, cloudLink, ...navNode } = _navNode; + const deepLink = deepLinks.find((dl) => dl.id === link); + const isVisible = isNodeVisible({ link, deepLink, cloudLink }, { cloudLinks }); + const title = getTitleForNode(_navNode, { deepLink, cloudLinks }); + const href = cloudLink ? cloudLinks[cloudLink]?.href : _navNode.href; if (href && !isAbsoluteLink(href)) { throw new Error(`href must be an absolute URL. Node id [${id}].`); @@ -104,7 +177,8 @@ export const useInitNavNode = < Id extends string = string, ChildrenId extends string = Id >( - node: NodePropsEnhanced + node: NodePropsEnhanced, + { cloudLinks }: { cloudLinks: CloudLinks } ) => { const { isActive: isActiveControlled } = node; @@ -150,8 +224,8 @@ export const useInitNavNode = < const id = getIdFromNavigationNode(node); const internalNavNode = useMemo( - () => createInternalNavNode(id, node, deepLinks, nodePath, isActive), - [node, id, deepLinks, nodePath, isActive] + () => createInternalNavNode(id, node, deepLinks, nodePath, isActive, { cloudLinks }), + [node, id, deepLinks, nodePath, isActive, cloudLinks] ); // Register the node on the parent whenever its properties change or whenever diff --git a/packages/shared-ux/chrome/navigation/types/index.ts b/packages/shared-ux/chrome/navigation/types/index.ts index dc6014dcdd28b..00d8e69edf0e3 100644 --- a/packages/shared-ux/chrome/navigation/types/index.ts +++ b/packages/shared-ux/chrome/navigation/types/index.ts @@ -7,6 +7,7 @@ */ import type { Observable } from 'rxjs'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; import type { ChromeNavLink, @@ -14,6 +15,7 @@ import type { ChromeProjectNavigationNode, } from '@kbn/core-chrome-browser'; import type { BasePathService, NavigateToUrlFn, RecentItem } from './internal'; +import type { CloudLinks } from '../src/cloud_links'; /** * A list of services that are consumed by this component. @@ -27,6 +29,7 @@ export interface NavigationServices { navigateToUrl: NavigateToUrlFn; onProjectNavigationChange: (chromeProjectNavigation: ChromeProjectNavigation) => void; activeNodes$: Observable; + cloudLinks: CloudLinks; } /** @@ -55,4 +58,5 @@ export interface NavigationKibanaDependencies { ) => void; getActiveNavigationNodes$: () => Observable; }; + cloud: CloudStart; } From e6d2b302c664ad89db9140639cfc551759861714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 11 Jul 2023 16:47:33 +0100 Subject: [PATCH 02/25] Add cloud config for new URLs --- x-pack/plugins/cloud/public/types.ts | 8 ++++++++ x-pack/plugins/cloud/server/config.ts | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/x-pack/plugins/cloud/public/types.ts b/x-pack/plugins/cloud/public/types.ts index 0224e0620c983..df4512f185ea1 100644 --- a/x-pack/plugins/cloud/public/types.ts +++ b/x-pack/plugins/cloud/public/types.ts @@ -36,6 +36,14 @@ export interface CloudStart { * The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud. */ organizationUrl?: string; + /** + * The full URL to the performance page on Elastic Cloud. Undefined if not running on Cloud. + */ + performanceUrl?: string; + /** + * The full URL to the users and roles page on Elastic Cloud. Undefined if not running on Cloud. + */ + usersAndRolesUrl?: string; /** * The full URL to the elasticsearch cluster. */ diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index 66cd1f3a47411..c13c0b255e7f2 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -25,6 +25,8 @@ const configSchema = schema.object({ deployment_url: schema.maybe(schema.string()), id: schema.maybe(schema.string()), billing_url: schema.maybe(schema.string()), + performance_url: schema.maybe(schema.string()), + users_and_roles_url: schema.maybe(schema.string()), organization_url: schema.maybe(schema.string()), profile_url: schema.maybe(schema.string()), trial_end_date: schema.maybe(schema.string()), @@ -45,6 +47,8 @@ export const config: PluginConfigDescriptor = { deployment_url: true, id: true, billing_url: true, + users_and_roles_url: true, + performance_url: true, organization_url: true, profile_url: true, trial_end_date: true, From 21cf3ca60aaa1b018c6497cf8c34d558b145608f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 11 Jul 2023 16:48:23 +0100 Subject: [PATCH 03/25] Update observability nav to include links --- .../serverless_observability/kibana.jsonc | 2 +- .../components/side_navigation/index.tsx | 19 ++++++++++++++++--- .../serverless_observability/public/plugin.ts | 4 ++-- .../public/services.tsx | 11 ++++++++--- .../serverless_observability/public/types.ts | 2 ++ 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/serverless_observability/kibana.jsonc b/x-pack/plugins/serverless_observability/kibana.jsonc index 0f85a1033c2ea..d7a8de7298c41 100644 --- a/x-pack/plugins/serverless_observability/kibana.jsonc +++ b/x-pack/plugins/serverless_observability/kibana.jsonc @@ -8,7 +8,7 @@ "server": true, "browser": true, "configPath": ["xpack", "serverless", "observability"], - "requiredPlugins": ["serverless", "observabilityShared", "kibanaReact", "management", "ml"], + "requiredPlugins": ["serverless", "observabilityShared", "kibanaReact", "management", "ml", "cloud"], "optionalPlugins": [], "requiredBundles": [] } diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx index 70ba2b98d5707..ab861acf826ad 100644 --- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { CoreStart } from '@kbn/core/public'; +import type { CoreStart } from '@kbn/core/public'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; import { ServerlessPluginStart } from '@kbn/serverless/public'; import { DefaultNavigation, @@ -148,6 +149,15 @@ const navigationTree: NavigationTreeDefinition = { { link: 'fleet', }, + { + cloudLink: 'userAndRoles', + }, + { + cloudLink: 'performance', + }, + { + cloudLink: 'billingAndSub', + }, ], }, ], @@ -156,10 +166,13 @@ const navigationTree: NavigationTreeDefinition = { }; export const getObservabilitySideNavComponent = - (core: CoreStart, { serverless }: { serverless: ServerlessPluginStart }) => + ( + core: CoreStart, + { serverless, cloud }: { serverless: ServerlessPluginStart; cloud: CloudStart } + ) => () => { return ( - + ); diff --git a/x-pack/plugins/serverless_observability/public/plugin.ts b/x-pack/plugins/serverless_observability/public/plugin.ts index 4a0841c98ad90..76d4d4db25946 100644 --- a/x-pack/plugins/serverless_observability/public/plugin.ts +++ b/x-pack/plugins/serverless_observability/public/plugin.ts @@ -29,10 +29,10 @@ export class ServerlessObservabilityPlugin core: CoreStart, setupDeps: ServerlessObservabilityPluginStartDependencies ): ServerlessObservabilityPluginStart { - const { observabilityShared, serverless, management } = setupDeps; + const { observabilityShared, serverless, management, cloud } = setupDeps; observabilityShared.setIsSidebarEnabled(false); serverless.setProjectHome('/app/observability/landing'); - serverless.setSideNavComponent(getObservabilitySideNavComponent(core, { serverless })); + serverless.setSideNavComponent(getObservabilitySideNavComponent(core, { serverless, cloud })); management.setupCardsNavigation({ enabled: true, hideLinksTo: [appIds.RULES], diff --git a/x-pack/plugins/serverless_observability/public/services.tsx b/x-pack/plugins/serverless_observability/public/services.tsx index a70db0885020d..186267ffa94bc 100644 --- a/x-pack/plugins/serverless_observability/public/services.tsx +++ b/x-pack/plugins/serverless_observability/public/services.tsx @@ -6,16 +6,21 @@ */ import { CoreStart } from '@kbn/core/public'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; import React from 'react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { ServerlessObservabilityPluginStartDependencies } from './types'; -type Services = CoreStart & ServerlessObservabilityPluginStartDependencies; +type Services = CoreStart & + ServerlessObservabilityPluginStartDependencies & { + cloud: CloudStart; + }; export const KibanaServicesProvider: React.FC<{ core: CoreStart; pluginsStart: ServerlessObservabilityPluginStartDependencies; -}> = ({ core, pluginsStart, children }) => { - const services: Services = { ...core, ...pluginsStart }; + cloud: CloudStart; +}> = ({ core, pluginsStart, cloud, children }) => { + const services: Services = { ...core, ...pluginsStart, cloud }; return {children}; }; diff --git a/x-pack/plugins/serverless_observability/public/types.ts b/x-pack/plugins/serverless_observability/public/types.ts index 299880b2cde47..335ce677242ab 100644 --- a/x-pack/plugins/serverless_observability/public/types.ts +++ b/x-pack/plugins/serverless_observability/public/types.ts @@ -11,6 +11,7 @@ import { ObservabilitySharedPluginStart, } from '@kbn/observability-shared-plugin/public'; import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ServerlessObservabilityPluginSetup {} @@ -28,4 +29,5 @@ export interface ServerlessObservabilityPluginStartDependencies { observabilityShared: ObservabilitySharedPluginStart; serverless: ServerlessPluginStart; management: ManagementStart; + cloud: CloudStart; } From e47a0024b74eebe0392568822b6012638e07b0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 11 Jul 2023 17:07:42 +0100 Subject: [PATCH 04/25] Add urls to cloud start contract --- .../chrome/navigation/src/cloud_links.tsx | 4 ++-- .../chrome/navigation/src/services.tsx | 4 ++-- .../ui/components/navigation_section_ui.tsx | 2 +- x-pack/plugins/cloud/public/plugin.tsx | 21 ++++++++++++++++++- .../components/side_navigation/index.tsx | 3 +++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/shared-ux/chrome/navigation/src/cloud_links.tsx b/packages/shared-ux/chrome/navigation/src/cloud_links.tsx index a0c2377fef7fd..4efd73b8e3a97 100644 --- a/packages/shared-ux/chrome/navigation/src/cloud_links.tsx +++ b/packages/shared-ux/chrome/navigation/src/cloud_links.tsx @@ -22,7 +22,7 @@ export const getCloudLinks = (cloud: CloudStart): CloudLinks => { const links: CloudLinks = {}; if (usersAndRolesUrl) { - links.billingAndSub = { + links.userAndRoles = { title: i18n.translate( 'sharedUXPackages.chrome.sideNavigation.cloudLinks.usersAndRolesLinkText', { @@ -34,7 +34,7 @@ export const getCloudLinks = (cloud: CloudStart): CloudLinks => { } if (performanceUrl) { - links.billingAndSub = { + links.performance = { title: i18n.translate( 'sharedUXPackages.chrome.sideNavigation.cloudLinks.performanceLinkText', { diff --git a/packages/shared-ux/chrome/navigation/src/services.tsx b/packages/shared-ux/chrome/navigation/src/services.tsx index 079ae21b6d39d..ebe7846b33378 100644 --- a/packages/shared-ux/chrome/navigation/src/services.tsx +++ b/packages/shared-ux/chrome/navigation/src/services.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useContext } from 'react'; +import React, { FC, useContext, useMemo } from 'react'; import { NavigationKibanaDependencies, NavigationServices } from '../types'; import { CloudLinks, getCloudLinks } from './cloud_links'; @@ -31,7 +31,7 @@ export const NavigationKibanaProvider: FC = ({ const { basePath } = http; const { navigateToUrl } = core.application; - const cloudLinks: CloudLinks = cloud ? getCloudLinks(cloud) : {}; + const cloudLinks: CloudLinks = useMemo(() => (cloud ? getCloudLinks(cloud) : {}), [cloud]); const value: NavigationServices = { basePath, diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx index 06be447ba2a65..9f8a585066b57 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx @@ -39,7 +39,7 @@ const navigationNodeToEuiItem = ( return () => (
- + {item.title}
diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index 11052f48de7a3..a12d090b0c9c4 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -24,6 +24,8 @@ export interface CloudConfigType { deployment_url?: string; billing_url?: string; organization_url?: string; + users_and_roles_url?: string; + performance_url?: string; trial_end_date?: string; is_elastic_staff_owned?: boolean; serverless?: { @@ -37,6 +39,8 @@ interface CloudUrls { billingUrl?: string; organizationUrl?: string; snapshotsUrl?: string; + performanceUrl?: string; + usersAndRolesUrl?: string; } export class CloudPlugin implements Plugin { @@ -110,7 +114,14 @@ export class CloudPlugin implements Plugin { ); }; - const { deploymentUrl, profileUrl, billingUrl, organizationUrl } = this.getCloudUrls(); + const { + deploymentUrl, + profileUrl, + billingUrl, + organizationUrl, + performanceUrl, + usersAndRolesUrl, + } = this.getCloudUrls(); let decodedId: DecodedCloudId | undefined; if (this.config.id) { @@ -131,6 +142,8 @@ export class CloudPlugin implements Plugin { serverless: { projectId: this.config.serverless?.project_id, }, + performanceUrl, + usersAndRolesUrl, }; } @@ -143,12 +156,16 @@ export class CloudPlugin implements Plugin { organization_url: organizationUrl, deployment_url: deploymentUrl, base_url: baseUrl, + performance_url: performanceUrl, + users_and_roles_url: usersAndRolesUrl, } = this.config; const fullCloudDeploymentUrl = getFullCloudUrl(baseUrl, deploymentUrl); const fullCloudProfileUrl = getFullCloudUrl(baseUrl, profileUrl); const fullCloudBillingUrl = getFullCloudUrl(baseUrl, billingUrl); const fullCloudOrganizationUrl = getFullCloudUrl(baseUrl, organizationUrl); + const fullCloudPerformanceUrl = getFullCloudUrl(baseUrl, performanceUrl); + const fullCloudUsersAndRolesUrl = getFullCloudUrl(baseUrl, usersAndRolesUrl); const fullCloudSnapshotsUrl = `${fullCloudDeploymentUrl}/${CLOUD_SNAPSHOTS_PATH}`; return { @@ -157,6 +174,8 @@ export class CloudPlugin implements Plugin { billingUrl: fullCloudBillingUrl, organizationUrl: fullCloudOrganizationUrl, snapshotsUrl: fullCloudSnapshotsUrl, + performanceUrl: fullCloudPerformanceUrl, + usersAndRolesUrl: fullCloudUsersAndRolesUrl, }; } } diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx index ab861acf826ad..f244f6703bc33 100644 --- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx @@ -150,12 +150,15 @@ const navigationTree: NavigationTreeDefinition = { link: 'fleet', }, { + id: 'cloudLinkUserAndRoles', cloudLink: 'userAndRoles', }, { + id: 'cloudLinkPerformance', cloudLink: 'performance', }, { + id: 'cloudLinkBilling', cloudLink: 'billingAndSub', }, ], From 9997866fe31a0942953ff31f954086f220b97a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 13 Jul 2023 12:56:24 +0100 Subject: [PATCH 05/25] Add missing i18n --- .../public/components/side_navigation/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx index f244f6703bc33..d69f547b9c205 100644 --- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx @@ -128,8 +128,10 @@ const navigationTree: NavigationTreeDefinition = { footer: [ { type: 'navGroup', - id: 'projest_settings_project_nav', - title: 'Project settings', + id: 'project_settings_project_nav', + title: i18n.translate('xpack.serverlessObservability.nav.projectSettings', { + defaultMessage: 'Project settings', + }), icon: 'gear', defaultIsCollapsed: true, breadcrumbStatus: 'hidden', From ecdbf6365ced521f13c6a08189c9d6524342d947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 13 Jul 2023 12:57:31 +0100 Subject: [PATCH 06/25] Add project settings group to serverless es --- .../serverless_search/public/layout/nav.tsx | 45 ++++++++++++++++++- .../serverless_search/public/plugin.ts | 4 +- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/serverless_search/public/layout/nav.tsx b/x-pack/plugins/serverless_search/public/layout/nav.tsx index afc8aba90cbc8..ae1a9a1739478 100644 --- a/x-pack/plugins/serverless_search/public/layout/nav.tsx +++ b/x-pack/plugins/serverless_search/public/layout/nav.tsx @@ -15,6 +15,7 @@ import { import React from 'react'; import { i18n } from '@kbn/i18n'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; +import type { CloudStart } from '@kbn/cloud-plugin/public'; const navigationTree: NavigationTreeDefinition = { body: [ @@ -105,13 +106,53 @@ const navigationTree: NavigationTreeDefinition = { ...getPresets('ml'), }, ], + footer: [ + { + type: 'navGroup', + id: 'project_settings_project_nav', + title: i18n.translate('xpack.serverlessSearch.nav.projectSettings', { + defaultMessage: 'Project settings', + }), + icon: 'gear', + defaultIsCollapsed: true, + breadcrumbStatus: 'hidden', + children: [ + { + id: 'settings', + children: [ + { + link: 'management', + title: i18n.translate('xpack.serverlessSearch.nav.mngt', { + defaultMessage: 'Management', + }), + }, + { + id: 'cloudLinkUserAndRoles', + cloudLink: 'userAndRoles', + }, + { + id: 'cloudLinkPerformance', + cloudLink: 'performance', + }, + { + id: 'cloudLinkBilling', + cloudLink: 'billingAndSub', + }, + ], + }, + ], + }, + ], }; export const createServerlessSearchSideNavComponent = - (core: CoreStart, { serverless }: { serverless: ServerlessPluginStart }) => + ( + core: CoreStart, + { serverless, cloud }: { serverless: ServerlessPluginStart; cloud: CloudStart } + ) => () => { return ( - + ); diff --git a/x-pack/plugins/serverless_search/public/plugin.ts b/x-pack/plugins/serverless_search/public/plugin.ts index 13d00a7ccdf5a..b4914b710bafa 100644 --- a/x-pack/plugins/serverless_search/public/plugin.ts +++ b/x-pack/plugins/serverless_search/public/plugin.ts @@ -69,10 +69,10 @@ export class ServerlessSearchPlugin public start( core: CoreStart, - { serverless, management }: ServerlessSearchPluginStartDependencies + { serverless, management, cloud }: ServerlessSearchPluginStartDependencies ): ServerlessSearchPluginStart { serverless.setProjectHome('/app/elasticsearch'); - serverless.setSideNavComponent(createComponent(core, { serverless })); + serverless.setSideNavComponent(createComponent(core, { serverless, cloud })); management.setupCardsNavigation({ enabled: true, hideLinksTo: [appIds.MAINTENANCE_WINDOWS], From 7c5c20f2f3121f2103d2ac076192b84352157ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 13 Jul 2023 15:23:52 +0100 Subject: [PATCH 07/25] Don't force collapsed state by default --- .../public/components/side_navigation/index.tsx | 1 - x-pack/plugins/serverless_search/public/layout/nav.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx index d69f547b9c205..bfa74682e6234 100644 --- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx @@ -133,7 +133,6 @@ const navigationTree: NavigationTreeDefinition = { defaultMessage: 'Project settings', }), icon: 'gear', - defaultIsCollapsed: true, breadcrumbStatus: 'hidden', children: [ { diff --git a/x-pack/plugins/serverless_search/public/layout/nav.tsx b/x-pack/plugins/serverless_search/public/layout/nav.tsx index ae1a9a1739478..e752ef5f187d0 100644 --- a/x-pack/plugins/serverless_search/public/layout/nav.tsx +++ b/x-pack/plugins/serverless_search/public/layout/nav.tsx @@ -114,7 +114,6 @@ const navigationTree: NavigationTreeDefinition = { defaultMessage: 'Project settings', }), icon: 'gear', - defaultIsCollapsed: true, breadcrumbStatus: 'hidden', children: [ { From 9e3208c7be36ae54f3113cd968813b917c82b49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 13 Jul 2023 15:25:21 +0100 Subject: [PATCH 08/25] Improve comment --- packages/shared-ux/chrome/navigation/src/ui/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared-ux/chrome/navigation/src/ui/types.ts b/packages/shared-ux/chrome/navigation/src/ui/types.ts index f4414ae2e003f..5175425b6ada4 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/types.ts +++ b/packages/shared-ux/chrome/navigation/src/ui/types.ts @@ -94,11 +94,11 @@ export interface GroupDefinition< /** * Flag to indicate if the group is initially collapsed or not. * + * `undefined`: (Recommended) the group will be opened if any of its children nodes matches the current URL. + * * `false`: the group will be opened event if none of its children nodes matches the current URL. * * `true`: the group will be collapsed event if any of its children nodes matches the current URL. - * - * `undefined`: the group will be opened if any of its children nodes matches the current URL. */ defaultIsCollapsed?: boolean; preset?: NavigationGroupPreset; From 47dc8f5e7ce8576ebad1d197a82cfda789a0b724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 13 Jul 2023 15:45:49 +0100 Subject: [PATCH 09/25] Update GetApi type to retrieve active state --- .../project_navigation_service.ts | 3 ++- .../src/project_navigation/utils.ts | 5 +++-- .../core-chrome-browser/src/project_navigation.ts | 13 +++++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts index 3d484b6d2fb0d..cbbca4d2c5828 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts @@ -140,7 +140,8 @@ export class ProjectNavigationService { const activeNodes = findActiveNodes( currentPathname, this.projectNavigationNavTreeFlattened, - location + location, + this.http?.basePath.prepend ); // Each time we call findActiveNodes() we create a new array of activeNodes. As this array is used diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts index e8a3750ade156..7d7b803291e0d 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts @@ -101,7 +101,8 @@ function extractParentPaths(key: string) { export const findActiveNodes = ( currentPathname: string, navTree: Record, - location?: Location + location?: Location, + prepend: (path: string) => string = (path) => path ): ChromeProjectNavigationNode[][] => { const activeNodes: ChromeProjectNavigationNode[][] = []; const matches: string[][] = []; @@ -113,7 +114,7 @@ export const findActiveNodes = ( Object.entries(navTree).forEach(([key, node]) => { if (node.getIsActive && location) { - const isActive = node.getIsActive(location); + const isActive = node.getIsActive({ pathNameSerialized: currentPathname, location, prepend }); if (isActive) { const keysWithParents = extractParentPaths(key); activeNodes.push(keysWithParents.map(activeNodeFromKey)); diff --git a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts b/packages/core/chrome/core-chrome-browser/src/project_navigation.ts index f41ceb54066bf..38317cfc1e0f2 100644 --- a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts +++ b/packages/core/chrome/core-chrome-browser/src/project_navigation.ts @@ -47,6 +47,15 @@ export type AppDeepLinkId = /** @public */ export type CloudLinkId = 'userAndRoles' | 'performance' | 'billingAndSub'; +export type GetIsActiveFn = (params: { + /** The current path name including the basePath + hash value but **without** any query params */ + pathNameSerialized: string; + /** The history Location */ + location: Location; + /** Utiliy function to prepend a path with the basePath */ + prepend: (path: string) => string; +}) => boolean; + /** @public */ export interface ChromeProjectNavigationNode { /** Optional id, if not passed a "link" must be provided. */ @@ -72,7 +81,7 @@ export interface ChromeProjectNavigationNode { /** * Optional function to get the active state. This function is called whenever the location changes. */ - getIsActive?: (location: Location) => boolean; + getIsActive?: GetIsActiveFn; /** * Optional flag to indicate if the breadcrumb should be hidden when this node is active. @@ -139,7 +148,7 @@ export interface NodeDefinition< /** * Optional function to get the active state. This function is called whenever the location changes. */ - getIsActive?: (location: Location) => boolean; + getIsActive?: GetIsActiveFn; /** * Optional flag to indicate if the breadcrumb should be hidden when this node is active. From 80c564d686b053a0fb51d132f85ce2cfcde0947a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 13 Jul 2023 15:46:11 +0100 Subject: [PATCH 10/25] Update navs to set active state in details views --- .../public/components/side_navigation/index.tsx | 15 +++++++++++++-- .../serverless_search/public/layout/nav.tsx | 10 ++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx index bfa74682e6234..f2b464c77ab73 100644 --- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx @@ -38,6 +38,9 @@ const navigationTree: NavigationTreeDefinition = { defaultMessage: 'Dashboards', }), link: 'dashboards', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/dashboards')); + }, }, { link: 'observability-overview:alerts', @@ -76,13 +79,14 @@ const navigationTree: NavigationTreeDefinition = { }, ], }, - { id: 'applications', children: [ { id: 'apm', - title: 'Applications', + title: i18n.translate('xpack.serverlessObservability.nav.applications', { + defaultMessage: 'Applications', + }), children: [ { link: 'apm:services', @@ -108,6 +112,13 @@ const navigationTree: NavigationTreeDefinition = { defaultMessage: 'Visualizations', }), link: 'visualize', + getIsActive: ({ pathNameSerialized, prepend }) => { + return ( + pathNameSerialized.startsWith(prepend('/app/visualize')) || + pathNameSerialized.startsWith(prepend('/app/lens')) || + pathNameSerialized.startsWith(prepend('/app/maps')) + ); + }, }, ], }, diff --git a/x-pack/plugins/serverless_search/public/layout/nav.tsx b/x-pack/plugins/serverless_search/public/layout/nav.tsx index e752ef5f187d0..067cee8e5a554 100644 --- a/x-pack/plugins/serverless_search/public/layout/nav.tsx +++ b/x-pack/plugins/serverless_search/public/layout/nav.tsx @@ -53,9 +53,19 @@ const navigationTree: NavigationTreeDefinition = { }, { link: 'dashboards', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.startsWith(prepend('/app/dashboards')); + }, }, { link: 'visualize', + getIsActive: ({ pathNameSerialized, prepend }) => { + return ( + pathNameSerialized.startsWith(prepend('/app/visualize')) || + pathNameSerialized.startsWith(prepend('/app/lens')) || + pathNameSerialized.startsWith(prepend('/app/maps')) + ); + }, }, ], }, From 4f9097c28a40ba4a1fc38c8fbff9a9de981b2d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 14 Jul 2023 12:31:33 +0100 Subject: [PATCH 11/25] Fix TS issue --- .../src/project_navigation/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.test.ts index 940a350f47c06..3f39a08385880 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.test.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.test.ts @@ -345,7 +345,7 @@ describe('findActiveNodes', () => { id: 'item1', title: 'Item 1', path: ['root', 'item1'], - getIsActive: (loc) => loc.pathname.startsWith('/foo'), // Should match + getIsActive: ({ location }) => location.pathname.startsWith('/foo'), // Should match }, '[0][2]': { id: 'item2', From fe0bda0ae0c9e3ca7306b381af283ac580caf0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 14 Jul 2023 13:06:37 +0100 Subject: [PATCH 12/25] Custom render group with no children with a link --- .../src/ui/components/group_as_link.tsx | 61 +++++++++++++++++++ .../ui/components/navigation_section_ui.tsx | 27 +++++++- .../navigation/src/ui/default_navigation.tsx | 11 ++-- 3 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 packages/shared-ux/chrome/navigation/src/ui/components/group_as_link.tsx diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/group_as_link.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/group_as_link.tsx new file mode 100644 index 0000000000000..092d243722cd4 --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/components/group_as_link.tsx @@ -0,0 +1,61 @@ +/* + * 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 from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; + +import type { NavigateToUrlFn } from '../../../types/internal'; + +interface Props { + title: string; + href: string; + navigateToUrl: NavigateToUrlFn; + iconType?: string; +} + +export const GroupAsLink = ({ title, href, navigateToUrl, iconType }: Props) => { + const groupID = useGeneratedHtmlId(); + const titleID = `${groupID}__title`; + const TitleElement = 'h3'; + + return ( + + {iconType && ( + + + + )} + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + e.preventDefault(); + e.stopPropagation(); + navigateToUrl(href); + }} + href={href} + > + + + {title} + + + + + + ); +}; diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx index 9f8a585066b57..c9cae230b2361 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx @@ -20,6 +20,7 @@ import { navigationStyles as styles } from '../../styles'; import { useNavigation as useServices } from '../../services'; import { ChromeProjectNavigationNodeEnhanced } from '../types'; import { isAbsoluteLink } from '../../utils'; +import { GroupAsLink } from './group_as_link'; type RenderItem = EuiSideNavItemType['renderItem']; @@ -108,6 +109,10 @@ export const NavigationSectionUI: FC = ({ navNode, items = [] }) => { }); const groupHasLink = Boolean(navNode.deepLink) || Boolean(navNode.href); + const groupHasChildren = filteredItems.some(itemHasLinkOrChildren); + // Group with a link and no children will be rendered as a link and not an EUI accordion + const groupIsLink = groupHasLink && !groupHasChildren; + const groupHref = navNode.deepLink?.url ?? navNode.href!; useEffect(() => { if (doCollapseFromActiveState) { @@ -115,17 +120,32 @@ export const NavigationSectionUI: FC = ({ navNode, items = [] }) => { } }, [isActive, doCollapseFromActiveState]); - if (!groupHasLink && !filteredItems.some(itemHasLinkOrChildren)) { + if (!groupHasLink && !groupHasChildren) { return null; } + const propsForGroupAsLink = groupIsLink + ? { + buttonElement: 'div' as const, + buttonContent: ( + + ), + arrowProps: { style: { display: 'none' } }, + } + : {}; + return ( { setIsCollapsed(!isOpen); @@ -133,6 +153,7 @@ export const NavigationSectionUI: FC = ({ navNode, items = [] }) => { }} forceState={isCollapsed ? 'closed' : 'open'} data-test-subj={`nav-bucket-${id}`} + {...propsForGroupAsLink} > - {renderItems(copy.children, [...path, id])} + return item.children || (item as GroupDefinition).type === 'navGroup' ? ( + + {renderItems(item.children, [...path, id])} ) : ( - + ); }); }, From e817610701aa94bf6bd6f6f0b4d15cba36cf5370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 14 Jul 2023 14:37:23 +0100 Subject: [PATCH 13/25] Update default navigation to include Developer tools link --- .../chrome/navigation/mocks/src/navlinks.ts | 1 + .../chrome/navigation/mocks/src/storybook.ts | 14 +++++++ .../ui/components/navigation_section_ui.tsx | 3 ++ .../navigation/src/ui/default_navigation.tsx | 40 ++++++++++++++++++- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/shared-ux/chrome/navigation/mocks/src/navlinks.ts b/packages/shared-ux/chrome/navigation/mocks/src/navlinks.ts index a002f072770a8..2a243080b9e46 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/navlinks.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/navlinks.ts @@ -28,6 +28,7 @@ const allNavLinks: AppDeepLinkId[] = [ 'discover', 'fleet', 'integrations', + 'management', 'management:api_keys', 'management:cases', 'management:cross_cluster_replication', diff --git a/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts b/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts index 4514ffe30fcd4..28184ee086cc7 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/storybook.ts @@ -44,6 +44,20 @@ export class StorybookMock extends AbstractStorybookMock<{}, NavigationServices> navLinks$: params.navLinks$ ?? new BehaviorSubject([]), onProjectNavigationChange: params.onProjectNavigationChange ?? (() => undefined), activeNodes$: new BehaviorSubject([]), + cloudLinks: { + billingAndSub: { + title: 'Billing & Subscriptions', + href: 'https://cloud.elastic.co/account/billing', + }, + performance: { + title: 'Performance', + href: 'https://cloud.elastic.co/deployments/123456789/performance', + }, + userAndRoles: { + title: 'Users & Roles', + href: 'https://cloud.elastic.co/deployments/123456789/security/users', + }, + }, }; } diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx index c9cae230b2361..6aba392c626f9 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx @@ -127,6 +127,9 @@ export const NavigationSectionUI: FC = ({ navNode, items = [] }) => { const propsForGroupAsLink = groupIsLink ? { buttonElement: 'div' as const, + // If we don't force the state there is a little UI animation as if the + // accordion was openin/closing. We don't want any animation when it is a link. + forceState: 'closed' as const, buttonContent: ( Date: Fri, 14 Jul 2023 14:37:46 +0100 Subject: [PATCH 14/25] Fix issue when active node is at root level of the tree --- .../src/project_navigation/utils.test.ts | 23 +++++++++++++++++++ .../src/project_navigation/utils.ts | 19 +++++++++------ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.test.ts index 3f39a08385880..93abfd5d5a1f7 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.test.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.test.ts @@ -254,6 +254,29 @@ describe('findActiveNodes', () => { ]); }); + test('should find active node at the root', () => { + const flattendNavTree: Record = { + '[0]': { + id: 'root', + title: 'Root', + deepLink: getDeepLink('root', `root`), + path: ['root'], + }, + }; + + expect(findActiveNodes(`/foo/root`, flattendNavTree)).toEqual([ + [ + { + id: 'root', + title: 'Root', + isActive: true, + deepLink: getDeepLink('root', `root`), + path: ['root'], + }, + ], + ]); + }); + test('should match the longest matching node', () => { const flattendNavTree: Record = { '[0]': { diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts index 7d7b803291e0d..48158025414cb 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts @@ -73,9 +73,12 @@ function serializeDeeplinkUrl(url?: string) { * @param key The key to extract parent paths from * @returns An array of parent paths */ -function extractParentPaths(key: string) { +function extractParentPaths(key: string, navTree: Record) { // Split the string on every '][' to get an array of values without the brackets. const arr = key.split(']['); + if (arr.length === 1) { + return arr; + } // Add the brackets back in for the first and last elements, and all elements in between. arr[0] = `${arr[0]}]`; arr[arr.length - 1] = `[${arr[arr.length - 1]}`; @@ -83,10 +86,12 @@ function extractParentPaths(key: string) { arr[i] = `[${arr[i]}]`; } - return arr.reduce((acc, currentValue, currentIndex) => { - acc.push(arr.slice(0, currentIndex + 1).join('')); - return acc; - }, []); + return arr + .reduce((acc, currentValue, currentIndex) => { + acc.push(arr.slice(0, currentIndex + 1).join('')); + return acc; + }, []) + .filter((k) => Boolean(navTree[k])); } /** @@ -116,7 +121,7 @@ export const findActiveNodes = ( if (node.getIsActive && location) { const isActive = node.getIsActive({ pathNameSerialized: currentPathname, location, prepend }); if (isActive) { - const keysWithParents = extractParentPaths(key); + const keysWithParents = extractParentPaths(key, navTree); activeNodes.push(keysWithParents.map(activeNodeFromKey)); } return; @@ -140,7 +145,7 @@ export const findActiveNodes = ( if (matches.length > 0) { const longestMatch = matches[matches.length - 1]; longestMatch.forEach((key) => { - const keysWithParents = extractParentPaths(key); + const keysWithParents = extractParentPaths(key, navTree); activeNodes.push(keysWithParents.map(activeNodeFromKey)); }); } From e394800660bd338bb7bdc2098c4b51291aca7fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 14 Jul 2023 14:45:38 +0100 Subject: [PATCH 15/25] Add dev tools link to search and observability --- .../public/components/side_navigation/index.tsx | 9 +++++++++ x-pack/plugins/serverless_search/public/layout/nav.tsx | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx index f2b464c77ab73..33394041c5561 100644 --- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx @@ -137,6 +137,15 @@ const navigationTree: NavigationTreeDefinition = { }, ], footer: [ + { + type: 'navGroup', + id: 'devTools', + title: i18n.translate('xpack.serverlessObservability.nav.devTools', { + defaultMessage: 'Developer tools', + }), + link: 'dev_tools', + icon: 'editorCodeBlock', + }, { type: 'navGroup', id: 'project_settings_project_nav', diff --git a/x-pack/plugins/serverless_search/public/layout/nav.tsx b/x-pack/plugins/serverless_search/public/layout/nav.tsx index 067cee8e5a554..48f1ed349e770 100644 --- a/x-pack/plugins/serverless_search/public/layout/nav.tsx +++ b/x-pack/plugins/serverless_search/public/layout/nav.tsx @@ -117,6 +117,15 @@ const navigationTree: NavigationTreeDefinition = { }, ], footer: [ + { + type: 'navGroup', + id: 'devTools', + title: i18n.translate('xpack.serverlessSearch.nav.devTools', { + defaultMessage: 'Developer tools', + }), + link: 'dev_tools', + icon: 'editorCodeBlock', + }, { type: 'navGroup', id: 'project_settings_project_nav', From 113aa6719260abcd96a09a6f4800a76cd9c7aa76 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:59:23 +0000 Subject: [PATCH 16/25] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- packages/shared-ux/chrome/navigation/tsconfig.json | 1 + x-pack/plugins/serverless_observability/tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/shared-ux/chrome/navigation/tsconfig.json b/packages/shared-ux/chrome/navigation/tsconfig.json index 45e8325bc9d3e..b3b9b636ff0a7 100644 --- a/packages/shared-ux/chrome/navigation/tsconfig.json +++ b/packages/shared-ux/chrome/navigation/tsconfig.json @@ -26,6 +26,7 @@ "@kbn/default-nav-devtools", "@kbn/shared-ux-storybook-mock", "@kbn/core-http-browser", + "@kbn/cloud-plugin", ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/serverless_observability/tsconfig.json b/x-pack/plugins/serverless_observability/tsconfig.json index c45e5484f3ca3..379ea410f86ad 100644 --- a/x-pack/plugins/serverless_observability/tsconfig.json +++ b/x-pack/plugins/serverless_observability/tsconfig.json @@ -25,5 +25,6 @@ "@kbn/ml-plugin", "@kbn/i18n", "@kbn/management-cards-navigation", + "@kbn/cloud-plugin", ] } From fda487c7f86fbe5a92a3b6e9512202b898a9ed99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 14 Jul 2023 15:08:26 +0100 Subject: [PATCH 17/25] Update storybook to render cloud links --- .../navigation/src/ui/navigation.stories.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 241e53cb8242f..eeff6afd945e9 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.stories.tsx @@ -363,8 +363,20 @@ export const WithUIComponents = (args: NavigationServices) => { - - + + + + + + + + + From e31f70c51668d8866d17bc2886964130e461967c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 14 Jul 2023 16:31:44 +0100 Subject: [PATCH 18/25] Add jest test for cloud links --- .../chrome/navigation/mocks/src/jest.ts | 14 + .../default_navigation.test.tsx.snap | 678 ++++++++++++++++++ .../src/ui/components/navigation.test.tsx | 32 + .../src/ui/default_navigation.test.tsx | 403 ++++++----- 4 files changed, 967 insertions(+), 160 deletions(-) create mode 100644 packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap diff --git a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts index 4fd298811b5d8..d2e7f9b77ca22 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/jest.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/jest.ts @@ -29,5 +29,19 @@ export const getServicesMock = ({ navigateToUrl, onProjectNavigationChange: jest.fn(), activeNodes$: of(activeNodes), + cloudLinks: { + billingAndSub: { + title: 'Mock Billing & Subscriptions', + href: 'https://cloud.elastic.co/account/billing', + }, + performance: { + title: 'Mock Performance', + href: 'https://cloud.elastic.co/deployments/123456789/performance', + }, + userAndRoles: { + title: 'Mock Users & Roles', + href: 'https://cloud.elastic.co/deployments/123456789/security/users', + }, + }, }; }; diff --git a/packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap b/packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap new file mode 100644 index 0000000000000..232d6b7dc610e --- /dev/null +++ b/packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap @@ -0,0 +1,678 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` builds the full navigation tree when only custom project is provided reading the title from config or deeplink 1`] = ` +Array [ + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": undefined, + "href": undefined, + "id": "item1", + "isActive": false, + "path": Array [ + "group1", + "item1", + ], + "renderItem": undefined, + "title": "Item 1", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "", + "href": "", + "id": "item2", + "title": "Title from deeplink!", + "url": "", + }, + "href": undefined, + "id": "item2", + "isActive": false, + "path": Array [ + "group1", + "item2", + ], + "renderItem": undefined, + "title": "Title from deeplink!", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "", + "href": "", + "id": "item2", + "title": "Title from deeplink!", + "url": "", + }, + "href": undefined, + "id": "item3", + "isActive": false, + "path": Array [ + "group1", + "item3", + ], + "renderItem": undefined, + "title": "Deeplink title overriden", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "group1", + "isActive": false, + "path": Array [ + "group1", + ], + "title": "Group 1", + "type": "navGroup", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/discover", + "id": "discover", + "title": "Deeplink discover", + "url": "/mocked/discover", + }, + "href": undefined, + "id": "discover", + "isActive": false, + "path": Array [ + "rootNav:analytics", + "root", + "discover", + ], + "renderItem": undefined, + "title": "Deeplink discover", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/dashboards", + "id": "dashboards", + "title": "Deeplink dashboards", + "url": "/mocked/dashboards", + }, + "href": undefined, + "id": "dashboards", + "isActive": false, + "path": Array [ + "rootNav:analytics", + "root", + "dashboards", + ], + "renderItem": undefined, + "title": "Deeplink dashboards", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/visualize", + "id": "visualize", + "title": "Deeplink visualize", + "url": "/mocked/visualize", + }, + "href": undefined, + "id": "visualize", + "isActive": false, + "path": Array [ + "rootNav:analytics", + "root", + "visualize", + ], + "renderItem": undefined, + "title": "Deeplink visualize", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "root", + "isActive": false, + "path": Array [ + "rootNav:analytics", + "root", + ], + "title": "", + }, + ], + "deepLink": undefined, + "href": undefined, + "icon": "stats", + "id": "rootNav:analytics", + "isActive": false, + "path": Array [ + "rootNav:analytics", + ], + "title": "Data exploration", + "type": "navGroup", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:overview", + "id": "ml:overview", + "title": "Deeplink ml:overview", + "url": "/mocked/ml:overview", + }, + "href": undefined, + "id": "ml:overview", + "isActive": false, + "path": Array [ + "rootNav:ml", + "root", + "ml:overview", + ], + "renderItem": undefined, + "title": "Deeplink ml:overview", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:notifications", + "id": "ml:notifications", + "title": "Deeplink ml:notifications", + "url": "/mocked/ml:notifications", + }, + "href": undefined, + "id": "ml:notifications", + "isActive": false, + "path": Array [ + "rootNav:ml", + "root", + "ml:notifications", + ], + "renderItem": undefined, + "title": "Deeplink ml:notifications", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "root", + "isActive": false, + "path": Array [ + "rootNav:ml", + "root", + ], + "title": "", + }, + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:anomalyDetection", + "id": "ml:anomalyDetection", + "title": "Deeplink ml:anomalyDetection", + "url": "/mocked/ml:anomalyDetection", + }, + "href": undefined, + "id": "ml:anomalyDetection", + "isActive": false, + "path": Array [ + "rootNav:ml", + "anomaly_detection", + "ml:anomalyDetection", + ], + "renderItem": undefined, + "title": "Jobs", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:anomalyExplorer", + "id": "ml:anomalyExplorer", + "title": "Deeplink ml:anomalyExplorer", + "url": "/mocked/ml:anomalyExplorer", + }, + "href": undefined, + "id": "ml:anomalyExplorer", + "isActive": false, + "path": Array [ + "rootNav:ml", + "anomaly_detection", + "ml:anomalyExplorer", + ], + "renderItem": undefined, + "title": "Deeplink ml:anomalyExplorer", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:singleMetricViewer", + "id": "ml:singleMetricViewer", + "title": "Deeplink ml:singleMetricViewer", + "url": "/mocked/ml:singleMetricViewer", + }, + "href": undefined, + "id": "ml:singleMetricViewer", + "isActive": false, + "path": Array [ + "rootNav:ml", + "anomaly_detection", + "ml:singleMetricViewer", + ], + "renderItem": undefined, + "title": "Deeplink ml:singleMetricViewer", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:settings", + "id": "ml:settings", + "title": "Deeplink ml:settings", + "url": "/mocked/ml:settings", + }, + "href": undefined, + "id": "ml:settings", + "isActive": false, + "path": Array [ + "rootNav:ml", + "anomaly_detection", + "ml:settings", + ], + "renderItem": undefined, + "title": "Deeplink ml:settings", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "anomaly_detection", + "isActive": false, + "path": Array [ + "rootNav:ml", + "anomaly_detection", + ], + "title": "Anomaly Detection", + }, + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:dataFrameAnalytics", + "id": "ml:dataFrameAnalytics", + "title": "Deeplink ml:dataFrameAnalytics", + "url": "/mocked/ml:dataFrameAnalytics", + }, + "href": undefined, + "id": "ml:dataFrameAnalytics", + "isActive": false, + "path": Array [ + "rootNav:ml", + "data_frame_analytics", + "ml:dataFrameAnalytics", + ], + "renderItem": undefined, + "title": "Jobs", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:resultExplorer", + "id": "ml:resultExplorer", + "title": "Deeplink ml:resultExplorer", + "url": "/mocked/ml:resultExplorer", + }, + "href": undefined, + "id": "ml:resultExplorer", + "isActive": false, + "path": Array [ + "rootNav:ml", + "data_frame_analytics", + "ml:resultExplorer", + ], + "renderItem": undefined, + "title": "Deeplink ml:resultExplorer", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:analyticsMap", + "id": "ml:analyticsMap", + "title": "Deeplink ml:analyticsMap", + "url": "/mocked/ml:analyticsMap", + }, + "href": undefined, + "id": "ml:analyticsMap", + "isActive": false, + "path": Array [ + "rootNav:ml", + "data_frame_analytics", + "ml:analyticsMap", + ], + "renderItem": undefined, + "title": "Deeplink ml:analyticsMap", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "data_frame_analytics", + "isActive": false, + "path": Array [ + "rootNav:ml", + "data_frame_analytics", + ], + "title": "Data Frame Analytics", + }, + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:nodesOverview", + "id": "ml:nodesOverview", + "title": "Deeplink ml:nodesOverview", + "url": "/mocked/ml:nodesOverview", + }, + "href": undefined, + "id": "ml:nodesOverview", + "isActive": false, + "path": Array [ + "rootNav:ml", + "model_management", + "ml:nodesOverview", + ], + "renderItem": undefined, + "title": "Deeplink ml:nodesOverview", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:nodes", + "id": "ml:nodes", + "title": "Deeplink ml:nodes", + "url": "/mocked/ml:nodes", + }, + "href": undefined, + "id": "ml:nodes", + "isActive": false, + "path": Array [ + "rootNav:ml", + "model_management", + "ml:nodes", + ], + "renderItem": undefined, + "title": "Deeplink ml:nodes", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "model_management", + "isActive": false, + "path": Array [ + "rootNav:ml", + "model_management", + ], + "title": "Model Management", + }, + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:fileUpload", + "id": "ml:fileUpload", + "title": "Deeplink ml:fileUpload", + "url": "/mocked/ml:fileUpload", + }, + "href": undefined, + "id": "ml:fileUpload", + "isActive": false, + "path": Array [ + "rootNav:ml", + "data_visualizer", + "ml:fileUpload", + ], + "renderItem": undefined, + "title": "File", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:indexDataVisualizer", + "id": "ml:indexDataVisualizer", + "title": "Deeplink ml:indexDataVisualizer", + "url": "/mocked/ml:indexDataVisualizer", + }, + "href": undefined, + "id": "ml:indexDataVisualizer", + "isActive": false, + "path": Array [ + "rootNav:ml", + "data_visualizer", + "ml:indexDataVisualizer", + ], + "renderItem": undefined, + "title": "Data view", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "data_visualizer", + "isActive": false, + "path": Array [ + "rootNav:ml", + "data_visualizer", + ], + "title": "Data Visualizer", + }, + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:explainLogRateSpikes", + "id": "ml:explainLogRateSpikes", + "title": "Deeplink ml:explainLogRateSpikes", + "url": "/mocked/ml:explainLogRateSpikes", + }, + "href": undefined, + "id": "ml:explainLogRateSpikes", + "isActive": false, + "path": Array [ + "rootNav:ml", + "aiops_labs", + "ml:explainLogRateSpikes", + ], + "renderItem": undefined, + "title": "Deeplink ml:explainLogRateSpikes", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:logPatternAnalysis", + "id": "ml:logPatternAnalysis", + "title": "Deeplink ml:logPatternAnalysis", + "url": "/mocked/ml:logPatternAnalysis", + }, + "href": undefined, + "id": "ml:logPatternAnalysis", + "isActive": false, + "path": Array [ + "rootNav:ml", + "aiops_labs", + "ml:logPatternAnalysis", + ], + "renderItem": undefined, + "title": "Deeplink ml:logPatternAnalysis", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:changePointDetections", + "id": "ml:changePointDetections", + "title": "Deeplink ml:changePointDetections", + "url": "/mocked/ml:changePointDetections", + }, + "href": undefined, + "id": "ml:changePointDetections", + "isActive": false, + "path": Array [ + "rootNav:ml", + "aiops_labs", + "ml:changePointDetections", + ], + "renderItem": undefined, + "title": "Deeplink ml:changePointDetections", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "aiops_labs", + "isActive": false, + "path": Array [ + "rootNav:ml", + "aiops_labs", + ], + "title": "AIOps labs", + }, + ], + "deepLink": undefined, + "href": undefined, + "icon": "machineLearningApp", + "id": "rootNav:ml", + "isActive": false, + "path": Array [ + "rootNav:ml", + ], + "title": "Machine Learning", + "type": "navGroup", + }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/dev_tools", + "id": "dev_tools", + "title": "Deeplink dev_tools", + "url": "/mocked/dev_tools", + }, + "href": undefined, + "icon": "editorCodeBlock", + "id": "devTools", + "isActive": false, + "path": Array [ + "devTools", + ], + "title": "Developer tools", + "type": "navGroup", + }, + Object { + "breadcrumbStatus": "hidden", + "children": Array [ + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/management", + "id": "management", + "title": "Deeplink management", + "url": "/mocked/management", + }, + "href": undefined, + "id": "management", + "isActive": false, + "path": Array [ + "project_settings_project_nav", + "settings", + "management", + ], + "renderItem": undefined, + "title": "Management", + }, + Object { + "children": undefined, + "deepLink": undefined, + "href": "https://cloud.elastic.co/deployments/123456789/security/users", + "id": "cloudLinkUserAndRoles", + "isActive": false, + "path": Array [ + "project_settings_project_nav", + "settings", + "cloudLinkUserAndRoles", + ], + "renderItem": undefined, + "title": "Mock Users & Roles", + }, + Object { + "children": undefined, + "deepLink": undefined, + "href": "https://cloud.elastic.co/deployments/123456789/performance", + "id": "cloudLinkPerformance", + "isActive": false, + "path": Array [ + "project_settings_project_nav", + "settings", + "cloudLinkPerformance", + ], + "renderItem": undefined, + "title": "Mock Performance", + }, + Object { + "children": undefined, + "deepLink": undefined, + "href": "https://cloud.elastic.co/account/billing", + "id": "cloudLinkBilling", + "isActive": false, + "path": Array [ + "project_settings_project_nav", + "settings", + "cloudLinkBilling", + ], + "renderItem": undefined, + "title": "Mock Billing & Subscriptions", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "settings", + "isActive": false, + "path": Array [ + "project_settings_project_nav", + "settings", + ], + "title": "", + }, + ], + "deepLink": undefined, + "href": undefined, + "icon": "gear", + "id": "project_settings_project_nav", + "isActive": false, + "path": Array [ + "project_settings_project_nav", + ], + "title": "Project settings", + "type": "navGroup", + }, +] +`; diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/navigation.test.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/navigation.test.tsx index 1c9655eaf27ce..fce8f41734014 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/navigation.test.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/navigation.test.tsx @@ -735,4 +735,36 @@ describe('', () => { ); }); }); + + describe('cloud links', () => { + test('render the cloud link', async () => { + const onProjectNavigationChange = jest.fn(); + + const { findByTestId } = render( + + + + + + + + + + ); + + expect(await findByTestId('nav-item-group1.cloudLink1')).toBeVisible(); + expect(await findByTestId('nav-item-group1.cloudLink2')).toBeVisible(); + expect(await findByTestId('nav-item-group1.cloudLink3')).toBeVisible(); + + expect(await (await findByTestId('nav-item-group1.cloudLink1')).textContent).toBe( + 'Mock Users & RolesExternal link' + ); + expect(await (await findByTestId('nav-item-group1.cloudLink2')).textContent).toBe( + 'Mock PerformanceExternal link' + ); + expect(await (await findByTestId('nav-item-group1.cloudLink3')).textContent).toBe( + 'Mock Billing & SubscriptionsExternal link' + ); + }); + }); }); diff --git a/packages/shared-ux/chrome/navigation/src/ui/default_navigation.test.tsx b/packages/shared-ux/chrome/navigation/src/ui/default_navigation.test.tsx index d7eb8d75a7b56..ad6f2caf16c0c 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/default_navigation.test.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/default_navigation.test.tsx @@ -106,60 +106,105 @@ describe('', () => { onProjectNavigationChange.mock.calls[onProjectNavigationChange.mock.calls.length - 1]; const [navTreeGenerated] = lastCall; - expect(navTreeGenerated.navigationTree).toEqual([ - { - id: 'group1', - path: ['group1'], - title: '', - isActive: false, - children: [ - { - id: 'item1', - title: 'Item 1', - href: 'http://foo', - isActive: false, - path: ['group1', 'item1'], - }, - { - id: 'item2', - title: 'Item 2', - href: 'http://foo', - isActive: false, - path: ['group1', 'item2'], - }, - { - id: 'group1A', - title: 'Group1A', - isActive: false, - path: ['group1', 'group1A'], - children: [ - { - id: 'item1', - title: 'Group 1A Item 1', - href: 'http://foo', - isActive: false, - path: ['group1', 'group1A', 'item1'], - }, - { - id: 'group1A_1', - title: 'Group1A_1', - isActive: false, - path: ['group1', 'group1A', 'group1A_1'], - children: [ - { - id: 'item1', - title: 'Group 1A_1 Item 1', - href: 'http://foo', - isActive: false, - path: ['group1', 'group1A', 'group1A_1', 'item1'], - }, - ], - }, - ], - }, - ], - }, - ]); + expect(navTreeGenerated.navigationTree).toMatchInlineSnapshot(` + Array [ + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": undefined, + "href": "http://foo", + "id": "item1", + "isActive": false, + "path": Array [ + "group1", + "item1", + ], + "renderItem": undefined, + "title": "Item 1", + }, + Object { + "children": undefined, + "deepLink": undefined, + "href": "http://foo", + "id": "item2", + "isActive": false, + "path": Array [ + "group1", + "item2", + ], + "renderItem": undefined, + "title": "Item 2", + }, + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": undefined, + "href": "http://foo", + "id": "item1", + "isActive": false, + "path": Array [ + "group1", + "group1A", + "item1", + ], + "renderItem": undefined, + "title": "Group 1A Item 1", + }, + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": undefined, + "href": "http://foo", + "id": "item1", + "isActive": false, + "path": Array [ + "group1", + "group1A", + "group1A_1", + "item1", + ], + "renderItem": undefined, + "title": "Group 1A_1 Item 1", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "group1A_1", + "isActive": false, + "path": Array [ + "group1", + "group1A", + "group1A_1", + ], + "title": "Group1A_1", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "group1A", + "isActive": false, + "path": Array [ + "group1", + "group1A", + ], + "title": "Group1A", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "group1", + "isActive": false, + "path": Array [ + "group1", + ], + "title": "", + "type": "navGroup", + }, + ] + `); }); test('should read the title from deeplink', async () => { @@ -223,50 +268,76 @@ describe('', () => { onProjectNavigationChange.mock.calls[onProjectNavigationChange.mock.calls.length - 1]; const [navTreeGenerated] = lastCall; - expect(navTreeGenerated.navigationTree).toEqual([ - { - id: 'root', - path: ['root'], - title: '', - isActive: false, - children: [ - { - id: 'group1', - path: ['root', 'group1'], - title: '', - isActive: false, - children: [ - { - id: 'item1', - path: ['root', 'group1', 'item1'], - title: 'Title from deeplink', - isActive: false, - deepLink: { - id: 'item1', - title: 'Title from deeplink', - baseUrl: '', - url: '', - href: '', + expect(navTreeGenerated.navigationTree).toMatchInlineSnapshot(` + Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "", + "href": "", + "id": "item1", + "title": "Title from deeplink", + "url": "", + }, + "href": undefined, + "id": "item1", + "isActive": false, + "path": Array [ + "root", + "group1", + "item1", + ], + "renderItem": undefined, + "title": "Title from deeplink", }, - }, - { - id: 'item2', - title: 'Overwrite deeplink title', - path: ['root', 'group1', 'item2'], - isActive: false, - deepLink: { - id: 'item1', - title: 'Title from deeplink', - baseUrl: '', - url: '', - href: '', + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "", + "href": "", + "id": "item1", + "title": "Title from deeplink", + "url": "", + }, + "href": undefined, + "id": "item2", + "isActive": false, + "path": Array [ + "root", + "group1", + "item2", + ], + "renderItem": undefined, + "title": "Overwrite deeplink title", }, - }, - ], - }, - ], - }, - ]); + ], + "deepLink": undefined, + "href": undefined, + "id": "group1", + "isActive": false, + "path": Array [ + "root", + "group1", + ], + "title": "", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "root", + "isActive": false, + "path": Array [ + "root", + ], + "title": "", + "type": "navGroup", + }, + ] + `); }); test('should allow href for absolute links', async () => { @@ -306,31 +377,50 @@ describe('', () => { onProjectNavigationChange.mock.calls[onProjectNavigationChange.mock.calls.length - 1]; const [navTreeGenerated] = lastCall; - expect(navTreeGenerated.navigationTree).toEqual([ - { - id: 'root', - path: ['root'], - title: '', - isActive: false, - children: [ - { - id: 'group1', - path: ['root', 'group1'], - title: '', - isActive: false, - children: [ - { - id: 'item1', - path: ['root', 'group1', 'item1'], - title: 'Absolute link', - href: 'https://example.com', - isActive: false, - }, - ], - }, - ], - }, - ]); + expect(navTreeGenerated.navigationTree).toMatchInlineSnapshot(` + Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": undefined, + "deepLink": undefined, + "href": "https://example.com", + "id": "item1", + "isActive": false, + "path": Array [ + "root", + "group1", + "item1", + ], + "renderItem": undefined, + "title": "Absolute link", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "group1", + "isActive": false, + "path": Array [ + "root", + "group1", + ], + "title": "", + }, + ], + "deepLink": undefined, + "href": undefined, + "id": "root", + "isActive": false, + "path": Array [ + "root", + ], + "title": "", + "type": "navGroup", + }, + ] + `); }); test('should throw if href is not an absolute links', async () => { @@ -601,45 +691,38 @@ describe('', () => { }); // The project navigation tree passed - expect(navTreeGenerated.navigationTree[0]).toEqual({ - id: 'group1', - title: 'Group 1', - path: ['group1'], - isActive: false, - children: [ - { - id: 'item1', - title: 'Item 1', - isActive: false, - path: ['group1', 'item1'], - }, - { - id: 'item2', - path: ['group1', 'item2'], - title: 'Title from deeplink!', - isActive: false, - deepLink: { - id: 'item2', - title: 'Title from deeplink!', - baseUrl: '', - url: '', - href: '', - }, - }, - { - id: 'item3', - title: 'Deeplink title overriden', - path: ['group1', 'item3'], - isActive: false, - deepLink: { - id: 'item2', - title: 'Title from deeplink!', - baseUrl: '', - url: '', - href: '', - }, - }, - ], + expect(navTreeGenerated.navigationTree).toMatchSnapshot(); + }); + + describe('cloud links', () => { + test('render the cloud link', async () => { + const { findByTestId } = render( + + + + ); + + expect( + await ( + await findByTestId( + 'nav-item-project_settings_project_nav.settings.cloudLinkUserAndRoles' + ) + ).textContent + ).toBe('Mock Users & RolesExternal link'); + + expect( + await ( + await findByTestId( + 'nav-item-project_settings_project_nav.settings.cloudLinkPerformance' + ) + ).textContent + ).toBe('Mock PerformanceExternal link'); + + expect( + await ( + await findByTestId('nav-item-project_settings_project_nav.settings.cloudLinkBilling') + ).textContent + ).toBe('Mock Billing & SubscriptionsExternal link'); }); }); }); From d91fbea75a4da4648f6d120230142cb36caf1c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Sat, 15 Jul 2023 14:01:38 +0100 Subject: [PATCH 19/25] Remove old dev tools menu --- x-pack/plugins/serverless_search/public/layout/nav.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/x-pack/plugins/serverless_search/public/layout/nav.tsx b/x-pack/plugins/serverless_search/public/layout/nav.tsx index 48f1ed349e770..c934df0333677 100644 --- a/x-pack/plugins/serverless_search/public/layout/nav.tsx +++ b/x-pack/plugins/serverless_search/public/layout/nav.tsx @@ -35,13 +35,6 @@ const navigationTree: NavigationTreeDefinition = { }), link: 'serverlessElasticsearch', }, - { - id: 'dev_tools', - title: i18n.translate('xpack.serverlessSearch.nav.devTools', { - defaultMessage: 'Dev Tools', - }), - children: getPresets('devtools').children[0].children, - }, { id: 'explore', title: i18n.translate('xpack.serverlessSearch.nav.explore', { From b900e98c83ec7ea409e91bd9fa769f6b0ccc9e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 17 Jul 2023 08:23:45 +0100 Subject: [PATCH 20/25] Add config to test --- test/plugin_functional/test_suites/core_plugins/rendering.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 9f57286daf9e2..2fa5c5767ce60 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -221,6 +221,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.cloud.organization_url (string)', 'xpack.cloud.billing_url (string)', 'xpack.cloud.profile_url (string)', + 'xpack.cloud.performance_url (string)', + 'xpack.cloud.users_and_roles_url (string)', // can't be used to infer urls or customer id from the outside 'xpack.cloud.serverless.project_id (string)', 'xpack.discoverEnhanced.actions.exploreDataInChart.enabled (boolean)', From 98253a4def82bdd2675355596d450f1b6432271a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 17 Jul 2023 09:07:35 +0100 Subject: [PATCH 21/25] Add links in serverless.yml --- config/serverless.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/serverless.yml b/config/serverless.yml index 0e616a97177c6..bb7086b947b8e 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -13,6 +13,8 @@ xpack.cloud.base_url: "https://cloud.elastic.co" xpack.cloud.profile_url: "/user/settings" xpack.cloud.billing_url: "/billing" xpack.cloud.organization_url: "/account" +xpack.cloud.performance_url: "/performance/" +xpack.cloud.users_and_roles_url: "/users-and-roles/" # Enable ZDT migration algorithm migrations.algorithm: zdt From 247d057780d3b7a9de09b4746fa3df3d1e1d8dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 17 Jul 2023 16:29:13 +0100 Subject: [PATCH 22/25] Remove trailing slash --- config/serverless.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/serverless.yml b/config/serverless.yml index bb7086b947b8e..8f393d56ab136 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -13,8 +13,8 @@ xpack.cloud.base_url: "https://cloud.elastic.co" xpack.cloud.profile_url: "/user/settings" xpack.cloud.billing_url: "/billing" xpack.cloud.organization_url: "/account" -xpack.cloud.performance_url: "/performance/" -xpack.cloud.users_and_roles_url: "/users-and-roles/" +xpack.cloud.performance_url: "/performance" +xpack.cloud.users_and_roles_url: "/users-and-roles" # Enable ZDT migration algorithm migrations.algorithm: zdt From f8a1af96f29b0d700378ee305926edc77b2e06c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 18 Jul 2023 08:59:09 +0100 Subject: [PATCH 23/25] Revert change to serverless search nav --- .../serverless_search/public/layout/nav.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/serverless_search/public/layout/nav.tsx b/x-pack/plugins/serverless_search/public/layout/nav.tsx index 814659699a787..a8efa012bddc8 100644 --- a/x-pack/plugins/serverless_search/public/layout/nav.tsx +++ b/x-pack/plugins/serverless_search/public/layout/nav.tsx @@ -35,6 +35,13 @@ const navigationTree: NavigationTreeDefinition = { }), link: 'serverlessElasticsearch', }, + { + id: 'dev_tools', + title: i18n.translate('xpack.serverlessSearch.nav.devTools', { + defaultMessage: 'Dev Tools', + }), + children: getPresets('devtools').children[0].children, + }, { id: 'explore', title: i18n.translate('xpack.serverlessSearch.nav.explore', { @@ -111,15 +118,6 @@ const navigationTree: NavigationTreeDefinition = { }, ], footer: [ - { - type: 'navGroup', - id: 'devTools', - title: i18n.translate('xpack.serverlessSearch.nav.devTools', { - defaultMessage: 'Developer tools', - }), - link: 'dev_tools', - icon: 'editorCodeBlock', - }, { type: 'navGroup', id: 'project_settings_project_nav', From 40b9c043e500be58ed669af85a127a0b652376f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 18 Jul 2023 09:13:51 +0100 Subject: [PATCH 24/25] Revert change to obs service type --- x-pack/plugins/serverless_observability/public/services.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugins/serverless_observability/public/services.tsx b/x-pack/plugins/serverless_observability/public/services.tsx index 186267ffa94bc..67b71268de0f1 100644 --- a/x-pack/plugins/serverless_observability/public/services.tsx +++ b/x-pack/plugins/serverless_observability/public/services.tsx @@ -11,10 +11,7 @@ import React from 'react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { ServerlessObservabilityPluginStartDependencies } from './types'; -type Services = CoreStart & - ServerlessObservabilityPluginStartDependencies & { - cloud: CloudStart; - }; +type Services = CoreStart & ServerlessObservabilityPluginStartDependencies; export const KibanaServicesProvider: React.FC<{ core: CoreStart; From de41cd2ec3fc288ac5c008c47fa9ecade747aaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 18 Jul 2023 11:57:28 +0100 Subject: [PATCH 25/25] Remove static cloud link We'll inject them from cloud. Issue https://github.com/elastic/kibana/issues/162127 --- config/serverless.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config/serverless.yml b/config/serverless.yml index 8f393d56ab136..8167ee5a42f4f 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -10,11 +10,6 @@ xpack.fleet.internal.activeAgentsSoftLimit: 25000 # Cloud links xpack.cloud.base_url: "https://cloud.elastic.co" -xpack.cloud.profile_url: "/user/settings" -xpack.cloud.billing_url: "/billing" -xpack.cloud.organization_url: "/account" -xpack.cloud.performance_url: "/performance" -xpack.cloud.users_and_roles_url: "/users-and-roles" # Enable ZDT migration algorithm migrations.algorithm: zdt