mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[App Search] Added Search UI form (#99359)
This commit is contained in:
parent
051eec703d
commit
eb52bd2318
14 changed files with 971 additions and 27 deletions
|
@ -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'],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -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' }
|
||||
);
|
|
@ -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' }
|
||||
);
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 }],
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
export enum ActiveField {
|
||||
Title,
|
||||
Filter,
|
||||
Sort,
|
||||
Url,
|
||||
None,
|
||||
Title = 'Title',
|
||||
Filter = 'Filter',
|
||||
Sort = 'Sort',
|
||||
Url = 'Url',
|
||||
None = '',
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 }
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue