[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](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
This commit is contained in:
Anton Dosov 2023-09-19 13:51:09 +02:00 committed by GitHub
parent 36a7a80f38
commit d91fe9fc92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 71 additions and 13 deletions

View file

@ -10,7 +10,8 @@
"share"
],
"optionalPlugins": [
"home"
"home",
"serverless"
],
"requiredBundles": [
"kibanaReact",

View file

@ -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$,

View file

@ -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/**/*"

View file

@ -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));

View file

@ -35,6 +35,10 @@ const HIDDEN_BREADCRUMBS = new Set<ProjectPageName>([
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);

View file

@ -14,7 +14,6 @@
],
"requiredPlugins": [
"kibanaReact",
"management",
"cloud"
],
"optionalPlugins": [],

View file

@ -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;

View file

@ -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;
}

View file

@ -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",

View file

@ -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],

View file

@ -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 */,
},
],
},

View file

@ -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],

View file

@ -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: {

View file

@ -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