From d91fe9fc92133cfa52c4b9aff664018b241f2ac3 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 19 Sep 2023 13:51:09 +0200 Subject: [PATCH] [Serverless] Improve breadcrumbs in management (#166259) ## Summary Close https://github.com/elastic/kibana/issues/164507 This PR improves management breadcrumbs in serverless project. ![Screenshot 2023-09-13 at 16 45 28](https://github.com/elastic/kibana/assets/7784120/97f9dd25-aeed-468b-8ea6-9ffa66ce14d0) - **Management**: I removed dependency from serverless -> management. details: https://github.com/elastic/kibana/pull/166259#discussion_r1324412333 - **Search**: Search project links directly to some management sub-apps from the side nav. In some cases I hid the breadcrumb that comes from the navigation config to avoid duplication: for example there was`Index Management > Index Management` where the first came from the nav and the second from the management sub-app. - **Security**: For security I disabled setting management sub-app breadcrumbs from the navigation config as they are set from the apps. This allows for deeper breadcrumbs, beyond just nav. https://github.com/elastic/kibana/pull/166259#discussion_r1324411585 --- src/plugins/management/kibana.jsonc | 3 ++- src/plugins/management/public/plugin.ts | 14 ++++++++++-- src/plugins/management/tsconfig.json | 3 ++- .../public/navigation/index.ts | 1 + .../public/navigation/navigation_tree.ts | 7 ++++-- x-pack/plugins/serverless/kibana.jsonc | 1 - x-pack/plugins/serverless/public/plugin.tsx | 2 -- x-pack/plugins/serverless/public/types.ts | 3 --- x-pack/plugins/serverless/tsconfig.json | 1 - .../serverless_observability/public/plugin.ts | 1 + .../serverless_search/public/layout/nav.tsx | 6 +++++ .../serverless_search/public/plugin.ts | 1 + .../page_objects/svl_common_navigation.ts | 19 ++++++++++++++++ .../test_suites/search/navigation.ts | 22 +++++++++++++++++++ 14 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/plugins/management/kibana.jsonc b/src/plugins/management/kibana.jsonc index b180cd74fa3d3..1c5f3ebc4bf36 100644 --- a/src/plugins/management/kibana.jsonc +++ b/src/plugins/management/kibana.jsonc @@ -10,7 +10,8 @@ "share" ], "optionalPlugins": [ - "home" + "home", + "serverless" ], "requiredBundles": [ "kibanaReact", diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index f272b84aa2e0a..d4ef130eb4de0 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import { ServerlessPluginStart } from '@kbn/serverless/public'; import { CoreSetup, CoreStart, @@ -39,6 +40,7 @@ interface ManagementSetupDependencies { interface ManagementStartDependencies { share: SharePluginStart; + serverless?: ServerlessPluginStart; } export class ManagementPlugin @@ -122,13 +124,21 @@ export class ManagementPlugin updater$: this.appUpdater, async mount(params: AppMountParameters) { const { renderApp } = await import('./application'); - const [coreStart] = await core.getStartServices(); + const [coreStart, deps] = await core.getStartServices(); return renderApp(params, { sections: getSectionsServiceStartPrivate(), kibanaVersion, coreStart, - setBreadcrumbs: coreStart.chrome.setBreadcrumbs, + setBreadcrumbs: (newBreadcrumbs) => { + if (deps.serverless) { + // drop the root management breadcrumb in serverless because it comes from the navigation tree + const [, ...trailingBreadcrumbs] = newBreadcrumbs; + deps.serverless.setBreadcrumbs(trailingBreadcrumbs); + } else { + coreStart.chrome.setBreadcrumbs(newBreadcrumbs); + } + }, isSidebarEnabled$: managementPlugin.isSidebarEnabled$, cardsNavigationConfig$: managementPlugin.cardsNavigationConfig$, landingPageRedirect$: managementPlugin.landingPageRedirect$, diff --git a/src/plugins/management/tsconfig.json b/src/plugins/management/tsconfig.json index 3ad07523cecb0..77c3752e7b0c3 100644 --- a/src/plugins/management/tsconfig.json +++ b/src/plugins/management/tsconfig.json @@ -25,7 +25,8 @@ "@kbn/test-jest-helpers", "@kbn/config-schema", "@kbn/core-application-browser", - "@kbn/core-http-browser" + "@kbn/core-http-browser", + "@kbn/serverless" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/index.ts b/x-pack/plugins/security_solution_serverless/public/navigation/index.ts index 84842a90e1f74..19684479e7dd6 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/index.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/index.ts @@ -27,6 +27,7 @@ export const configureNavigation = ( if (!serverConfig.developer.disableManagementUrlRedirect) { management.setLandingPageRedirect(SECURITY_PROJECT_SETTINGS_PATH); } + management.setIsSidebarEnabled(false); serverless.setProjectHome(APP_PATH); serverless.setSideNavComponent(getSecuritySideNavComponent(services)); diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree.ts b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree.ts index 7210498a97d57..55b82759e6a02 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/navigation_tree.ts @@ -35,6 +35,10 @@ const HIDDEN_BREADCRUMBS = new Set([ SecurityPageName.sessions, ]); +const isBreadcrumbHidden = (id: ProjectPageName): boolean => + HIDDEN_BREADCRUMBS.has(id) || + id.startsWith('management:'); /* management sub-pages set their breadcrumbs themselves */ + export const subscribeNavigationTree = (services: Services): void => { const { serverless, getProjectNavLinks$ } = services; @@ -59,13 +63,12 @@ export const getFormatChromeProjectNavNodes = (services: Services) => { const navLinkId = getNavLinkIdFromProjectPageName(id); if (chrome.navLinks.has(navLinkId)) { - const breadcrumbHidden = HIDDEN_BREADCRUMBS.has(id); const link: ChromeProjectNavigationNode = { id: navLinkId, title, path: [...path, navLinkId], deepLink: chrome.navLinks.get(navLinkId), - ...(breadcrumbHidden && { breadcrumbStatus: 'hidden' }), + ...(isBreadcrumbHidden(id) && { breadcrumbStatus: 'hidden' }), }; // check default navigation for children const defaultChildrenNav = getDefaultChildrenNav(id, link); diff --git a/x-pack/plugins/serverless/kibana.jsonc b/x-pack/plugins/serverless/kibana.jsonc index 35b21a5fc39b5..d8993d5ac7c72 100644 --- a/x-pack/plugins/serverless/kibana.jsonc +++ b/x-pack/plugins/serverless/kibana.jsonc @@ -14,7 +14,6 @@ ], "requiredPlugins": [ "kibanaReact", - "management", "cloud" ], "optionalPlugins": [], diff --git a/x-pack/plugins/serverless/public/plugin.tsx b/x-pack/plugins/serverless/public/plugin.tsx index 2b2c11001e8f3..6333d9a5f58a0 100644 --- a/x-pack/plugins/serverless/public/plugin.tsx +++ b/x-pack/plugins/serverless/public/plugin.tsx @@ -49,7 +49,6 @@ export class ServerlessPlugin dependencies: ServerlessPluginStartDependencies ): ServerlessPluginStart { const { developer } = this.config; - const { management } = dependencies; if (developer && developer.projectSwitcher && developer.projectSwitcher.enabled) { const { currentType } = developer.projectSwitcher; @@ -61,7 +60,6 @@ export class ServerlessPlugin } core.chrome.setChromeStyle('project'); - management.setIsSidebarEnabled(false); // Casting the "chrome.projects" service to an "internal" type: this is intentional to obscure the property from Typescript. const { project } = core.chrome as InternalChromeStart; diff --git a/x-pack/plugins/serverless/public/types.ts b/x-pack/plugins/serverless/public/types.ts index 1de2e2fd75e1a..84fc2565905d6 100644 --- a/x-pack/plugins/serverless/public/types.ts +++ b/x-pack/plugins/serverless/public/types.ts @@ -12,7 +12,6 @@ import type { SideNavComponent, ChromeProjectNavigationNode, } from '@kbn/core-chrome-browser'; -import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import type { Observable } from 'rxjs'; @@ -31,11 +30,9 @@ export interface ServerlessPluginStart { } export interface ServerlessPluginSetupDependencies { - management: ManagementSetup; cloud: CloudSetup; } export interface ServerlessPluginStartDependencies { - management: ManagementStart; cloud: CloudStart; } diff --git a/x-pack/plugins/serverless/tsconfig.json b/x-pack/plugins/serverless/tsconfig.json index 4bb3e35e34472..e8224182afae9 100644 --- a/x-pack/plugins/serverless/tsconfig.json +++ b/x-pack/plugins/serverless/tsconfig.json @@ -17,7 +17,6 @@ "@kbn/config-schema", "@kbn/core", "@kbn/kibana-react-plugin", - "@kbn/management-plugin", "@kbn/serverless-project-switcher", "@kbn/serverless-types", "@kbn/utils", diff --git a/x-pack/plugins/serverless_observability/public/plugin.ts b/x-pack/plugins/serverless_observability/public/plugin.ts index e208fd5f8cadd..bbc2ac7e0435b 100644 --- a/x-pack/plugins/serverless_observability/public/plugin.ts +++ b/x-pack/plugins/serverless_observability/public/plugin.ts @@ -45,6 +45,7 @@ export class ServerlessObservabilityPlugin observabilityShared.setIsSidebarEnabled(false); serverless.setProjectHome('/app/observability/landing'); serverless.setSideNavComponent(getObservabilitySideNavComponent(core, { serverless, cloud })); + management.setIsSidebarEnabled(false); management.setupCardsNavigation({ enabled: true, hideLinksTo: [appIds.RULES], diff --git a/x-pack/plugins/serverless_search/public/layout/nav.tsx b/x-pack/plugins/serverless_search/public/layout/nav.tsx index 0879f86bc2b73..10d963100f89c 100644 --- a/x-pack/plugins/serverless_search/public/layout/nav.tsx +++ b/x-pack/plugins/serverless_search/public/layout/nav.tsx @@ -93,12 +93,16 @@ const navigationTree: NavigationTreeDefinition = { defaultMessage: 'Index Management', }), link: 'management:index_management', + breadcrumbStatus: + 'hidden' /* management sub-pages set their breadcrumbs themselves */, }, { title: i18n.translate('xpack.serverlessSearch.nav.content.pipelines', { defaultMessage: 'Pipelines', }), link: 'management:ingest_pipelines', + breadcrumbStatus: + 'hidden' /* management sub-pages set their breadcrumbs themselves */, }, ], }, @@ -110,6 +114,8 @@ const navigationTree: NavigationTreeDefinition = { children: [ { link: 'management:api_keys', + breadcrumbStatus: + 'hidden' /* management sub-pages set their breadcrumbs themselves */, }, ], }, diff --git a/x-pack/plugins/serverless_search/public/plugin.ts b/x-pack/plugins/serverless_search/public/plugin.ts index 6bd5b384fd581..dfe19e4dd55d3 100644 --- a/x-pack/plugins/serverless_search/public/plugin.ts +++ b/x-pack/plugins/serverless_search/public/plugin.ts @@ -79,6 +79,7 @@ export class ServerlessSearchPlugin ): ServerlessSearchPluginStart { serverless.setProjectHome('/app/elasticsearch'); serverless.setSideNavComponent(createComponent(core, { serverless, cloud })); + management.setIsSidebarEnabled(false); management.setupCardsNavigation({ enabled: true, hideLinksTo: [appIds.MAINTENANCE_WINDOWS], diff --git a/x-pack/test_serverless/functional/page_objects/svl_common_navigation.ts b/x-pack/test_serverless/functional/page_objects/svl_common_navigation.ts index 8d3edb04d640a..3c3b10a5d03c8 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_common_navigation.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_common_navigation.ts @@ -151,6 +151,25 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) { }); } }, + async expectBreadcrumbMissing(by: { deepLinkId: AppDeepLinkId } | { text: string }) { + if ('deepLinkId' in by) { + await testSubjects.missingOrFail(`~breadcrumb-deepLinkId-${by.deepLinkId}`); + } else { + await retry.try(async () => { + expect(await getByVisibleText('~breadcrumb', by.text)).be(null); + }); + } + }, + async expectBreadcrumbTexts(expectedBreadcrumbTexts: string[]) { + await retry.try(async () => { + const breadcrumbsContainer = await testSubjects.find('breadcrumbs'); + const breadcrumbs = await breadcrumbsContainer.findAllByTestSubject('~breadcrumb'); + breadcrumbs.shift(); // remove home + expect(expectedBreadcrumbTexts.length).to.eql(breadcrumbs.length); + const texts = await Promise.all(breadcrumbs.map((b) => b.getVisibleText())); + expect(expectedBreadcrumbTexts).to.eql(texts); + }); + }, }, search: new SvlNavigationSearchPageObject(ctx), recent: { diff --git a/x-pack/test_serverless/functional/test_suites/search/navigation.ts b/x-pack/test_serverless/functional/test_suites/search/navigation.ts index 52369c12c66bb..4852c0a369b58 100644 --- a/x-pack/test_serverless/functional/test_suites/search/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/search/navigation.ts @@ -71,6 +71,28 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await expectNoPageReload(); }); + it("management apps from the sidenav hide the 'stack management' root from the breadcrumbs", async () => { + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:triggersActions' }); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Explore', 'Alerts', 'Rules']); + + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:index_management' }); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Content', 'Index Management']); + + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:ingest_pipelines' }); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Content', 'Ingest Pipelines']); + + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management:api_keys' }); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Security', 'API keys']); + }); + + it('navigate management', async () => { + await svlCommonNavigation.sidenav.openSection('project_settings_project_nav'); + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'management' }); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Management']); + await testSubjects.click('app-card-dataViews'); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbTexts(['Management', 'Data views']); + }); + it('navigate using search', async () => { await svlCommonNavigation.search.showSearch(); // TODO: test something search project specific instead of generic discover