[App Search] Added Search UI form (#99359)

This commit is contained in:
Jason Stoltzfus 2021-05-11 09:16:27 -04:00 committed by GitHub
parent 051eec703d
commit eb52bd2318
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 971 additions and 27 deletions

View file

@ -0,0 +1,202 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
jest.mock('../utils', () => ({
generatePreviewUrl: jest.fn(),
}));
import { setMockValues, setMockActions } from '../../../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
import { ActiveField } from '../types';
import { generatePreviewUrl } from '../utils';
import { SearchUIForm } from './search_ui_form';
describe('SearchUIForm', () => {
const values = {
validFields: ['title', 'url', 'category', 'size'],
validSortFields: ['title', 'url', 'category', 'size'],
validFacetFields: ['title', 'url', 'category', 'size'],
titleField: 'title',
urlField: 'url',
facetFields: ['category'],
sortFields: ['size'],
};
const actions = {
onActiveFieldChange: jest.fn(),
onFacetFieldsChange: jest.fn(),
onSortFieldsChange: jest.fn(),
onTitleFieldChange: jest.fn(),
onUrlFieldChange: jest.fn(),
};
beforeAll(() => {
setMockValues(values);
setMockActions(actions);
});
beforeEach(() => {
jest.clearAllMocks();
});
it('renders', () => {
const wrapper = shallow(<SearchUIForm />);
expect(wrapper.find('[data-test-subj="selectTitle"]').exists()).toBe(true);
expect(wrapper.find('[data-test-subj="selectFilters"]').exists()).toBe(true);
expect(wrapper.find('[data-test-subj="selectSort"]').exists()).toBe(true);
expect(wrapper.find('[data-test-subj="selectUrl"]').exists()).toBe(true);
});
describe('title field', () => {
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectTitle"]');
it('renders with its value set from state', () => {
setMockValues({
...values,
titleField: 'foo',
});
expect(subject().prop('value')).toBe('foo');
});
it('updates state with new value when changed', () => {
subject().simulate('change', { target: { value: 'foo' } });
expect(actions.onTitleFieldChange).toHaveBeenCalledWith('foo');
});
it('updates active field in state on focus', () => {
subject().simulate('focus');
expect(actions.onActiveFieldChange).toHaveBeenCalledWith(ActiveField.Title);
});
it('removes active field in state on blur', () => {
subject().simulate('blur');
expect(actions.onActiveFieldChange).toHaveBeenCalledWith(ActiveField.None);
});
});
describe('url field', () => {
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectUrl"]');
it('renders with its value set from state', () => {
setMockValues({
...values,
urlField: 'foo',
});
expect(subject().prop('value')).toBe('foo');
});
it('updates state with new value when changed', () => {
subject().simulate('change', { target: { value: 'foo' } });
expect(actions.onUrlFieldChange).toHaveBeenCalledWith('foo');
});
it('updates active field in state on focus', () => {
subject().simulate('focus');
expect(actions.onActiveFieldChange).toHaveBeenCalledWith(ActiveField.Url);
});
it('removes active field in state on blur', () => {
subject().simulate('blur');
expect(actions.onActiveFieldChange).toHaveBeenCalledWith(ActiveField.None);
});
});
describe('filters field', () => {
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectFilters"]');
it('renders with its value set from state', () => {
setMockValues({
...values,
facetFields: ['foo'],
});
expect(subject().prop('selectedOptions')).toEqual([
{ label: 'foo', text: 'foo', value: 'foo' },
]);
});
it('updates state with new value when changed', () => {
subject().simulate('change', [
{ label: 'foo', text: 'foo', value: 'foo' },
{ label: 'bar', text: 'bar', value: 'bar' },
]);
expect(actions.onFacetFieldsChange).toHaveBeenCalledWith(['foo', 'bar']);
});
it('updates active field in state on focus', () => {
subject().simulate('focus');
expect(actions.onActiveFieldChange).toHaveBeenCalledWith(ActiveField.Filter);
});
it('removes active field in state on blur', () => {
subject().simulate('blur');
expect(actions.onActiveFieldChange).toHaveBeenCalledWith(ActiveField.None);
});
});
describe('sorts field', () => {
const subject = () => shallow(<SearchUIForm />).find('[data-test-subj="selectSort"]');
it('renders with its value set from state', () => {
setMockValues({
...values,
sortFields: ['foo'],
});
expect(subject().prop('selectedOptions')).toEqual([
{ label: 'foo', text: 'foo', value: 'foo' },
]);
});
it('updates state with new value when changed', () => {
subject().simulate('change', [
{ label: 'foo', text: 'foo', value: 'foo' },
{ label: 'bar', text: 'bar', value: 'bar' },
]);
expect(actions.onSortFieldsChange).toHaveBeenCalledWith(['foo', 'bar']);
});
it('updates active field in state on focus', () => {
subject().simulate('focus');
expect(actions.onActiveFieldChange).toHaveBeenCalledWith(ActiveField.Sort);
});
it('removes active field in state on blur', () => {
subject().simulate('blur');
expect(actions.onActiveFieldChange).toHaveBeenCalledWith(ActiveField.None);
});
});
it('includes a link to generate the preview', () => {
(generatePreviewUrl as jest.Mock).mockReturnValue('http://www.example.com?foo=bar');
setMockValues({
...values,
urlField: 'foo',
titleField: 'bar',
facetFields: ['baz'],
sortFields: ['qux'],
});
const subject = () =>
shallow(<SearchUIForm />).find('[data-test-subj="generateSearchUiPreview"]');
expect(subject().prop('href')).toBe('http://www.example.com?foo=bar');
expect(generatePreviewUrl).toHaveBeenCalledWith({
urlField: 'foo',
titleField: 'bar',
facets: ['baz'],
sortFields: ['qux'],
});
});
});

View file

@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useValues, useActions } from 'kea';
import { EuiForm, EuiFormRow, EuiSelect, EuiComboBox, EuiButton } from '@elastic/eui';
import {
TITLE_FIELD_LABEL,
TITLE_FIELD_HELP_TEXT,
FILTER_FIELD_LABEL,
FILTER_FIELD_HELP_TEXT,
SORT_FIELD_LABEL,
SORT_FIELD_HELP_TEXT,
URL_FIELD_LABEL,
URL_FIELD_HELP_TEXT,
GENERATE_PREVIEW_BUTTON_LABEL,
} from '../i18n';
import { SearchUILogic } from '../search_ui_logic';
import { ActiveField } from '../types';
import { generatePreviewUrl } from '../utils';
export const SearchUIForm: React.FC = () => {
const {
validFields,
validSortFields,
validFacetFields,
titleField,
urlField,
facetFields,
sortFields,
} = useValues(SearchUILogic);
const {
onActiveFieldChange,
onFacetFieldsChange,
onSortFieldsChange,
onTitleFieldChange,
onUrlFieldChange,
} = useActions(SearchUILogic);
const previewHref = generatePreviewUrl({
titleField,
urlField,
facets: facetFields,
sortFields,
});
const formatSelectOption = (fieldName: string) => {
return { text: fieldName, value: fieldName };
};
const formatMultiOptions = (fieldNames: string[]) =>
fieldNames.map((fieldName) => ({ label: fieldName, text: fieldName, value: fieldName }));
const formatMultiOptionsWithEmptyOption = (fieldNames: string[]) => [
{ label: '', text: '', value: '' },
...formatMultiOptions(fieldNames),
];
const optionFields = formatMultiOptionsWithEmptyOption(validFields);
const sortOptionFields = formatMultiOptions(validSortFields);
const facetOptionFields = formatMultiOptions(validFacetFields);
const selectedTitleOption = formatSelectOption(titleField);
const selectedURLOption = formatSelectOption(urlField);
const selectedSortOptions = formatMultiOptions(sortFields);
const selectedFacetOptions = formatMultiOptions(facetFields);
return (
<EuiForm>
<EuiFormRow label={TITLE_FIELD_LABEL} helpText={TITLE_FIELD_HELP_TEXT} fullWidth>
<EuiSelect
options={optionFields}
value={selectedTitleOption && selectedTitleOption.value}
onChange={(e) => onTitleFieldChange(e.target.value)}
fullWidth
onFocus={() => onActiveFieldChange(ActiveField.Title)}
onBlur={() => onActiveFieldChange(ActiveField.None)}
hasNoInitialSelection
data-test-subj="selectTitle"
/>
</EuiFormRow>
<EuiFormRow label={FILTER_FIELD_LABEL} helpText={FILTER_FIELD_HELP_TEXT} fullWidth>
<EuiComboBox
options={facetOptionFields}
selectedOptions={selectedFacetOptions}
onChange={(newValues) => onFacetFieldsChange(newValues.map((field) => field.value!))}
onFocus={() => onActiveFieldChange(ActiveField.Filter)}
onBlur={() => onActiveFieldChange(ActiveField.None)}
fullWidth
data-test-subj="selectFilters"
/>
</EuiFormRow>
<EuiFormRow label={SORT_FIELD_LABEL} helpText={SORT_FIELD_HELP_TEXT} fullWidth>
<EuiComboBox
options={sortOptionFields}
selectedOptions={selectedSortOptions}
onChange={(newValues) => onSortFieldsChange(newValues.map((field) => field.value!))}
onFocus={() => onActiveFieldChange(ActiveField.Sort)}
onBlur={() => onActiveFieldChange(ActiveField.None)}
fullWidth
data-test-subj="selectSort"
/>
</EuiFormRow>
<EuiFormRow label={URL_FIELD_LABEL} helpText={URL_FIELD_HELP_TEXT} fullWidth>
<EuiSelect
options={optionFields}
value={selectedURLOption && selectedURLOption.value}
onChange={(e) => onUrlFieldChange(e.target.value)}
fullWidth
onFocus={() => onActiveFieldChange(ActiveField.Url)}
onBlur={() => onActiveFieldChange(ActiveField.None)}
hasNoInitialSelection
data-test-subj="selectUrl"
/>
</EuiFormRow>
<EuiButton
href={previewHref}
target="_blank"
fill
iconType="popout"
iconSide="right"
data-test-subj="generateSearchUiPreview"
>
{GENERATE_PREVIEW_BUTTON_LABEL}
</EuiButton>
</EuiForm>
);
};

View file

@ -0,0 +1,188 @@
.searchUIGraphic {
transform-style: preserve-3d;
transform: rotate3d(0, 1, 0, -8deg);
#search-area {
.outerBox {
fill: $euiColorLightShade;
}
.field {
fill: $euiColorEmptyShade;
}
.searchIcon {
fill: $euiColorDarkShade;
}
.type {
fill: $euiColorDarkShade;
}
}
#background {
fill: $euiColorLightestShade;
}
#results {
.outerBox {
fill: $euiColorEmptyShade;
stroke: $euiColorLightShade;
stroke-width: $euiBorderWidthThin;
}
.shoe {
fill: $euiColorMediumShade;
}
.url {
fill: $euiColorDarkShade;
transform: translateY(5px);
}
.titleCopy {
fill: $euiColorDarkShade;
}
.titleBox {
fill: $euiColorEmptyShade;
}
.blockIn {
fill: $euiColorLightShade;
}
}
#filter {
.outerBox {
fill: $euiColorEmptyShade;
stroke: $euiColorLightShade;
stroke-width: 1px;
}
.header {
fill: $euiColorDarkShade;
}
.checkbox {
fill: $euiColorDarkShade;
}
.check {
fill: $euiColorEmptyShade;
}
.filterCopy {
fill: $euiColorDarkShade;
}
}
#sort {
.outerBox {
fill: $euiColorEmptyShade;
stroke: $euiColorLightShade;
stroke-width: 1px;
}
.header {
fill: $euiColorDarkShade;
}
.selectCopy {
fill: $euiColorDarkShade;
}
.selectBox {
fill: $euiColorEmptyShade;
stroke: $euiColorLightShade;
stroke-width: 1px;
}
.selectControl {
fill: $euiColorDarkShade;
}
}
#pagination {
.outerBox {
fill: $euiColorLightShade;
}
.arrow {
fill: $euiColorEmptyShade;
}
}
&.activeTitle {
#results {
.titleBox {
fill: $euiColorEmptyShade;
stroke: $euiColorPrimary;
stroke-width: 1px;
}
.titleCopy {
fill: $euiColorPrimary;
}
.outerBox {
fill: $euiColorEmptyShade;
stroke: $euiColorPrimary;
}
.url {
fill: $euiColorPrimary;
opacity: .1;
}
.shoe {
fill: $euiColorPrimary;
opacity: .1;
}
}
}
&.activeUrl {
#results {
.outerBox {
fill: $euiColorEmptyShade;
stroke: $euiColorPrimary;
stroke-width: 1px;
}
.url {
fill: $euiColorPrimary;
}
.titleBox {
fill: $euiColorEmptyShade;
}
.titleCopy {
fill: $euiColorPrimary;
opacity: .1;
}
.shoe {
fill: $euiColorPrimary;
opacity: .1;
}
}
}
&.activeFilter {
#filter {
.outerBox {
fill: $euiColorEmptyShade;
stroke: $euiColorPrimary;
stroke-width: $euiBorderWidthThick;
}
.header {
fill: $euiColorPrimary;
}
.checkbox {
fill: $euiColorPrimary;
}
.check {
fill: $euiColorEmptyShade;
}
.filterCopy {
fill: $euiColorPrimary;
}
}
}
&.activeSort {
#sort {
.outerBox {
fill: $euiColorEmptyShade;
stroke: $euiColorPrimary;
stroke-width: 2px;
}
.header {
fill: $euiColorPrimary;
}
.selectCopy {
fill: $euiColorPrimary;
}
.selectBox {
fill: $euiColorEmptyShade;
stroke: $euiColorPrimary;
stroke-width: 1px;
}
.selectControl {
fill: $euiColorPrimary;
}
}
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setMockValues } from '../../../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
import { ActiveField } from '../types';
import { SearchUIGraphic } from './search_ui_graphic';
describe('SearchUIGraphic', () => {
const values = {
activeField: ActiveField.Sort,
};
beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
});
it('renders an svg with a className determined by the currently active field', () => {
const wrapper = shallow(<SearchUIGraphic />);
expect(wrapper.hasClass('activeSort')).toBe(true);
});
});

View file

@ -1,13 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const SEARCH_UI_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.title',
{ defaultMessage: 'Search UI' }
);

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const SEARCH_UI_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.title',
{ defaultMessage: 'Search UI' }
);
export const TITLE_FIELD_LABEL = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.titleFieldLabel',
{ defaultMessage: 'Title field (Optional)' }
);
export const TITLE_FIELD_HELP_TEXT = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.titleFieldHelpText',
{ defaultMessage: 'Used as the top-level visual identifier for every rendered result' }
);
export const FILTER_FIELD_LABEL = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.filterFieldLabel',
{ defaultMessage: 'Filter fields (Optional)' }
);
export const FILTER_FIELD_HELP_TEXT = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.filterFieldHelpText',
{ defaultMessage: 'Faceted values rendered as filters and available as query refinement' }
);
export const SORT_FIELD_LABEL = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.sortFieldLabel',
{ defaultMessage: 'Sort fields (Optional)' }
);
export const SORT_FIELD_HELP_TEXT = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.sortHelpText',
{ defaultMessage: 'Used to display result sorting options, ascending and descending' }
);
export const URL_FIELD_LABEL = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.urlFieldLabel',
{ defaultMessage: 'URL field (Optional)' }
);
export const URL_FIELD_HELP_TEXT = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.urlFieldHelpText',
{ defaultMessage: "Used as a result's link target, if applicable" }
);
export const GENERATE_PREVIEW_BUTTON_LABEL = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.searchUI.generatePreviewButtonLabel',
{ defaultMessage: 'Generate search experience' }
);

View file

@ -5,6 +5,6 @@
* 2.0.
*/
export { SEARCH_UI_TITLE } from './constants';
export { SEARCH_UI_TITLE } from './i18n';
export { SearchUI } from './search_ui';
export { SearchUILogic } from './search_ui_logic';

View file

@ -9,14 +9,26 @@ import React, { useEffect } from 'react';
import { useActions } from 'kea';
import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui';
import {
EuiPageHeader,
EuiPageContentBody,
EuiText,
EuiFlexItem,
EuiFlexGroup,
EuiSpacer,
EuiLink,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FlashMessages } from '../../../shared/flash_messages';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { DOCS_PREFIX } from '../../routes';
import { getEngineBreadcrumbs } from '../engine';
import { SEARCH_UI_TITLE } from './constants';
import { SearchUIForm } from './components/search_ui_form';
import { SearchUIGraphic } from './components/search_ui_graphic';
import { SEARCH_UI_TITLE } from './i18n';
import { SearchUILogic } from './search_ui_logic';
export const SearchUI: React.FC = () => {
@ -31,7 +43,51 @@ export const SearchUI: React.FC = () => {
<SetPageChrome trail={getEngineBreadcrumbs([SEARCH_UI_TITLE])} />
<EuiPageHeader pageTitle={SEARCH_UI_TITLE} />
<FlashMessages />
<EuiPageContentBody>TODO</EuiPageContentBody>
<EuiPageContentBody>
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.engine.searchUI.bodyDescription"
defaultMessage="Search UI is a free and open library for building search experiences with React. {link}."
values={{
link: (
<EuiLink target="_blank" href="https://github.com/elastic/search-ui">
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.engine.searchUI.repositoryLinkText"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
</p>
<p>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.engine.searchUI.lowerBodyDescription"
defaultMessage="Use the fields below to generate a sample search experience built with Search UI. Use the sample to preview search results, or build upon it to create your own custom search experience. {link}."
values={{
link: (
<EuiLink target="_blank" href={`${DOCS_PREFIX}/reference-ui-guide.html`}>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.engine.searchUI.guideLinkText"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
</p>
</EuiText>
<EuiSpacer />
<SearchUIForm />
</EuiFlexItem>
<EuiFlexItem>
<SearchUIGraphic />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</>
);
};

View file

@ -78,10 +78,10 @@ describe('SearchUILogic', () => {
});
});
describe('onURLFieldChange', () => {
describe('onUrlFieldChange', () => {
it('sets the urlField value', () => {
mount({ urlField: '' });
SearchUILogic.actions.onURLFieldChange('foo');
SearchUILogic.actions.onUrlFieldChange('foo');
expect(SearchUILogic.values).toEqual({
...DEFAULT_VALUES,
urlField: 'foo',

View file

@ -25,7 +25,7 @@ interface SearchUIActions {
onFacetFieldsChange(facetFields: string[]): { facetFields: string[] };
onSortFieldsChange(sortFields: string[]): { sortFields: string[] };
onTitleFieldChange(titleField: string): { titleField: string };
onURLFieldChange(urlField: string): { urlField: string };
onUrlFieldChange(urlField: string): { urlField: string };
}
interface SearchUIValues {
@ -49,7 +49,7 @@ export const SearchUILogic = kea<MakeLogicType<SearchUIValues, SearchUIActions>>
onFacetFieldsChange: (facetFields) => ({ facetFields }),
onSortFieldsChange: (sortFields) => ({ sortFields }),
onTitleFieldChange: (titleField) => ({ titleField }),
onURLFieldChange: (urlField) => ({ urlField }),
onUrlFieldChange: (urlField) => ({ urlField }),
}),
reducers: () => ({
dataLoading: [
@ -62,7 +62,7 @@ export const SearchUILogic = kea<MakeLogicType<SearchUIValues, SearchUIActions>>
validSortFields: [[], { onFieldDataLoaded: (_, { validSortFields }) => validSortFields }],
validFacetFields: [[], { onFieldDataLoaded: (_, { validFacetFields }) => validFacetFields }],
titleField: ['', { onTitleFieldChange: (_, { titleField }) => titleField }],
urlField: ['', { onURLFieldChange: (_, { urlField }) => urlField }],
urlField: ['', { onUrlFieldChange: (_, { urlField }) => urlField }],
facetFields: [[], { onFacetFieldsChange: (_, { facetFields }) => facetFields }],
sortFields: [[], { onSortFieldsChange: (_, { sortFields }) => sortFields }],
activeField: [ActiveField.None, { onActiveFieldChange: (_, { activeField }) => activeField }],

View file

@ -6,9 +6,9 @@
*/
export enum ActiveField {
Title,
Filter,
Sort,
Url,
None,
Title = 'Title',
Filter = 'Filter',
Sort = 'Sort',
Url = 'Url',
None = '',
}

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import '../../../__mocks__/enterprise_search_url.mock';
import { generatePreviewUrl } from './utils';
jest.mock('../engine', () => ({
EngineLogic: {
values: {
engineName: 'national-parks-demo',
},
},
}));
describe('generatePreviewUrl', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('generates a url to the preview application from state', () => {
expect(
generatePreviewUrl({
titleField: 'foo',
urlField: 'bar',
facets: ['baz', 'qux'],
sortFields: ['quux', 'quuz'],
empty: '', // Empty fields should be stripped
empty2: [''], // Empty fields should be stripped
})
).toEqual(
'http://localhost:3002/as/engines/national-parks-demo/reference_application/preview?facets[]=baz&facets[]=qux&fromKibana=true&sortFields[]=quux&sortFields[]=quuz&titleField=foo&urlField=bar'
);
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import queryString, { ParsedQuery } from 'query-string';
import { getAppSearchUrl } from '../../../shared/enterprise_search_url';
import { EngineLogic } from '../engine';
export const generatePreviewUrl = (query: ParsedQuery) => {
const { engineName } = EngineLogic.values;
return queryString.stringifyUrl(
{
query: {
...query,
fromKibana: 'true',
},
url: getAppSearchUrl(`/engines/${engineName}/reference_application/preview`),
},
{ arrayFormat: 'bracket', skipEmptyString: true }
);
};