Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

import type { AppDeepLinkLocations } from '@kbn/core-application-browser/src/application';

export const DEFAULT_APP_VISIBILITY: AppDeepLinkLocations[] = ['globalSearch', 'sideNav'];
export const DEFAULT_APP_VISIBILITY: AppDeepLinkLocations[] = [
'globalSearch',
'classicSideNav',
'solutionSideNav',
];

export const DEFAULT_LINK_VISIBILITY: AppDeepLinkLocations[] = ['globalSearch'];
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('getAppInfo', () => {
id: 'some-id',
title: 'some-title',
status: AppStatus.accessible,
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
appRoute: `/app/some-id`,
keywords: [],
deepLinks: [],
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('getAppInfo', () => {
id: 'some-id',
title: 'some-title',
status: AppStatus.accessible,
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
appRoute: `/app/some-id`,
keywords: [],
deepLinks: [
Expand Down Expand Up @@ -98,7 +98,7 @@ describe('getAppInfo', () => {
expect(
getAppInfo(
createApp({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
status: AppStatus.inaccessible,
})
)
Expand All @@ -110,25 +110,25 @@ describe('getAppInfo', () => {
expect(
getAppInfo(
createApp({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
status: AppStatus.accessible,
})
)
).toEqual(
expect.objectContaining({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
})
);
expect(
getAppInfo(
createApp({
// status is not set, default to accessible
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
})
)
).toEqual(
expect.objectContaining({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
})
);
});
Expand Down Expand Up @@ -159,7 +159,7 @@ describe('getAppInfo', () => {
id: 'some-id',
title: 'some-title',
status: AppStatus.accessible,
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
appRoute: `/app/some-id`,
keywords: [],
order: 3,
Expand Down Expand Up @@ -192,7 +192,7 @@ describe('getAppInfo', () => {
createApp({
deepLinks: [
createDeepLink({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
}),
],
})
Expand All @@ -201,7 +201,7 @@ describe('getAppInfo', () => {
expect.objectContaining({
deepLinks: [
expect.objectContaining({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
}),
],
})
Expand Down
20 changes: 14 additions & 6 deletions src/core/packages/application/browser/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,13 @@ export interface App<HistoryLocationState = unknown> extends AppNavOptions {
* - "globalSearch": the link will appear in the global search bar
* - "home": the link will appear on the Kibana home page
* - "kibanaOverview": the link will appear in the Kibana overview page
* - "sideNav": the link will appear in the side navigation.
* Note: "sideNav" will be deprecated when we change the navigation to "solutions" style.
* - "classicSideNav": the link will appear in the classic (hamburger) side navigation only.
* - "solutionSideNav": the link will appear in solution navs only, not in the classic side navigation.
* Use this when the link should be accessible from a solution nav but hidden from the classic hamburger menu.
*
* @default ['globalSearch', 'sideNav']
* To appear in both classic and solution navs, use `['classicSideNav', 'solutionSideNav']`.
*
* @default ['globalSearch', 'classicSideNav', 'solutionSideNav']
* unless the status is marked as `inaccessible`.
* @note Set to `[]` (empty array) to hide this link
*/
Expand Down Expand Up @@ -150,9 +153,9 @@ export interface App<HistoryLocationState = unknown> extends AppNavOptions {
*
* start() {
* // later, when the navlink needs to be updated
* appUpdater.next(() => {
* appUpdater.next(() => ({
* visibleIn: ['globalSearch'],
* })
* }))
* }
* ```
*/
Expand Down Expand Up @@ -254,7 +257,12 @@ export type PublicAppDeepLinkInfo = Omit<AppDeepLink, 'deepLinks' | 'keywords' |
};

/** The places in the UI where a deepLink can be shown */
export type AppDeepLinkLocations = 'globalSearch' | 'sideNav' | 'home' | 'kibanaOverview';
export type AppDeepLinkLocations =
| 'globalSearch'
| 'classicSideNav'
| 'solutionSideNav'
| 'home'
| 'kibanaOverview';

/**
* Input type for registering secondary in-app locations for an application.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { CollapsibleNav } from './collapsible_nav';

const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES;

const visibleIn = ['globalSearch' as const, 'sideNav' as const];
const visibleIn = ['globalSearch' as const, 'classicSideNav' as const, 'solutionSideNav' as const];

function mockLink({ title = 'discover', category }: Partial<ChromeNavLink>) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function CollapsibleNav({
allLinks.filter(
(link) =>
// Filterting out hidden links,
link.visibleIn.includes('sideNav') &&
link.visibleIn.includes('classicSideNav') &&
// and non-data overview pages
!overviewIDsToHide.includes(link.id)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ describe('hidden panel link', () => {
baseUrl: '/',
href: '/app/management',
url: '/app/management',
visibleIn: ['sideNav'],
visibleIn: ['classicSideNav', 'solutionSideNav'],
},
sideNavStatus: 'hidden',
id: 'stack_management',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const availableApps: ReadonlyMap<string, App> = new Map([
order: 50,
title: 'Deep App 1',
path: '/deepapp1',
visibleIn: ['sideNav'],
visibleIn: ['classicSideNav', 'solutionSideNav'],
deepLinks: [
{
id: 'deepApp2',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('toNavLink', () => {
order: 12,
tooltip: 'tooltip',
euiIconType: 'my-icon',
visibleIn: ['sideNav'],
visibleIn: ['classicSideNav', 'solutionSideNav'],
}),
basePath
);
Expand All @@ -58,7 +58,7 @@ describe('toNavLink', () => {
order: 12,
tooltip: 'tooltip',
euiIconType: 'my-icon',
visibleIn: ['sideNav'],
visibleIn: ['classicSideNav', 'solutionSideNav'],
})
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const getNavLink = (partial: Partial<ChromeNavLink> = {}): ChromeNavLink => ({
baseUrl: '/app',
url: `/app/${partial.id ?? 'kibana'}`,
href: `/app/${partial.id ?? 'kibana'}`,
visibleIn: ['globalSearch'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
...partial,
});

Expand Down Expand Up @@ -208,6 +208,89 @@ describe('initNavigation()', () => {
expect(node.children?.[0].href).toBe('https://elastic.co');
});

test('should filter out deepLinks that exclude solutionSideNav from visibleIn', async () => {
const { projectNavigation: svc, navLinksService: nls } = setup({
navLinkIds: ['management:genAiSettings'],
});

// Add a second link that is registered but explicitly excludes solutionSideNav
const evalsNoSideNav: ChromeNavLink = getNavLink({
id: 'management:evals',
title: 'MANAGEMENT:EVALS',
visibleIn: ['globalSearch'],
});
const existing = nls.getAll();
nls.getNavLinks$.mockReturnValue(of([...existing, evalsNoSideNav]));
nls.getAll.mockReturnValue([...existing, evalsNoSideNav]);

svc.initNavigation<any>(
'es',
of({
body: [
{
id: 'group1',
type: 'navGroup',
children: [
{ link: 'management:genAiSettings' },
{ link: 'management:evals' }, // registered but no solutionSideNav → removed
],
},
],
})
);

const treeDefinition = await lastValueFrom(
svc.getNavigation$().pipe(
take(1),
map((nav) => nav.navigationTree)
)
);

const [node] = treeDefinition.body as [ChromeProjectNavigationNode];
expect(node.children?.map((c) => c.id)).toEqual(['management:genAiSettings']);
});

test('should filter out deepLinks that only include classicSideNav from visibleIn', async () => {
const { projectNavigation: svc, navLinksService: nls } = setup({
navLinkIds: ['management:genAiSettings'],
});

const classicOnlyLink: ChromeNavLink = getNavLink({
id: 'management:evals',
title: 'MANAGEMENT:EVALS',
visibleIn: ['globalSearch', 'classicSideNav'],
});
const existing = nls.getAll();
nls.getNavLinks$.mockReturnValue(of([...existing, classicOnlyLink]));
nls.getAll.mockReturnValue([...existing, classicOnlyLink]);

svc.initNavigation<any>(
'es',
of({
body: [
{
id: 'group1',
type: 'navGroup',
children: [
{ link: 'management:genAiSettings' },
{ link: 'management:evals' }, // classicSideNav only → removed from solution nav
],
},
],
})
);

const treeDefinition = await lastValueFrom(
svc.getNavigation$().pipe(
take(1),
map((nav) => nav.navigationTree)
)
);

const [node] = treeDefinition.body as [ChromeProjectNavigationNode];
expect(node.children?.map((c) => c.id)).toEqual(['management:genAiSettings']);
});

test('should filter out missing deepLinks (e.g. evals) from the navigation tree', async () => {
const { projectNavigation: projectNavigationService } = setup({
navLinkIds: ['management:genAiSettings'],
Expand Down Expand Up @@ -289,7 +372,7 @@ describe('initNavigation()', () => {
id: 'foo',
title: 'FOO',
url: '/app/foo',
visibleIn: ['globalSearch'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
},
href: '/app/foo',
id: 'foo',
Expand Down Expand Up @@ -838,7 +921,7 @@ describe('getNavigation$() active nodes', () => {
baseUrl: '/app',
url: '/app/item1',
href: '/app/item1',
visibleIn: ['globalSearch'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
},
},
],
Expand Down Expand Up @@ -892,7 +975,7 @@ describe('getNavigation$() active nodes', () => {
baseUrl: '/app',
url: '/app/item1',
href: '/app/item1',
visibleIn: ['globalSearch'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
},
getIsActive: expect.any(Function),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const getDeepLink = (id: string, path: string, title = ''): ChromeNavLink => ({
href: `http://mocked/kibana/foo/${path}`,
title,
baseUrl: '',
visibleIn: ['globalSearch'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav'],
});

describe('flattenNav', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ function getNodeStatus(
},
{ cloudLinks }: { cloudLinks: CloudLinks }
): SideNavNodeStatus | 'remove' {
if (link && !deepLink) {
// If a link is provided, but no deepLink is found, don't render anything
if (link && (!deepLink || !deepLink.visibleIn.includes('solutionSideNav'))) {
// If a link is provided but no deepLink found, or the app excluded solutionSideNav
return 'remove';
Comment thread
paulinashakirova marked this conversation as resolved.
Outdated
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class KibanaOverviewPlugin
order: 1,
updater$: appUpdater$,
appRoute: PLUGIN_PATH,
visibleIn: ['globalSearch', 'home', 'sideNav'],
visibleIn: ['globalSearch', 'home', 'classicSideNav', 'solutionSideNav'],
async mount(params: AppMountParameters) {
// Load application bundle
const { renderApp } = await import('./application');
Expand Down
1 change: 1 addition & 0 deletions src/platform/plugins/shared/dev_tools/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export class DevToolsPlugin implements Plugin<DevToolsSetup, void> {
id: tool.id,
title: tool.title as string,
path: `#/${tool.id}`,
visibleIn: ['globalSearch', 'solutionSideNav'],
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Without this line these deep links default to ['globalSearch'] (deep-link default, not the app one). On main that still worked in solution navs because the resolver was silently ignoring visibleIn — this PR makes it actually respect it, so without 'solutionSideNav' here the Console / Grok Debugger / Painless Lab / Search Profiler entries would disappear from any solution nav that references them.

Skipping 'classicSideNav' on purpose: the classic hamburger already has the single "Dev Tools" app entry. Adding it here would surface all four tools as separate top-level hamburger items, which isn't what main shows.

};
if (!devtoolsDeeplinkIds.some((id) => id === deepLink.id)) {
throw new Error('Deeplink must be registered in package.');
Expand Down
2 changes: 1 addition & 1 deletion src/platform/plugins/shared/discover/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export class DiscoverPlugin
euiIconType: 'logoKibana',
defaultPath: '#/',
category: DEFAULT_APP_CATEGORIES.kibana,
visibleIn: ['globalSearch', 'sideNav', 'kibanaOverview'],
visibleIn: ['globalSearch', 'classicSideNav', 'solutionSideNav', 'kibanaOverview'],
mount: async (params: AppMountParameters) => {
const [[coreStart, discoverStartPlugins], historyService, ebtManager, { renderApp }] =
await Promise.all([
Expand Down
5 changes: 4 additions & 1 deletion src/platform/plugins/shared/management/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ export class ManagementPlugin
title: mgmtApp.title,
path: mgmtApp.basePath,
keywords: mgmtApp.keywords,
...(mgmtApp.visibleIn ? { visibleIn: mgmtApp.visibleIn } : {}),
// Default includes both 'classicSideNav' and 'solutionSideNav' so all management sections
// appear in both navigation modes. Sections that explicitly set visibleIn
// (e.g. to hide from globalSearch) must also include the relevant nav flags.
visibleIn: mgmtApp.visibleIn ?? ['globalSearch', 'classicSideNav', 'solutionSideNav'],
})),
}));

Expand Down
Loading
Loading