Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
567e3b6
add launchpad to classic nav with sub links
ashokaditya Mar 31, 2026
99ff01c
remove dashboard sub nav panel open
ashokaditya Mar 31, 2026
c82b2ee
fix feature flagged breadcrumbs and links
ashokaditya Mar 31, 2026
55fd026
tests
ashokaditya Apr 1, 2026
67d6b15
cleanup tests
ashokaditya Apr 1, 2026
598cc82
fix
ashokaditya Apr 1, 2026
418edf7
Merge branch 'main' into task/security-deployment-classic-nav-udpates
ashokaditya Apr 6, 2026
6fdb608
update bundle limit for security_solution
ashokaditya Apr 6, 2026
8f9a24e
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 6, 2026
ef71199
fix
ashokaditya Apr 7, 2026
e6cfa3d
add test for feature flagged nav version
ashokaditya Apr 7, 2026
90857a8
Merge branch 'main' into task/security-deployment-classic-nav-udpates
ashokaditya Apr 7, 2026
dc9f1d4
fix bad merge
ashokaditya Apr 7, 2026
735473b
redo test files
ashokaditya Apr 7, 2026
333665b
fix: path
logeekal Apr 7, 2026
d09702f
Merge branch 'main' into task/security-deployment-classic-nav-udpates
ashokaditya Apr 7, 2026
c767b39
add `manage automatic migrations` to launchpad
ashokaditya Apr 7, 2026
c5ffc21
Merge branch 'task/security-deployment-classic-nav-udpates' of github…
ashokaditya Apr 7, 2026
cc9752c
Merge branch 'main' into task/security-deployment-classic-nav-udpates
ashokaditya Apr 7, 2026
07ec795
Merge branch 'main' into task/security-deployment-classic-nav-udpates
james-elastic Apr 8, 2026
36603c5
Merge branch 'main' into task/security-deployment-classic-nav-udpates
james-elastic Apr 8, 2026
8c59d81
Merge branch 'main' into task/security-deployment-classic-nav-udpates
ashokaditya Apr 8, 2026
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 @@ -162,7 +162,7 @@ pageLoadAssetSize:
searchQueryRules: 6689
searchSynonyms: 6371
security: 79627
securitySolution: 135301
securitySolution: 157027
securitySolutionEss: 38689
securitySolutionServerless: 52082
serverless: 7412
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export enum SecurityPageName {
hostsUncommonProcesses = 'hosts-uncommon_processes',
kubernetes = 'kubernetes',
landing = 'get_started',
launchpad = 'launchpad',
network = 'network',
networkAnomalies = 'network-anomalies',
networkDns = 'network-dns',
Expand Down
6 changes: 5 additions & 1 deletion x-pack/solutions/security/packages/side-nav/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
* 2.0.
*/

export { SolutionSideNav, type SolutionSideNavProps } from './src';
export {
SolutionSideNav,
type SolutionSideNavProps,
type SolutionSideNavInteractionVariant,
} from './src';
export { SolutionSideNavItemPosition } from './src/types';
export type { SolutionSideNavItem, Tracker } from './src/types';
4 changes: 2 additions & 2 deletions x-pack/solutions/security/packages/side-nav/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { SolutionSideNavProps } from './solution_side_nav';
import type { SolutionSideNavInteractionVariant, SolutionSideNavProps } from './solution_side_nav';

export type { SolutionSideNavProps };
export type { SolutionSideNavProps, SolutionSideNavInteractionVariant };

const SolutionSideNavLazy = lazy(() => import('./solution_side_nav'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@
* 2.0.
*/

import { transparentize, type EuiThemeComputed } from '@elastic/eui';
import { type EuiThemeComputed, transparentize } from '@elastic/eui';
import { css } from '@emotion/css';

export const SolutionSideNavItemStyles = (euiTheme: EuiThemeComputed<{}>) => css`
export type SolutionSideNavSelectedAppearance = 'default' | 'primary';

export const SolutionSideNavItemStyles = (
euiTheme: EuiThemeComputed<{}>,
selectedAppearance: SolutionSideNavSelectedAppearance = 'default'
) => css`
* {
// EuiListGroupItem changes the links font-weight, we need to override it
font-weight: ${euiTheme.font.weight.regular};
}
&.solutionSideNavItem--isSelected {
background-color: ${transparentize(euiTheme.colors.lightShade, 0.5)};
background-color: ${selectedAppearance === 'primary'
? euiTheme.colors.backgroundBasePrimary
: transparentize(euiTheme.colors.lightShade, 0.5)};
& * {
font-weight: ${euiTheme.font.weight.medium};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

import React from 'react';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { SolutionSideNav, type SolutionSideNavProps } from './solution_side_nav';
import { SolutionSideNav } from './solution_side_nav';
import type { SolutionSideNavProps } from './solution_side_nav';
import type { SolutionSideNavItem } from './types';
import { METRIC_TYPE } from '@kbn/analytics';
import { TELEMETRY_EVENT } from './telemetry/const';
Expand Down Expand Up @@ -169,4 +170,177 @@ describe('SolutionSideNav', () => {
expect(result.getByText('Users')).toBeInTheDocument();
});
});

describe('unifiedRow interaction variant', () => {
const firstChildOnClick = jest.fn((ev: { preventDefault: () => void }) => {
ev.preventDefault();
});
const panelItems: SolutionSideNavItem[] = [
{
id: 'dashboardsLanding',
label: 'Dashboards',
href: '/dashboards',
items: [
{
id: 'overview',
label: 'Overview',
href: '/overview-first',
onClick: firstChildOnClick,
description: 'Overview description',
},
],
},
{
id: 'alerts',
label: 'Alerts',
href: '/alerts',
},
{
id: 'Rules',
label: 'Rules',
href: '/rules',
items: [
{
id: 'rulesManagement',
label: 'Rules Management',
href: '/rules-management',
description: 'Rules Management description',
},
],
},
];

beforeEach(() => {
firstChildOnClick.mockClear();
});

it('renders arrow hint instead of split panel button when item has children', () => {
const result = renderNav({
items: panelItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
expect(
result.queryByTestId('solutionSideNavItemButton-dashboardsLanding')
).not.toBeInTheDocument();
expect(
result.getByTestId('solutionSideNavItemPanelHint-dashboardsLanding')
).toBeInTheDocument();
});

it('uses first child href on the parent row link', () => {
const result = renderNav({
items: panelItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
expect(
result.getByTestId('solutionSideNavItemLink-dashboardsLanding').getAttribute('href')
).toBe('/overview-first');
});

it('clicking the parent row opens the panel', async () => {
const result = renderNav({
items: panelItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
await userEvent.click(result.getByTestId('solutionSideNavItemLink-dashboardsLanding'));
expect(result.getByTestId('solutionSideNavPanel')).toBeInTheDocument();
expect(result.getByText('Overview')).toBeInTheDocument();
expect(mockTrack).toHaveBeenCalledWith(
METRIC_TYPE.CLICK,
`${TELEMETRY_EVENT.PANEL_NAVIGATION_TOGGLE}dashboardsLanding`
);
});

it('should not show panel button in unifiedRow variant', () => {
const result = renderNav({
items: panelItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
// Panel button should not exist for items with children in unifiedRow mode
expect(
result.queryByTestId('solutionSideNavItemButton-dashboardsLanding')
).not.toBeInTheDocument();
expect(result.queryByTestId('solutionSideNavItemButton-Rules')).not.toBeInTheDocument();
});

it('should navigate directly when clicking item without children in unifiedRow mode', async () => {
const mockOnClick = jest.fn((ev) => {
ev.preventDefault();
});
const itemsWithoutChildren: SolutionSideNavItem[] = [
{
id: 'alerts',
label: 'Alerts',
href: '/alerts',
onClick: mockOnClick,
},
];
const result = renderNav({
items: itemsWithoutChildren,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
await userEvent.click(result.getByTestId('solutionSideNavItemLink-alerts'));
expect(mockOnClick).toHaveBeenCalled();
});
});

describe('navLinkInteractionVariant attribute', () => {
it('should use splitButton as default variant', () => {
const result = renderNav();
// In splitButton mode, panel button should be visible for items with children
expect(result.getByTestId('solutionSideNavItemButton-dashboardsLanding')).toBeInTheDocument();
});

it('should apply unifiedRow variant correctly', () => {
const result = renderNav({
items: mockItems,
navLinkInteractionVariant: 'unifiedRow',
selectedId: 'alerts',
});
// In unifiedRow mode, arrow hint should show instead of button
expect(
result.getByTestId('solutionSideNavItemPanelHint-dashboardsLanding')
).toBeInTheDocument();
expect(
result.queryByTestId('solutionSideNavItemButton-dashboardsLanding')
).not.toBeInTheDocument();
});

it('should switch between variants correctly', () => {
const { rerender } = render(
<SolutionSideNav
items={mockItems}
selectedId={'alerts'}
navLinkInteractionVariant="splitButton"
tracker={mockTrack}
/>
);

// Check splitButton mode
expect(screen.getByTestId('solutionSideNavItemButton-dashboardsLanding')).toBeInTheDocument();

// Re-render with unifiedRow
rerender(
<SolutionSideNav
items={mockItems}
selectedId={'alerts'}
navLinkInteractionVariant="unifiedRow"
tracker={mockTrack}
/>
);

// Check unifiedRow mode
expect(
screen.getByTestId('solutionSideNavItemPanelHint-dashboardsLanding')
).toBeInTheDocument();
expect(
screen.queryByTestId('solutionSideNavItemButton-dashboardsLanding')
).not.toBeInTheDocument();
});
});
});
Loading
Loading