-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[App Search] Updated Search UI to new URL #101320
Changes from all commits
cf96bf5
6069260
3135796
c224c8c
8c0f0c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,8 @@ | |
import { kea, MakeLogicType } from 'kea'; | ||
|
||
import { HttpLogic } from '../../../shared/http'; | ||
import { ApiTokenTypes } from '../credentials/constants'; | ||
import { ApiToken } from '../credentials/types'; | ||
|
||
import { EngineDetails, EngineTypes } from './types'; | ||
|
||
|
@@ -21,6 +23,7 @@ interface EngineValues { | |
hasSchemaConflicts: boolean; | ||
hasUnconfirmedSchemaFields: boolean; | ||
engineNotFound: boolean; | ||
searchKey: string; | ||
} | ||
|
||
interface EngineActions { | ||
|
@@ -87,6 +90,14 @@ export const EngineLogic = kea<MakeLogicType<EngineValues, EngineActions>>({ | |
() => [selectors.engine], | ||
(engine) => engine?.unconfirmedFields?.length > 0, | ||
], | ||
searchKey: [ | ||
() => [selectors.engine], | ||
(engine: Partial<EngineDetails>) => { | ||
const isSearchKey = (token: ApiToken) => token.type === ApiTokenTypes.Search; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could pull this function out instead of declaring it in here every time (idk if its worth writing a test for it) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going completely the opposite direction, I would have just left it as an inline fn in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haha, quite the array of opinions! I just pulled this out for readability. It was long in the tooth inline in the |
||
const searchKey = (engine.apiTokens || []).find(isSearchKey); | ||
return searchKey?.key || ''; | ||
}, | ||
], | ||
}), | ||
listeners: ({ actions, values }) => ({ | ||
initializeEngine: async () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,9 @@ import { setMockValues, setMockActions } from '../../../../__mocks__'; | |
|
||
import React from 'react'; | ||
|
||
import { shallow } from 'enzyme'; | ||
import { shallow, ShallowWrapper } from 'enzyme'; | ||
|
||
import { EuiForm } from '@elastic/eui'; | ||
|
||
import { ActiveField } from '../types'; | ||
import { generatePreviewUrl } from '../utils'; | ||
|
@@ -29,6 +31,7 @@ describe('SearchUIForm', () => { | |
urlField: 'url', | ||
facetFields: ['category'], | ||
sortFields: ['size'], | ||
dataLoading: false, | ||
}; | ||
const actions = { | ||
onActiveFieldChange: jest.fn(), | ||
|
@@ -43,10 +46,6 @@ describe('SearchUIForm', () => { | |
setMockActions(actions); | ||
}); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
cee-chen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
it('renders', () => { | ||
const wrapper = shallow(<SearchUIForm />); | ||
expect(wrapper.find('[data-test-subj="selectTitle"]').exists()).toBe(true); | ||
|
@@ -56,6 +55,7 @@ describe('SearchUIForm', () => { | |
}); | ||
|
||
describe('title field', () => { | ||
beforeEach(() => jest.clearAllMocks()); | ||
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectTitle"]'); | ||
|
||
it('renders with its value set from state', () => { | ||
|
@@ -84,6 +84,7 @@ describe('SearchUIForm', () => { | |
}); | ||
|
||
describe('url field', () => { | ||
beforeEach(() => jest.clearAllMocks()); | ||
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectUrl"]'); | ||
|
||
it('renders with its value set from state', () => { | ||
|
@@ -112,6 +113,7 @@ describe('SearchUIForm', () => { | |
}); | ||
|
||
describe('filters field', () => { | ||
beforeEach(() => jest.clearAllMocks()); | ||
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectFilters"]'); | ||
|
||
it('renders with its value set from state', () => { | ||
|
@@ -145,6 +147,7 @@ describe('SearchUIForm', () => { | |
}); | ||
|
||
describe('sorts field', () => { | ||
beforeEach(() => jest.clearAllMocks()); | ||
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectSort"]'); | ||
|
||
it('renders with its value set from state', () => { | ||
|
@@ -177,26 +180,61 @@ describe('SearchUIForm', () => { | |
}); | ||
}); | ||
|
||
it('includes a link to generate the preview', () => { | ||
(generatePreviewUrl as jest.Mock).mockReturnValue('http://www.example.com?foo=bar'); | ||
describe('generate preview button', () => { | ||
let wrapper: ShallowWrapper; | ||
|
||
setMockValues({ | ||
...values, | ||
urlField: 'foo', | ||
titleField: 'bar', | ||
facetFields: ['baz'], | ||
sortFields: ['qux'], | ||
beforeAll(() => { | ||
jest.clearAllMocks(); | ||
(generatePreviewUrl as jest.Mock).mockReturnValue('http://www.example.com?foo=bar'); | ||
setMockValues({ | ||
...values, | ||
urlField: 'foo', | ||
titleField: 'bar', | ||
facetFields: ['baz'], | ||
sortFields: ['qux'], | ||
searchKey: 'search-123abc', | ||
}); | ||
wrapper = shallow(<SearchUIForm />); | ||
}); | ||
|
||
it('should be a submit button', () => { | ||
expect(wrapper.find('[data-test-subj="generateSearchUiPreview"]').prop('type')).toBe( | ||
'submit' | ||
); | ||
}); | ||
|
||
it('should be wrapped in a form configured to POST to the preview screen in a new tab', () => { | ||
const form = wrapper.find(EuiForm); | ||
expect(generatePreviewUrl).toHaveBeenCalledWith({ | ||
urlField: 'foo', | ||
titleField: 'bar', | ||
facets: ['baz'], | ||
sortFields: ['qux'], | ||
}); | ||
expect(form.prop('action')).toBe('http://www.example.com?foo=bar'); | ||
expect(form.prop('target')).toBe('_blank'); | ||
expect(form.prop('method')).toBe('POST'); | ||
expect(form.prop('component')).toBe('form'); | ||
}); | ||
|
||
const subject = () => | ||
shallow(<SearchUIForm />).find('[data-test-subj="generateSearchUiPreview"]'); | ||
it('should include a searchKey in that form POST', () => { | ||
const form = wrapper.find(EuiForm); | ||
const hiddenInput = form.find('input[type="hidden"]'); | ||
expect(hiddenInput.prop('id')).toBe('searchKey'); | ||
expect(hiddenInput.prop('value')).toBe('search-123abc'); | ||
}); | ||
}); | ||
|
||
expect(subject().prop('href')).toBe('http://www.example.com?foo=bar'); | ||
expect(generatePreviewUrl).toHaveBeenCalledWith({ | ||
urlField: 'foo', | ||
titleField: 'bar', | ||
facets: ['baz'], | ||
sortFields: ['qux'], | ||
it('should disable everything while data is loading', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for adding this piece of UX + test - love it! 🎉 |
||
setMockValues({ | ||
...values, | ||
dataLoading: true, | ||
}); | ||
const wrapper = shallow(<SearchUIForm />); | ||
expect(wrapper.find('[data-test-subj="selectTitle"]').prop('disabled')).toBe(true); | ||
expect(wrapper.find('[data-test-subj="selectFilters"]').prop('isDisabled')).toBe(true); | ||
expect(wrapper.find('[data-test-subj="selectSort"]').prop('isDisabled')).toBe(true); | ||
expect(wrapper.find('[data-test-subj="selectUrl"]').prop('disabled')).toBe(true); | ||
expect(wrapper.find('[data-test-subj="generateSearchUiPreview"]').prop('disabled')).toBe(true); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import { useValues, useActions } from 'kea'; | |
|
||
import { EuiForm, EuiFormRow, EuiSelect, EuiComboBox, EuiButton } from '@elastic/eui'; | ||
|
||
import { EngineLogic } from '../../engine'; | ||
import { | ||
TITLE_FIELD_LABEL, | ||
TITLE_FIELD_HELP_TEXT, | ||
|
@@ -27,7 +28,9 @@ import { ActiveField } from '../types'; | |
import { generatePreviewUrl } from '../utils'; | ||
|
||
export const SearchUIForm: React.FC = () => { | ||
const { searchKey } = useValues(EngineLogic); | ||
const { | ||
dataLoading, | ||
validFields, | ||
validSortFields, | ||
validFacetFields, | ||
|
@@ -70,9 +73,11 @@ export const SearchUIForm: React.FC = () => { | |
const selectedFacetOptions = formatMultiOptions(facetFields); | ||
|
||
return ( | ||
<EuiForm> | ||
<EuiForm component="form" action={previewHref} target="_blank" method="POST"> | ||
<input type="hidden" id="searchKey" name="searchKey" value={searchKey} /> | ||
<EuiFormRow label={TITLE_FIELD_LABEL} helpText={TITLE_FIELD_HELP_TEXT} fullWidth> | ||
<EuiSelect | ||
disabled={dataLoading} | ||
options={optionFields} | ||
value={selectedTitleOption && selectedTitleOption.value} | ||
onChange={(e) => onTitleFieldChange(e.target.value)} | ||
|
@@ -85,6 +90,7 @@ export const SearchUIForm: React.FC = () => { | |
</EuiFormRow> | ||
<EuiFormRow label={FILTER_FIELD_LABEL} helpText={FILTER_FIELD_HELP_TEXT} fullWidth> | ||
<EuiComboBox | ||
isDisabled={dataLoading} | ||
options={facetOptionFields} | ||
selectedOptions={selectedFacetOptions} | ||
onChange={(newValues) => onFacetFieldsChange(newValues.map((field) => field.value!))} | ||
|
@@ -96,6 +102,7 @@ export const SearchUIForm: React.FC = () => { | |
</EuiFormRow> | ||
<EuiFormRow label={SORT_FIELD_LABEL} helpText={SORT_FIELD_HELP_TEXT} fullWidth> | ||
<EuiComboBox | ||
isDisabled={dataLoading} | ||
options={sortOptionFields} | ||
selectedOptions={selectedSortOptions} | ||
onChange={(newValues) => onSortFieldsChange(newValues.map((field) => field.value!))} | ||
|
@@ -108,6 +115,7 @@ export const SearchUIForm: React.FC = () => { | |
|
||
<EuiFormRow label={URL_FIELD_LABEL} helpText={URL_FIELD_HELP_TEXT} fullWidth> | ||
<EuiSelect | ||
disabled={dataLoading} | ||
options={optionFields} | ||
value={selectedURLOption && selectedURLOption.value} | ||
onChange={(e) => onUrlFieldChange(e.target.value)} | ||
|
@@ -119,8 +127,8 @@ export const SearchUIForm: React.FC = () => { | |
/> | ||
</EuiFormRow> | ||
<EuiButton | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than being a link as before, this is not a form with a submit button so that we can POST to the new route. |
||
href={previewHref} | ||
target="_blank" | ||
disabled={dataLoading} | ||
type="submit" | ||
fill | ||
iconType="popout" | ||
iconSide="right" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to POST the search key to the new search experience URL from the Search UI form. Since all keys that are valid for a particular engine are already available on
engine.apiTokens
from state, I added a selector to easily access the first available search token.Since tokens that do not have access to the current engine are not returned in this object, I do not have to check their access level, as we already know it has access to the current engine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this added logic/selector. Feels elegant and makes sense.