Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pageLoadAssetSize:
inference: 10368
inferenceWorkflows: 25000
infra: 56302
ingestHub: 13422
ingestHub: 14788
ingestPipelines: 17866
inputControlVis: 7638
inspectComponent: 4590
Expand Down
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',
'projectSideNav',
];

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', 'projectSideNav'],
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', 'projectSideNav'],
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', 'projectSideNav'],
status: AppStatus.inaccessible,
})
)
Expand All @@ -110,25 +110,25 @@ describe('getAppInfo', () => {
expect(
getAppInfo(
createApp({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'projectSideNav'],
status: AppStatus.accessible,
})
)
).toEqual(
expect.objectContaining({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'projectSideNav'],
})
);
expect(
getAppInfo(
createApp({
// status is not set, default to accessible
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'projectSideNav'],
})
)
).toEqual(
expect.objectContaining({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'projectSideNav'],
})
);
});
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', 'projectSideNav'],
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', 'projectSideNav'],
}),
],
})
Expand All @@ -201,7 +201,7 @@ describe('getAppInfo', () => {
expect.objectContaining({
deepLinks: [
expect.objectContaining({
visibleIn: ['globalSearch', 'sideNav'],
visibleIn: ['globalSearch', 'classicSideNav', 'projectSideNav'],
}),
],
})
Expand Down
33 changes: 19 additions & 14 deletions src/core/packages/application/browser/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,16 @@ export interface App<HistoryLocationState = unknown> extends AppNavOptions {
status?: AppStatus;

/**
* Optional list of locations where the app is visible.
* Locations where the app is visible. Each value enables one surface:
* - `globalSearch`: top-bar search results
* - `classicSideNav`: classic (hamburger) side navigation
* - `projectSideNav`: project side navs (Observability, Security, Search)
* - `home`: Kibana home page
* - `kibanaOverview`: Kibana overview page
*
* Accepts the following values:
* - "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.
*
* @default ['globalSearch', 'sideNav']
* @default ['globalSearch', 'classicSideNav', 'projectSideNav']
* unless the status is marked as `inaccessible`.
* @note Set to `[]` (empty array) to hide this link
* @note Set to `[]` (empty array) to hide this link from every surface.
*/
visibleIn?: AppDeepLinkLocations[];

Expand Down Expand Up @@ -150,9 +148,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 +252,13 @@ 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'
// TODO: rename to 'sideNav' when 'classicSideNav' is removed (classic nav deprecation).
| 'projectSideNav'
| 'home'
| 'kibanaOverview';

/**
* Input type for registering secondary in-app locations for an application.
Expand All @@ -272,7 +276,8 @@ export type AppDeepLink<Id extends string = string> = {
/** Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. */
keywords?: string[];
/**
* Optional list of locations where the deepLink is visible. By default the deepLink is visible in "globalSearch".
* Locations where the deep link is visible. See {@link App.visibleIn} for the list of surfaces.
* @default ['globalSearch']
*/
visibleIn?: AppDeepLinkLocations[];
/**
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, 'projectSideNav' 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', 'projectSideNav'],
},
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', 'projectSideNav'],
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', 'projectSideNav'],
}),
basePath
);
Expand All @@ -58,7 +58,7 @@ describe('toNavLink', () => {
order: 12,
tooltip: 'tooltip',
euiIconType: 'my-icon',
visibleIn: ['sideNav'],
visibleIn: ['classicSideNav', 'projectSideNav'],
})
);
});
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', 'projectSideNav'],
...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 projectSideNav from visibleIn', async () => {
const { projectNavigation: svc, navLinksService: nls } = setup({
navLinkIds: ['management:genAiSettings'],
});

// Add a second link that is registered but explicitly excludes projectSideNav
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 projectSideNav → 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('removes deepLinks from the solution nav tree when their visibleIn excludes projectSideNav', 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' }, // visibleIn excludes projectSideNav → 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 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', 'projectSideNav'],
},
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', 'projectSideNav'],
},
},
],
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', 'projectSideNav'],
},
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', 'projectSideNav'],
});

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('projectSideNav'))) {
// If a link is provided but no deepLink found, or the app excluded projectSideNav
return 'remove';
}

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', 'projectSideNav'],
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', 'projectSideNav'],
};
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', 'projectSideNav', 'kibanaOverview'],
mount: async (params: AppMountParameters) => {
const [[coreStart, discoverStartPlugins], historyService, ebtManager, { renderApp }] =
await Promise.all([
Expand Down
Loading
Loading