Skip to content

Commit dd34af3

Browse files
Constancekibanamachine
andauthored
[Enterprise Search] Update chrome/breadcrumbs to support dynamic/nested breadcrumbs (#79231) (#79508)
* [Setup] Add new stripLeadingSlash util - will be used by upcoming breadcrumb/path logic - rename folder + update references + clean up tests * Update breadcrumb helpers with new useGenerateBreadcrumbs - responsible for generating an array of IBreadcrumb objs with correct react router paths, given an array of breadcrumb text + rename previous generic useBreadcrumbs helper to a more specific useEuiBreadcrumbs (indicates the type of transforming happening) + misc typing updates/improvements * Update SetChrome helpers - to use new useGenerateBreadcrumbs() helper + simplify props - remove `isRoot` and `text` (now just accepts a single `trail` array - an empty trail creates the same effect as isRoot + simplify/improve typing as a result (yay!) - improve docs + useEffect update - update breadcrumbs/titles if `trail` ever changes. This will primarily be most helpful for pages that fetch dynamic data on page load (e.g. a dynamic engineName, groupName, etc.) - note that in the above case trail arrays should probably be wrapped in useMemo() to reduce unnecessary rerenders * Update all instances of SetPageChrome to new props Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent 5236d76 commit dd34af3

19 files changed

Lines changed: 204 additions & 99 deletions

File tree

x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts renamed to x-pack/plugins/enterprise_search/common/strip_slashes/index.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { stripTrailingSlash } from './';
7+
import { stripTrailingSlash, stripLeadingSlash } from './';
88

99
describe('Strip Trailing Slash helper', () => {
10-
it('strips trailing slashes', async () => {
10+
it('strips trailing slashes', () => {
1111
expect(stripTrailingSlash('http://trailing.slash/')).toEqual('http://trailing.slash');
1212
});
1313

14-
it('does nothing is there is no trailing slash', async () => {
14+
it('does nothing if there is no trailing slash', () => {
1515
expect(stripTrailingSlash('http://ok.url')).toEqual('http://ok.url');
1616
});
1717
});
18+
19+
describe('Strip Leading Slash helper', () => {
20+
it('strips leading slashes', () => {
21+
expect(stripLeadingSlash('/some/long/path/')).toEqual('some/long/path/');
22+
});
23+
24+
it('does nothing if there is no trailing slash', () => {
25+
expect(stripLeadingSlash('ok')).toEqual('ok');
26+
});
27+
});

x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts renamed to x-pack/plugins/enterprise_search/common/strip_slashes/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
*/
66

77
/**
8-
* Small helper for stripping trailing slashes from URLs or paths
8+
* Helpers for stripping trailing or leading slashes from URLs or paths
99
* (usually ones that come in from React Router or API endpoints)
1010
*/
11+
1112
export const stripTrailingSlash = (url: string): string => {
1213
return url && url.endsWith('/') ? url.slice(0, -1) : url;
1314
};
15+
16+
export const stripLeadingSlash = (path: string): string => {
17+
return path && path.startsWith('/') ? path.substring(1) : path;
18+
};

x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const EmptyState: React.FC = () => {
3636

3737
return (
3838
<>
39-
<SetPageChrome isRoot />
39+
<SetPageChrome />
4040
<EngineOverviewHeader />
4141
<EuiPageContent className="emptyState">
4242
<EuiEmptyPrompt

x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/loading_state.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { EngineOverviewHeader } from './header';
1313
export const LoadingState: React.FC = () => {
1414
return (
1515
<>
16-
<SetPageChrome isRoot />
16+
<SetPageChrome />
1717
<EngineOverviewHeader />
1818
<EuiPageContent paddingSize="l">
1919
<EuiLoadingContent lines={5} />

x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const EngineOverview: React.FC = () => {
8585

8686
return (
8787
<>
88-
<SetPageChrome isRoot />
88+
<SetPageChrome />
8989
<SendTelemetry action="viewed" metric="engines_overview" />
9090

9191
<EngineOverviewHeader />

x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemet
1414
export const ErrorConnecting: React.FC = () => {
1515
return (
1616
<>
17-
<SetPageChrome isRoot />
17+
<SetPageChrome />
1818
<SendTelemetry action="error" metric="cannot_connect" />
1919

2020
<EuiPageContent>

x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ export const SetupGuide: React.FC = () => (
2323
elasticsearchNativeAuthLink="https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm"
2424
>
2525
<SetPageChrome
26-
text={i18n.translate('xpack.enterpriseSearch.setupGuide.title', {
27-
defaultMessage: 'Setup Guide',
28-
})}
26+
trail={[
27+
i18n.translate('xpack.enterpriseSearch.setupGuide.title', {
28+
defaultMessage: 'Setup Guide',
29+
}),
30+
]}
2931
/>
3032
<SendTelemetry action="viewed" metric="setup_guide" />
3133

x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const ProductSelector: React.FC<IProductSelectorProps> = ({ access }) =>
5151

5252
return (
5353
<EuiPage restrictWidth className="enterpriseSearchOverview">
54-
<SetPageChrome isRoot />
54+
<SetPageChrome />
5555
<SendTelemetry action="viewed" metric="overview" />
5656

5757
<EuiPageBody>

x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ export const SetupGuide: React.FC = () => (
2323
elasticsearchNativeAuthLink="https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm"
2424
>
2525
<SetPageChrome
26-
text={i18n.translate('xpack.enterpriseSearch.setupGuide.title', {
27-
defaultMessage: 'Setup Guide',
28-
})}
26+
trail={[
27+
i18n.translate('xpack.enterpriseSearch.setupGuide.title', {
28+
defaultMessage: 'Setup Guide',
29+
}),
30+
]}
2931
/>
3032
<SendTelemetry action="viewed" metric="setup_guide" />
3133

x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import '../../__mocks__/kea.mock';
7+
import { setMockValues } from '../../__mocks__/kea.mock';
88
import { mockKibanaValues, mockHistory } from '../../__mocks__';
99

1010
jest.mock('../react_router_helpers', () => ({
@@ -14,19 +14,64 @@ jest.mock('../react_router_helpers', () => ({
1414
import { letBrowserHandleEvent } from '../react_router_helpers';
1515

1616
import {
17-
useBreadcrumbs,
17+
useGenerateBreadcrumbs,
18+
useEuiBreadcrumbs,
1819
useEnterpriseSearchBreadcrumbs,
1920
useAppSearchBreadcrumbs,
2021
useWorkplaceSearchBreadcrumbs,
2122
} from './generate_breadcrumbs';
2223

23-
describe('useBreadcrumbs', () => {
24+
describe('useGenerateBreadcrumbs', () => {
25+
const mockCurrentPath = (pathname: string) =>
26+
setMockValues({ history: { location: { pathname } } });
27+
28+
afterAll(() => {
29+
setMockValues({ history: mockHistory });
30+
});
31+
32+
it('accepts a trail of breadcrumb text and generates IBreadcrumb objs based on the current routing path', () => {
33+
const trail = ['Groups', 'Example Group Name', 'Source Prioritization'];
34+
const path = '/groups/{id}/source_prioritization';
35+
36+
mockCurrentPath(path);
37+
const breadcrumbs = useGenerateBreadcrumbs(trail);
38+
39+
expect(breadcrumbs).toEqual([
40+
{ text: 'Groups', path: '/groups' },
41+
{ text: 'Example Group Name', path: '/groups/{id}' },
42+
{ text: 'Source Prioritization', path: '/groups/{id}/source_prioritization' },
43+
]);
44+
});
45+
46+
it('handles empty arrays gracefully', () => {
47+
mockCurrentPath('');
48+
expect(useGenerateBreadcrumbs([])).toEqual([]);
49+
});
50+
51+
it('attempts to handle mismatched trail/path lengths gracefully', () => {
52+
mockCurrentPath('/page1/page2');
53+
expect(useGenerateBreadcrumbs(['Page 1', 'Page 2', 'Page 3'])).toEqual([
54+
{ text: 'Page 1', path: '/page1' },
55+
{ text: 'Page 2', path: '/page1/page2' },
56+
{ text: 'Page 3' }, // The missing path falls back to breadcrumb text w/ no link
57+
]);
58+
59+
mockCurrentPath('/page1/page2/page3');
60+
expect(useGenerateBreadcrumbs(['Page 1', 'Page 2'])).toEqual([
61+
{ text: 'Page 1', path: '/page1' },
62+
{ text: 'Page 2', path: '/page1/page2' },
63+
// the /page3 path is ignored/not used
64+
]);
65+
});
66+
});
67+
68+
describe('useEuiBreadcrumbs', () => {
2469
beforeEach(() => {
2570
jest.clearAllMocks();
2671
});
2772

2873
it('accepts an array of breadcrumbs and to the array correctly injects SPA link navigation props', () => {
29-
const breadcrumb = useBreadcrumbs([
74+
const breadcrumb = useEuiBreadcrumbs([
3075
{
3176
text: 'Hello',
3277
path: '/hello',
@@ -51,7 +96,7 @@ describe('useBreadcrumbs', () => {
5196
});
5297

5398
it('prevents default navigation and uses React Router history on click', () => {
54-
const breadcrumb = useBreadcrumbs([{ text: '', path: '/test' }])[0] as any;
99+
const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/test' }])[0] as any;
55100

56101
expect(breadcrumb.href).toEqual('/app/enterprise_search/test');
57102
expect(mockHistory.createHref).toHaveBeenCalled();
@@ -64,7 +109,7 @@ describe('useBreadcrumbs', () => {
64109
});
65110

66111
it('does not call createHref if shouldNotCreateHref is passed', () => {
67-
const breadcrumb = useBreadcrumbs([
112+
const breadcrumb = useEuiBreadcrumbs([
68113
{ text: '', path: '/test', shouldNotCreateHref: true },
69114
])[0] as any;
70115

@@ -73,7 +118,7 @@ describe('useBreadcrumbs', () => {
73118
});
74119

75120
it('does not prevent default browser behavior on new tab/window clicks', () => {
76-
const breadcrumb = useBreadcrumbs([{ text: '', path: '/' }])[0] as any;
121+
const breadcrumb = useEuiBreadcrumbs([{ text: '', path: '/' }])[0] as any;
77122

78123
(letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true);
79124
breadcrumb.onClick();
@@ -82,7 +127,7 @@ describe('useBreadcrumbs', () => {
82127
});
83128

84129
it('does not generate link behavior if path is excluded', () => {
85-
const breadcrumb = useBreadcrumbs([{ text: 'Unclickable breadcrumb' }])[0];
130+
const breadcrumb = useEuiBreadcrumbs([{ text: 'Unclickable breadcrumb' }])[0];
86131

87132
expect(breadcrumb.href).toBeUndefined();
88133
expect(breadcrumb.onClick).toBeUndefined();

0 commit comments

Comments
 (0)