Skip to content

Commit a9e64ab

Browse files
authored
[App Search] Updated Search UI to new URL (#101320)
1 parent 59dae3d commit a9e64ab

File tree

10 files changed

+169
-29
lines changed

10 files changed

+169
-29
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { generateEncodedPath } from '../utils/encode_path_params';
1212
export const mockEngineValues = {
1313
engineName: 'some-engine',
1414
engine: {} as EngineDetails,
15+
searchKey: 'search-abc123',
1516
};
1617

1718
export const mockEngineActions = {

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { LogicMounter, mockHttpValues } from '../../../__mocks__';
99

1010
import { nextTick } from '@kbn/test/jest';
1111

12+
import { ApiTokenTypes } from '../credentials/constants';
13+
1214
import { EngineTypes } from './types';
1315

1416
import { EngineLogic } from './';
@@ -47,6 +49,7 @@ describe('EngineLogic', () => {
4749
hasSchemaConflicts: false,
4850
hasUnconfirmedSchemaFields: false,
4951
engineNotFound: false,
52+
searchKey: '',
5053
};
5154

5255
beforeEach(() => {
@@ -263,5 +266,57 @@ describe('EngineLogic', () => {
263266
});
264267
});
265268
});
269+
270+
describe('searchKey', () => {
271+
it('should select the first available search key for this engine', () => {
272+
const engine = {
273+
...mockEngineData,
274+
apiTokens: [
275+
{
276+
key: 'private-123xyz',
277+
name: 'privateKey',
278+
type: ApiTokenTypes.Private,
279+
},
280+
{
281+
key: 'search-123xyz',
282+
name: 'searchKey',
283+
type: ApiTokenTypes.Search,
284+
},
285+
{
286+
key: 'search-8910abc',
287+
name: 'searchKey2',
288+
type: ApiTokenTypes.Search,
289+
},
290+
],
291+
};
292+
mount({ engine });
293+
294+
expect(EngineLogic.values).toEqual({
295+
...DEFAULT_VALUES,
296+
engine,
297+
searchKey: 'search-123xyz',
298+
});
299+
});
300+
301+
it('should return an empty string if none are available', () => {
302+
const engine = {
303+
...mockEngineData,
304+
apiTokens: [
305+
{
306+
key: 'private-123xyz',
307+
name: 'privateKey',
308+
type: ApiTokenTypes.Private,
309+
},
310+
],
311+
};
312+
mount({ engine });
313+
314+
expect(EngineLogic.values).toEqual({
315+
...DEFAULT_VALUES,
316+
engine,
317+
searchKey: '',
318+
});
319+
});
320+
});
266321
});
267322
});

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import { kea, MakeLogicType } from 'kea';
99

1010
import { HttpLogic } from '../../../shared/http';
11+
import { ApiTokenTypes } from '../credentials/constants';
12+
import { ApiToken } from '../credentials/types';
1113

1214
import { EngineDetails, EngineTypes } from './types';
1315

@@ -21,6 +23,7 @@ interface EngineValues {
2123
hasSchemaConflicts: boolean;
2224
hasUnconfirmedSchemaFields: boolean;
2325
engineNotFound: boolean;
26+
searchKey: string;
2427
}
2528

2629
interface EngineActions {
@@ -87,6 +90,14 @@ export const EngineLogic = kea<MakeLogicType<EngineValues, EngineActions>>({
8790
() => [selectors.engine],
8891
(engine) => engine?.unconfirmedFields?.length > 0,
8992
],
93+
searchKey: [
94+
() => [selectors.engine],
95+
(engine: Partial<EngineDetails>) => {
96+
const isSearchKey = (token: ApiToken) => token.type === ApiTokenTypes.Search;
97+
const searchKey = (engine.apiTokens || []).find(isSearchKey);
98+
return searchKey?.key || '';
99+
},
100+
],
90101
}),
91102
listeners: ({ actions, values }) => ({
92103
initializeEngine: async () => {

x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/search_ui_form.test.tsx

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import { setMockValues, setMockActions } from '../../../../__mocks__';
1313

1414
import React from 'react';
1515

16-
import { shallow } from 'enzyme';
16+
import { shallow, ShallowWrapper } from 'enzyme';
17+
18+
import { EuiForm } from '@elastic/eui';
1719

1820
import { ActiveField } from '../types';
1921
import { generatePreviewUrl } from '../utils';
@@ -29,6 +31,7 @@ describe('SearchUIForm', () => {
2931
urlField: 'url',
3032
facetFields: ['category'],
3133
sortFields: ['size'],
34+
dataLoading: false,
3235
};
3336
const actions = {
3437
onActiveFieldChange: jest.fn(),
@@ -43,10 +46,6 @@ describe('SearchUIForm', () => {
4346
setMockActions(actions);
4447
});
4548

46-
beforeEach(() => {
47-
jest.clearAllMocks();
48-
});
49-
5049
it('renders', () => {
5150
const wrapper = shallow(<SearchUIForm />);
5251
expect(wrapper.find('[data-test-subj="selectTitle"]').exists()).toBe(true);
@@ -56,6 +55,7 @@ describe('SearchUIForm', () => {
5655
});
5756

5857
describe('title field', () => {
58+
beforeEach(() => jest.clearAllMocks());
5959
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectTitle"]');
6060

6161
it('renders with its value set from state', () => {
@@ -84,6 +84,7 @@ describe('SearchUIForm', () => {
8484
});
8585

8686
describe('url field', () => {
87+
beforeEach(() => jest.clearAllMocks());
8788
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectUrl"]');
8889

8990
it('renders with its value set from state', () => {
@@ -112,6 +113,7 @@ describe('SearchUIForm', () => {
112113
});
113114

114115
describe('filters field', () => {
116+
beforeEach(() => jest.clearAllMocks());
115117
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectFilters"]');
116118

117119
it('renders with its value set from state', () => {
@@ -145,6 +147,7 @@ describe('SearchUIForm', () => {
145147
});
146148

147149
describe('sorts field', () => {
150+
beforeEach(() => jest.clearAllMocks());
148151
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectSort"]');
149152

150153
it('renders with its value set from state', () => {
@@ -177,26 +180,61 @@ describe('SearchUIForm', () => {
177180
});
178181
});
179182

180-
it('includes a link to generate the preview', () => {
181-
(generatePreviewUrl as jest.Mock).mockReturnValue('http://www.example.com?foo=bar');
183+
describe('generate preview button', () => {
184+
let wrapper: ShallowWrapper;
182185

183-
setMockValues({
184-
...values,
185-
urlField: 'foo',
186-
titleField: 'bar',
187-
facetFields: ['baz'],
188-
sortFields: ['qux'],
186+
beforeAll(() => {
187+
jest.clearAllMocks();
188+
(generatePreviewUrl as jest.Mock).mockReturnValue('http://www.example.com?foo=bar');
189+
setMockValues({
190+
...values,
191+
urlField: 'foo',
192+
titleField: 'bar',
193+
facetFields: ['baz'],
194+
sortFields: ['qux'],
195+
searchKey: 'search-123abc',
196+
});
197+
wrapper = shallow(<SearchUIForm />);
198+
});
199+
200+
it('should be a submit button', () => {
201+
expect(wrapper.find('[data-test-subj="generateSearchUiPreview"]').prop('type')).toBe(
202+
'submit'
203+
);
204+
});
205+
206+
it('should be wrapped in a form configured to POST to the preview screen in a new tab', () => {
207+
const form = wrapper.find(EuiForm);
208+
expect(generatePreviewUrl).toHaveBeenCalledWith({
209+
urlField: 'foo',
210+
titleField: 'bar',
211+
facets: ['baz'],
212+
sortFields: ['qux'],
213+
});
214+
expect(form.prop('action')).toBe('http://www.example.com?foo=bar');
215+
expect(form.prop('target')).toBe('_blank');
216+
expect(form.prop('method')).toBe('POST');
217+
expect(form.prop('component')).toBe('form');
189218
});
190219

191-
const subject = () =>
192-
shallow(<SearchUIForm />).find('[data-test-subj="generateSearchUiPreview"]');
220+
it('should include a searchKey in that form POST', () => {
221+
const form = wrapper.find(EuiForm);
222+
const hiddenInput = form.find('input[type="hidden"]');
223+
expect(hiddenInput.prop('id')).toBe('searchKey');
224+
expect(hiddenInput.prop('value')).toBe('search-123abc');
225+
});
226+
});
193227

194-
expect(subject().prop('href')).toBe('http://www.example.com?foo=bar');
195-
expect(generatePreviewUrl).toHaveBeenCalledWith({
196-
urlField: 'foo',
197-
titleField: 'bar',
198-
facets: ['baz'],
199-
sortFields: ['qux'],
228+
it('should disable everything while data is loading', () => {
229+
setMockValues({
230+
...values,
231+
dataLoading: true,
200232
});
233+
const wrapper = shallow(<SearchUIForm />);
234+
expect(wrapper.find('[data-test-subj="selectTitle"]').prop('disabled')).toBe(true);
235+
expect(wrapper.find('[data-test-subj="selectFilters"]').prop('isDisabled')).toBe(true);
236+
expect(wrapper.find('[data-test-subj="selectSort"]').prop('isDisabled')).toBe(true);
237+
expect(wrapper.find('[data-test-subj="selectUrl"]').prop('disabled')).toBe(true);
238+
expect(wrapper.find('[data-test-subj="generateSearchUiPreview"]').prop('disabled')).toBe(true);
201239
});
202240
});

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useValues, useActions } from 'kea';
1111

1212
import { EuiForm, EuiFormRow, EuiSelect, EuiComboBox, EuiButton } from '@elastic/eui';
1313

14+
import { EngineLogic } from '../../engine';
1415
import {
1516
TITLE_FIELD_LABEL,
1617
TITLE_FIELD_HELP_TEXT,
@@ -27,7 +28,9 @@ import { ActiveField } from '../types';
2728
import { generatePreviewUrl } from '../utils';
2829

2930
export const SearchUIForm: React.FC = () => {
31+
const { searchKey } = useValues(EngineLogic);
3032
const {
33+
dataLoading,
3134
validFields,
3235
validSortFields,
3336
validFacetFields,
@@ -70,9 +73,11 @@ export const SearchUIForm: React.FC = () => {
7073
const selectedFacetOptions = formatMultiOptions(facetFields);
7174

7275
return (
73-
<EuiForm>
76+
<EuiForm component="form" action={previewHref} target="_blank" method="POST">
77+
<input type="hidden" id="searchKey" name="searchKey" value={searchKey} />
7478
<EuiFormRow label={TITLE_FIELD_LABEL} helpText={TITLE_FIELD_HELP_TEXT} fullWidth>
7579
<EuiSelect
80+
disabled={dataLoading}
7681
options={optionFields}
7782
value={selectedTitleOption && selectedTitleOption.value}
7883
onChange={(e) => onTitleFieldChange(e.target.value)}
@@ -85,6 +90,7 @@ export const SearchUIForm: React.FC = () => {
8590
</EuiFormRow>
8691
<EuiFormRow label={FILTER_FIELD_LABEL} helpText={FILTER_FIELD_HELP_TEXT} fullWidth>
8792
<EuiComboBox
93+
isDisabled={dataLoading}
8894
options={facetOptionFields}
8995
selectedOptions={selectedFacetOptions}
9096
onChange={(newValues) => onFacetFieldsChange(newValues.map((field) => field.value!))}
@@ -96,6 +102,7 @@ export const SearchUIForm: React.FC = () => {
96102
</EuiFormRow>
97103
<EuiFormRow label={SORT_FIELD_LABEL} helpText={SORT_FIELD_HELP_TEXT} fullWidth>
98104
<EuiComboBox
105+
isDisabled={dataLoading}
99106
options={sortOptionFields}
100107
selectedOptions={selectedSortOptions}
101108
onChange={(newValues) => onSortFieldsChange(newValues.map((field) => field.value!))}
@@ -108,6 +115,7 @@ export const SearchUIForm: React.FC = () => {
108115

109116
<EuiFormRow label={URL_FIELD_LABEL} helpText={URL_FIELD_HELP_TEXT} fullWidth>
110117
<EuiSelect
118+
disabled={dataLoading}
111119
options={optionFields}
112120
value={selectedURLOption && selectedURLOption.value}
113121
onChange={(e) => onUrlFieldChange(e.target.value)}
@@ -119,8 +127,8 @@ export const SearchUIForm: React.FC = () => {
119127
/>
120128
</EuiFormRow>
121129
<EuiButton
122-
href={previewHref}
123-
target="_blank"
130+
disabled={dataLoading}
131+
type="submit"
124132
fill
125133
iconType="popout"
126134
iconSide="right"

x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/i18n.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import { i18n } from '@kbn/i18n';
99

10+
import { CREDENTIALS_TITLE } from '../credentials';
11+
1012
export const SEARCH_UI_TITLE = i18n.translate(
1113
'xpack.enterpriseSearch.appSearch.engine.searchUI.title',
1214
{ defaultMessage: 'Search UI' }
@@ -48,3 +50,9 @@ export const GENERATE_PREVIEW_BUTTON_LABEL = i18n.translate(
4850
'xpack.enterpriseSearch.appSearch.engine.searchUI.generatePreviewButtonLabel',
4951
{ defaultMessage: 'Generate search experience' }
5052
);
53+
export const NO_SEARCH_KEY_ERROR = (engineName: string) =>
54+
i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.noSearchKeyErrorMessage', {
55+
defaultMessage:
56+
"It looks like you don't have any Public Search Keys with access to the '{engineName}' engine. Please visit the {credentialsTitle} page to set one up.",
57+
values: { engineName, credentialsTitle: CREDENTIALS_TITLE },
58+
});

x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { SearchUILogic } from './';
1818
describe('SearchUILogic', () => {
1919
const { mount } = new LogicMounter(SearchUILogic);
2020
const { http } = mockHttpValues;
21-
const { flashAPIErrors } = mockFlashMessageHelpers;
21+
const { flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
2222

2323
const DEFAULT_VALUES = {
2424
dataLoading: true,
@@ -35,6 +35,7 @@ describe('SearchUILogic', () => {
3535
beforeEach(() => {
3636
jest.clearAllMocks();
3737
mockEngineValues.engineName = 'engine1';
38+
mockEngineValues.searchKey = 'search-abc123';
3839
});
3940

4041
it('has expected default values', () => {
@@ -155,6 +156,17 @@ describe('SearchUILogic', () => {
155156
});
156157
});
157158

159+
it('will short circuit the call if there is no searchKey available for this engine', async () => {
160+
mockEngineValues.searchKey = '';
161+
mount();
162+
163+
SearchUILogic.actions.loadFieldData();
164+
165+
expect(setErrorMessage).toHaveBeenCalledWith(
166+
"It looks like you don't have any Public Search Keys with access to the 'engine1' engine. Please visit the Credentials page to set one up."
167+
);
168+
});
169+
158170
it('handles errors', async () => {
159171
http.get.mockReturnValueOnce(Promise.reject('error'));
160172
mount();

0 commit comments

Comments
 (0)