Skip to content

Commit a20f1e9

Browse files
Constancekibanamachine
authored andcommitted
[App Search] DRY helper for encoding/decoding routes that can have special characters in params (#89811)
* Add encodePathParam helper + update components that need it - Primarily document URLs & analytics queries (which uses generateEnginePath) * Add useDecodedParams helper + update components that need it - Documents titles & Analytics queries * [Misc] Change popout icon to eye - Feedback from Davey - the pages don't open in a new window, so shouldn't use the popout icon - Not strictly related but since we're touching these links anyway, I'm shoving it in (sorry) * Remove document detail decode test - now handled/tested by useDecodedParams helper * Add new generateEncodedPath helper - Should be used in place of generatePath * Update all instances of generatePath to generateEncodedPath for consistency across the App Search codebase * Fix failing tests due to extra encodeURI() done by generatePath * Add missing branch test for analytics query titles Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent 15c5b00 commit a20f1e9

12 files changed

Lines changed: 125 additions & 30 deletions

File tree

x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,20 @@ export const mockLocation = {
2424
state: {},
2525
};
2626

27-
jest.mock('react-router-dom', () => ({
28-
...(jest.requireActual('react-router-dom') as object),
29-
useHistory: jest.fn(() => mockHistory),
30-
useLocation: jest.fn(() => mockLocation),
31-
useParams: jest.fn(() => ({})),
32-
}));
27+
jest.mock('react-router-dom', () => {
28+
const originalModule = jest.requireActual('react-router-dom');
29+
return {
30+
...originalModule,
31+
useHistory: jest.fn(() => mockHistory),
32+
useLocation: jest.fn(() => mockLocation),
33+
useParams: jest.fn(() => ({})),
34+
// Note: RR's generatePath() opinionatedly encodeURI()s paths (although this doesn't actually
35+
// show up/affect the final browser URL). Since we already have a generateEncodedPath helper &
36+
// RR is removing this behavior in history 5.0+, I'm mocking tests to remove the extra encoding
37+
// for now to make reading generateEncodedPath URLs a little less of a pain
38+
generatePath: jest.fn((path, params) => decodeURI(originalModule.generatePath(path, params))),
39+
};
40+
});
3341

3442
/**
3543
* For example usage, @see public/applications/shared/react_router_helpers/eui_link.test.tsx

x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts

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

7-
import { generatePath } from 'react-router-dom';
7+
import { generateEncodedPath } from '../utils/encode_path_params';
88

99
export const mockEngineValues = {
1010
engineName: 'some-engine',
1111
engine: {},
1212
};
1313

1414
export const mockGenerateEnginePath = jest.fn((path, pathParams = {}) =>
15-
generatePath(path, { engineName: mockEngineValues.engineName, ...pathParams })
15+
generateEncodedPath(path, { engineName: mockEngineValues.engineName, ...pathParams })
1616
);
1717

1818
jest.mock('../components/engine', () => ({

x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const ACTIONS_COLUMN = {
5656
{ defaultMessage: 'View query analytics' }
5757
),
5858
type: 'icon',
59-
icon: 'popout',
59+
icon: 'eye',
6060
color: 'primary',
6161
onClick: (item: Query | RecentQuery) => {
6262
const { navigateToUrl } = KibanaLogic.values;

x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ import { shallow } from 'enzyme';
1313

1414
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
1515

16+
import { AnalyticsLayout } from '../analytics_layout';
1617
import { AnalyticsCards, AnalyticsChart, QueryClicksTable } from '../components';
1718
import { QueryDetail } from './';
1819

1920
describe('QueryDetail', () => {
2021
const mockBreadcrumbs = ['Engines', 'some-engine', 'Analytics'];
2122

2223
beforeEach(() => {
23-
(useParams as jest.Mock).mockReturnValueOnce({ query: 'some-query' });
24+
(useParams as jest.Mock).mockReturnValue({ query: 'some-query' });
2425

2526
setMockValues({
2627
totalQueriesForQuery: 100,
@@ -31,6 +32,7 @@ describe('QueryDetail', () => {
3132
it('renders', () => {
3233
const wrapper = shallow(<QueryDetail breadcrumbs={mockBreadcrumbs} />);
3334

35+
expect(wrapper.find(AnalyticsLayout).prop('title')).toEqual('"some-query"');
3436
expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([
3537
'Engines',
3638
'some-engine',
@@ -43,4 +45,11 @@ describe('QueryDetail', () => {
4345
expect(wrapper.find(AnalyticsChart)).toHaveLength(1);
4446
expect(wrapper.find(QueryClicksTable)).toHaveLength(1);
4547
});
48+
49+
it('renders empty "" search titles correctly', () => {
50+
(useParams as jest.Mock).mockReturnValue({ query: '""' });
51+
const wrapper = shallow(<QueryDetail breadcrumbs={mockBreadcrumbs} />);
52+
53+
expect(wrapper.find(AnalyticsLayout).prop('title')).toEqual('""');
54+
});
4655
});

x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
*/
66

77
import React from 'react';
8-
import { useParams } from 'react-router-dom';
98
import { useValues } from 'kea';
109

1110
import { i18n } from '@kbn/i18n';
1211
import { EuiSpacer } from '@elastic/eui';
1312

1413
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
1514
import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
15+
import { useDecodedParams } from '../../../utils/encode_path_params';
1616

1717
import { AnalyticsLayout } from '../analytics_layout';
1818
import { AnalyticsSection, QueryClicksTable } from '../components';
@@ -27,14 +27,15 @@ interface Props {
2727
breadcrumbs: BreadcrumbTrail;
2828
}
2929
export const QueryDetail: React.FC<Props> = ({ breadcrumbs }) => {
30-
const { query } = useParams() as { query: string };
30+
const { query } = useDecodedParams();
31+
const queryTitle = query === '""' ? query : `"${query}"`;
3132

3233
const { totalQueriesForQuery, queriesPerDayForQuery, startDate, topClicksForQuery } = useValues(
3334
AnalyticsLogic
3435
);
3536

3637
return (
37-
<AnalyticsLayout isQueryView title={`"${query}"`}>
38+
<AnalyticsLayout isQueryView title={queryTitle}>
3839
<SetPageChrome trail={[...breadcrumbs, QUERY_DETAIL_TITLE, query]} />
3940

4041
<AnalyticsCards

x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,4 @@ describe('DocumentDetail', () => {
104104

105105
expect(actions.deleteDocument).toHaveBeenCalledWith('1');
106106
});
107-
108-
it('correctly decodes document IDs', () => {
109-
(useParams as jest.Mock).mockReturnValueOnce({ documentId: 'hello%20world%20%26%3F!' });
110-
const wrapper = shallow(<DocumentDetail engineBreadcrumb={['test']} />);
111-
expect(wrapper.find('h1').text()).toEqual('Document: hello world &?!');
112-
});
113107
});

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { i18n } from '@kbn/i18n';
2323
import { Loading } from '../../../shared/loading';
2424
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
2525
import { FlashMessages } from '../../../shared/flash_messages';
26+
import { useDecodedParams } from '../../utils/encode_path_params';
2627
import { ResultFieldValue } from '../result';
2728

2829
import { DocumentDetailLogic } from './document_detail_logic';
@@ -43,6 +44,7 @@ export const DocumentDetail: React.FC<Props> = ({ engineBreadcrumb }) => {
4344
const { deleteDocument, getDocumentDetails, setFields } = useActions(DocumentDetailLogic);
4445

4546
const { documentId } = useParams() as { documentId: string };
47+
const { documentId: documentTitle } = useDecodedParams();
4648

4749
useEffect(() => {
4850
getDocumentDetails(documentId);
@@ -74,13 +76,11 @@ export const DocumentDetail: React.FC<Props> = ({ engineBreadcrumb }) => {
7476

7577
return (
7678
<>
77-
<SetPageChrome
78-
trail={[...engineBreadcrumb, DOCUMENTS_TITLE, decodeURIComponent(documentId)]}
79-
/>
79+
<SetPageChrome trail={[...engineBreadcrumb, DOCUMENTS_TITLE, documentTitle]} />
8080
<EuiPageHeader>
8181
<EuiPageHeaderSection>
8282
<EuiTitle size="l">
83-
<h1>{DOCUMENT_DETAIL_TITLE(decodeURIComponent(documentId))}</h1>
83+
<h1>{DOCUMENT_DETAIL_TITLE(documentTitle)}</h1>
8484
</EuiTitle>
8585
</EuiPageHeaderSection>
8686
<EuiPageHeaderSection>

x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/utils.ts

Lines changed: 2 additions & 2 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 { generatePath } from 'react-router-dom';
7+
import { generateEncodedPath } from '../../utils/encode_path_params';
88

99
import { EngineLogic } from './';
1010

@@ -13,5 +13,5 @@ import { EngineLogic } from './';
1313
*/
1414
export const generateEnginePath = (path: string, pathParams: object = {}) => {
1515
const { engineName } = EngineLogic.values;
16-
return generatePath(path, { engineName, ...pathParams });
16+
return generateEncodedPath(path, { engineName, ...pathParams });
1717
};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
*/
66

77
import React from 'react';
8-
import { generatePath } from 'react-router-dom';
98
import { useActions } from 'kea';
109
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
1110
import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react';
1211
import { i18n } from '@kbn/i18n';
1312

1413
import { TelemetryLogic } from '../../../shared/telemetry';
1514
import { EuiLinkTo } from '../../../shared/react_router_helpers';
15+
import { generateEncodedPath } from '../../utils/encode_path_params';
1616
import { ENGINE_PATH } from '../../routes';
1717

1818
import { ENGINES_PAGE_SIZE } from '../../../../../common/constants';
@@ -41,7 +41,7 @@ export const EnginesTable: React.FC<EnginesTableProps> = ({
4141
const { sendAppSearchTelemetry } = useActions(TelemetryLogic);
4242

4343
const engineLinkProps = (engineName: string) => ({
44-
to: generatePath(ENGINE_PATH, { engineName }),
44+
to: generateEncodedPath(ENGINE_PATH, { engineName }),
4545
onClick: () =>
4646
sendAppSearchTelemetry({
4747
action: 'clicked',

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import React, { useState, useMemo } from 'react';
8-
import { generatePath } from 'react-router-dom';
98
import classNames from 'classnames';
109

1110
import './result.scss';
@@ -14,6 +13,7 @@ import { EuiPanel, EuiIcon } from '@elastic/eui';
1413
import { i18n } from '@kbn/i18n';
1514

1615
import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components';
16+
import { generateEncodedPath } from '../../utils/encode_path_params';
1717
import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes';
1818

1919
import { Schema } from '../../../shared/types';
@@ -52,7 +52,7 @@ export const Result: React.FC<Props> = ({
5252
if (schemaForTypeHighlights) return schemaForTypeHighlights[fieldName];
5353
};
5454

55-
const documentLink = generatePath(ENGINE_DOCUMENT_DETAIL_PATH, {
55+
const documentLink = generateEncodedPath(ENGINE_DOCUMENT_DETAIL_PATH, {
5656
engineName: resultMeta.engine,
5757
documentId: resultMeta.id,
5858
});
@@ -135,7 +135,7 @@ export const Result: React.FC<Props> = ({
135135
{ defaultMessage: 'Visit document details' }
136136
)}
137137
>
138-
<EuiIcon type="popout" />
138+
<EuiIcon type="eye" />
139139
</a>
140140
</ReactRouterHelper>
141141
)}

0 commit comments

Comments
 (0)