[Enterprise Search] Support active nav links that have both subnav & non-subnav child routes (#103036)

* Update generateNavlink to take an `items` subNav and use it to determine isSelected

+ change getNavLinkActive to early returns
+ tweak tests for readability

* Update WS nav Sources link
- to show active on creation routes but not on single source routes

* Update AS nav Engines link
- should eventually show active on creation routes but not on single engine routes

* Update AS engine creation routing
- so that it correctly shows as a child route of the Engines link

+ update breadcrumbs
This commit is contained in:
Constance 2021-06-23 10:22:04 -07:00 committed by GitHub
parent 3864fe1559
commit 045a32b054
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 85 additions and 28 deletions

View file

@ -22,6 +22,7 @@ import {
EuiButton,
} from '@elastic/eui';
import { ENGINES_TITLE } from '../engines';
import { AppSearchPageTemplate } from '../layout';
import {
@ -43,7 +44,7 @@ export const EngineCreation: React.FC = () => {
return (
<AppSearchPageTemplate
pageChrome={[ENGINE_CREATION_TITLE]}
pageChrome={[ENGINES_TITLE, ENGINE_CREATION_TITLE]}
pageHeader={{ pageTitle: ENGINE_CREATION_TITLE }}
data-test-subj="EngineCreation"
>

View file

@ -53,7 +53,7 @@ describe('EmptyState', () => {
});
it('sends a user to engine creation', () => {
expect(button.prop('to')).toEqual('/engine_creation');
expect(button.prop('to')).toEqual('/engines/new');
});
});
});

View file

@ -8,7 +8,7 @@
import { setMockValues } from '../../../__mocks__/kea_logic';
jest.mock('../../../shared/layout', () => ({
generateNavLink: jest.fn(({ to }) => ({ href: to })),
generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })),
}));
jest.mock('../engine/engine_nav', () => ({
useEngineNav: () => [],

View file

@ -28,8 +28,12 @@ export const useAppSearchNav = () => {
{
id: 'engines',
name: ENGINES_TITLE,
...generateNavLink({ to: ENGINES_PATH, isRoot: true }),
items: useEngineNav(),
...generateNavLink({
to: ENGINES_PATH,
isRoot: true,
shouldShowActiveForSubroutes: true,
items: useEngineNav(),
}),
},
];

View file

@ -25,6 +25,7 @@ import {
} from '@elastic/eui';
import { AppLogic } from '../../app_logic';
import { ENGINES_TITLE } from '../engines';
import { AppSearchPageTemplate } from '../layout';
import {
@ -73,7 +74,7 @@ export const MetaEngineCreation: React.FC = () => {
return (
<AppSearchPageTemplate
pageChrome={[META_ENGINE_CREATION_TITLE]}
pageChrome={[ENGINES_TITLE, META_ENGINE_CREATION_TITLE]}
pageHeader={{
pageTitle: META_ENGINE_CREATION_TITLE,
description: (

View file

@ -104,9 +104,6 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) =
<Route exact path={ENGINES_PATH}>
<EnginesOverview />
</Route>
<Route path={ENGINE_PATH}>
<EngineRouter />
</Route>
{canManageEngines && (
<Route exact path={ENGINE_CREATION_PATH}>
<EngineCreation />
@ -117,6 +114,9 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) =
<MetaEngineCreation />
</Route>
)}
<Route path={ENGINE_PATH}>
<EngineRouter />
</Route>
{canViewSettings && (
<Route exact path={SETTINGS_PATH}>
<Settings />

View file

@ -18,7 +18,7 @@ export const CREDENTIALS_PATH = '/credentials';
export const ROLE_MAPPINGS_PATH = '/role_mappings';
export const ENGINES_PATH = '/engines';
export const ENGINE_CREATION_PATH = '/engine_creation';
export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; // This is safe from conflicting with an :engineName path because new is a reserved name
export const ENGINE_PATH = `${ENGINES_PATH}/:engineName`;
export const ENGINE_ANALYTICS_PATH = `${ENGINE_PATH}/analytics`;
@ -39,7 +39,7 @@ export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_SCHEMA_PATH}/reindex_job/:reind
export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`;
export const ENGINE_CRAWLER_DOMAIN_PATH = `${ENGINE_CRAWLER_PATH}/domains/:domainId`;
export const META_ENGINE_CREATION_PATH = '/meta_engine_creation';
export const META_ENGINE_CREATION_PATH = `${ENGINES_PATH}/new_meta_engine`; // This is safe from conflicting with an :engineName path because engine names cannot have underscores
export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`;
export const ENGINE_RELEVANCE_TUNING_PATH = `${ENGINE_PATH}/relevance_tuning`;

View file

@ -19,21 +19,23 @@ import { generateNavLink, getNavLinkActive } from './nav_link_helpers';
describe('generateNavLink', () => {
beforeEach(() => {
jest.clearAllMocks();
mockKibanaValues.history.location.pathname = '/current_page';
mockKibanaValues.history.location.pathname = '/';
});
it('generates React Router props & isSelected (active) state for use within an EuiSideNavItem obj', () => {
it('generates React Router props for use within an EuiSideNavItem obj', () => {
const navItem = generateNavLink({ to: '/test' });
expect(navItem.href).toEqual('/app/enterprise_search/test');
expect(navItem).toEqual({
href: '/app/enterprise_search/test',
onClick: expect.any(Function),
isSelected: false,
});
navItem.onClick({} as any);
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test');
expect(navItem.isSelected).toEqual(false);
});
describe('getNavLinkActive', () => {
describe('isSelected / getNavLinkActive', () => {
it('returns true when the current path matches the link path', () => {
mockKibanaValues.history.location.pathname = '/test';
const isSelected = getNavLinkActive({ to: '/test' });
@ -41,6 +43,13 @@ describe('generateNavLink', () => {
expect(isSelected).toEqual(true);
});
it('return false when the current path does not match the link path', () => {
mockKibanaValues.history.location.pathname = '/hello';
const isSelected = getNavLinkActive({ to: '/world' });
expect(isSelected).toEqual(false);
});
describe('isRoot', () => {
it('returns true if the current path is "/"', () => {
mockKibanaValues.history.location.pathname = '/';
@ -58,7 +67,31 @@ describe('generateNavLink', () => {
expect(isSelected).toEqual(true);
});
it('returns false if not', () => {
/* NOTE: This logic is primarily used for the following routing scenario:
* 1. /item/{itemId} shows a child subnav, e.g. /items/{itemId}/settings
* - BUT when the child subnav is open, the parent `Item` nav link should not show as active - its child nav links should
* 2. /item/create_item (example) does *not* show a child subnav
* - BUT the parent `Item` nav link should highlight when on this non-subnav route
*/
it('returns false if subroutes already have their own items subnav (with active state)', () => {
mockKibanaValues.history.location.pathname = '/items/123/settings';
const isSelected = getNavLinkActive({
to: '/items',
shouldShowActiveForSubroutes: true,
items: [{ id: 'settings', name: 'Settings' }],
});
expect(isSelected).toEqual(false);
});
it('returns false if not a valid subroute', () => {
mockKibanaValues.history.location.pathname = '/hello/world';
const isSelected = getNavLinkActive({ to: '/world', shouldShowActiveForSubroutes: true });
expect(isSelected).toEqual(false);
});
it('returns false for subroutes if the flag is not passed', () => {
mockKibanaValues.history.location.pathname = '/hello/world';
const isSelected = getNavLinkActive({ to: '/hello' });
@ -66,4 +99,10 @@ describe('generateNavLink', () => {
});
});
});
it('optionally passes items', () => {
const navItem = generateNavLink({ to: '/test', items: [] });
expect(navItem.items).toEqual([]);
});
});

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { EuiSideNavItemType } from '@elastic/eui';
import { stripTrailingSlash } from '../../../../common/strip_slashes';
import { KibanaLogic } from '../kibana';
@ -14,12 +16,14 @@ interface Params {
to: string;
isRoot?: boolean;
shouldShowActiveForSubroutes?: boolean;
items?: Array<EuiSideNavItemType<unknown>>; // Primarily passed if using `items` to determine isSelected - if not, you can just set `items` outside of this helper
}
export const generateNavLink = ({ to, ...rest }: Params & ReactRouterProps) => {
export const generateNavLink = ({ to, items, ...rest }: Params & ReactRouterProps) => {
return {
...generateReactRouterProps({ to, ...rest }),
isSelected: getNavLinkActive({ to, ...rest }),
isSelected: getNavLinkActive({ to, items, ...rest }),
items,
};
};
@ -27,14 +31,19 @@ export const getNavLinkActive = ({
to,
isRoot = false,
shouldShowActiveForSubroutes = false,
items = [],
}: Params): boolean => {
const { pathname } = KibanaLogic.values.history.location;
const currentPath = stripTrailingSlash(pathname);
const isActive =
currentPath === to ||
(shouldShowActiveForSubroutes && currentPath.startsWith(to)) ||
(isRoot && currentPath === '');
if (currentPath === to) return true;
return isActive;
if (isRoot && currentPath === '') return true;
if (shouldShowActiveForSubroutes) {
if (items.length) return false; // If a nav link has sub-nav items open, never show it as active
if (currentPath.startsWith(to)) return true;
}
return false;
};

View file

@ -7,7 +7,7 @@
jest.mock('../../../shared/layout', () => ({
...jest.requireActual('../../../shared/layout'),
generateNavLink: jest.fn(({ to }) => ({ href: to })),
generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })),
}));
jest.mock('../../views/content_sources/components/source_sub_nav', () => ({
useSourceSubNav: () => [],

View file

@ -33,8 +33,11 @@ export const useWorkplaceSearchNav = () => {
{
id: 'sources',
name: NAV.SOURCES,
...generateNavLink({ to: SOURCES_PATH }),
items: useSourceSubNav(),
...generateNavLink({
to: SOURCES_PATH,
shouldShowActiveForSubroutes: true,
items: useSourceSubNav(),
}),
},
{
id: 'groups',