Skip to content

Commit

Permalink
[App Search] Add relevance tuning boost forms (#91912)
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonStoltz authored Feb 24, 2021
1 parent e1fa0bf commit d9d0790
Show file tree
Hide file tree
Showing 26 changed files with 861 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,29 @@ import React, { useMemo } from 'react';

import { EuiFlexItem, EuiAccordion, EuiFlexGroup, EuiHideFor } from '@elastic/eui';

import { BoostIcon } from '../../../boost_icon';
import { BOOST_TYPE_TO_DISPLAY_MAP } from '../../../constants';
import { Boost } from '../../../types';
import { ValueBadge } from '../../value_badge';
import { BoostIcon } from '../boost_icon';
import { BOOST_TYPE_TO_DISPLAY_MAP } from '../constants';
import { Boost } from '../types';
import { ValueBadge } from '../value_badge';

import { BoostItemContent } from './boost_item_content';
import { getBoostSummary } from './get_boost_summary';

interface Props {
boost: Boost;
id: string;
index: number;
name: string;
}

export const BoostItem: React.FC<Props> = ({ id, boost }) => {
export const BoostItem: React.FC<Props> = ({ id, boost, index, name }) => {
const summary = useMemo(() => getBoostSummary(boost), [boost]);

return (
<EuiAccordion
id={id}
className="boosts__item"
buttonContentClassName="boosts__itemContent"
buttonContentClassName="boosts__itemButton"
buttonContent={
<EuiFlexGroup responsive={false} wrap>
<EuiFlexItem>
Expand All @@ -48,6 +51,8 @@ export const BoostItem: React.FC<Props> = ({ id, boost }) => {
</EuiFlexGroup>
}
paddingSize="s"
/>
>
<BoostItemContent boost={boost} index={index} name={name} />
</EuiAccordion>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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 { setMockActions } from '../../../../../__mocks__/kea.mock';

import React from 'react';

import { shallow } from 'enzyme';

import { EuiButton, EuiRange } from '@elastic/eui';

import { BoostType } from '../../types';

import { BoostItemContent } from './boost_item_content';
import { FunctionalBoostForm } from './functional_boost_form';
import { ProximityBoostForm } from './proximity_boost_form';
import { ValueBoostForm } from './value_boost_form';

describe('BoostItemContent', () => {
const actions = {
updateBoostFactor: jest.fn(),
deleteBoost: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
setMockActions(actions);
});

it('renders a value boost form if the provided boost is "value" boost', () => {
const boost = {
factor: 2,
type: 'value' as BoostType,
};

const wrapper = shallow(<BoostItemContent boost={boost} index={3} name="foo" />);

expect(wrapper.find(ValueBoostForm).exists()).toBe(true);
expect(wrapper.find(FunctionalBoostForm).exists()).toBe(false);
expect(wrapper.find(ProximityBoostForm).exists()).toBe(false);
});

it('renders a functional boost form if the provided boost is "functional" boost', () => {
const boost = {
factor: 10,
type: 'functional' as BoostType,
};

const wrapper = shallow(<BoostItemContent boost={boost} index={3} name="foo" />);

expect(wrapper.find(ValueBoostForm).exists()).toBe(false);
expect(wrapper.find(FunctionalBoostForm).exists()).toBe(true);
expect(wrapper.find(ProximityBoostForm).exists()).toBe(false);
});

it('renders a proximity boost form if the provided boost is "proximity" boost', () => {
const boost = {
factor: 8,
type: 'proximity' as BoostType,
};

const wrapper = shallow(<BoostItemContent boost={boost} index={3} name="foo" />);

expect(wrapper.find(ValueBoostForm).exists()).toBe(false);
expect(wrapper.find(FunctionalBoostForm).exists()).toBe(false);
expect(wrapper.find(ProximityBoostForm).exists()).toBe(true);
});

it("renders an impact slider that can be used to update the boost's 'factor'", () => {
const boost = {
factor: 8,
type: 'proximity' as BoostType,
};

const wrapper = shallow(<BoostItemContent boost={boost} index={3} name="foo" />);
const impactSlider = wrapper.find(EuiRange);
expect(impactSlider.prop('value')).toBe(8);

impactSlider.simulate('change', { target: { value: '2' } });

expect(actions.updateBoostFactor).toHaveBeenCalledWith('foo', 3, 2);
});

it("will delete the current boost if the 'Delete Boost' button is clicked", () => {
const boost = {
factor: 8,
type: 'proximity' as BoostType,
};

const wrapper = shallow(<BoostItemContent boost={boost} index={3} name="foo" />);
wrapper.find(EuiButton).simulate('click');

expect(actions.deleteBoost).toHaveBeenCalledWith('foo', 3);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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 { useActions } from 'kea';

import { EuiButton, EuiFormRow, EuiPanel, EuiRange, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { RelevanceTuningLogic } from '../..';
import { Boost, BoostType } from '../../types';

import { FunctionalBoostForm } from './functional_boost_form';
import { ProximityBoostForm } from './proximity_boost_form';
import { ValueBoostForm } from './value_boost_form';

interface Props {
boost: Boost;
index: number;
name: string;
}

export const BoostItemContent: React.FC<Props> = ({ boost, index, name }) => {
const { deleteBoost, updateBoostFactor } = useActions(RelevanceTuningLogic);
const { type } = boost;

const getBoostForm = () => {
switch (type) {
case BoostType.Value:
return <ValueBoostForm boost={boost} index={index} name={name} />;
case BoostType.Functional:
return <FunctionalBoostForm boost={boost} index={index} name={name} />;
case BoostType.Proximity:
return <ProximityBoostForm boost={boost} index={index} name={name} />;
}
};

return (
<EuiPanel hasShadow={false} className="relevanceTuningAccordionItem">
{getBoostForm()}
<EuiSpacer />
<EuiFormRow
label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.impactLabel',
{
defaultMessage: 'Impact',
}
)}
fullWidth
>
<EuiRange
min={-10}
max={10}
step={0.1}
value={boost.factor}
onChange={(e) =>
updateBoostFactor(
name,
index,
parseFloat((e as React.ChangeEvent<HTMLInputElement>).target.value)
)
}
showInput
compressed
fullWidth
/>
</EuiFormRow>
<EuiButton color="danger" iconType="cross" size="s" onClick={() => deleteBoost(name, index)}>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.deleteBoostButtonLabel',
{
defaultMessage: 'Delete Boost',
}
)}
</EuiButton>
</EuiPanel>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 { setMockActions } from '../../../../../__mocks__/kea.mock';

import React from 'react';

import { shallow, ShallowWrapper } from 'enzyme';

import { EuiSelect } from '@elastic/eui';

import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from '../../types';

import { FunctionalBoostForm } from './functional_boost_form';

describe('FunctionalBoostForm', () => {
const boost: Boost = {
factor: 2,
type: 'functional' as BoostType,
function: 'logarithmic' as FunctionalBoostFunction,
operation: 'multiply' as BoostOperation,
};

const actions = {
updateBoostSelectOption: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
setMockActions(actions);
});

const functionSelect = (wrapper: ShallowWrapper) => wrapper.find(EuiSelect).at(0);
const operationSelect = (wrapper: ShallowWrapper) => wrapper.find(EuiSelect).at(1);

it('renders select boxes with values from the provided boost selected', () => {
const wrapper = shallow(<FunctionalBoostForm boost={boost} index={3} name="foo" />);
expect(functionSelect(wrapper).prop('value')).toEqual('logarithmic');
expect(operationSelect(wrapper).prop('value')).toEqual('multiply');
});

it('will update state when a user makes a selection', () => {
const wrapper = shallow(<FunctionalBoostForm boost={boost} index={3} name="foo" />);

functionSelect(wrapper).simulate('change', {
target: {
value: 'exponential',
},
});
expect(actions.updateBoostSelectOption).toHaveBeenCalledWith(
'foo',
3,
'function',
'exponential'
);

operationSelect(wrapper).simulate('change', {
target: {
value: 'add',
},
});
expect(actions.updateBoostSelectOption).toHaveBeenCalledWith('foo', 3, 'operation', 'add');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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 { useActions } from 'kea';

import { EuiFormRow, EuiSelect } from '@elastic/eui';

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

import { RelevanceTuningLogic } from '../..';
import {
BOOST_OPERATION_DISPLAY_MAP,
FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP,
} from '../../constants';
import {
Boost,
BoostFunction,
BoostOperation,
BoostType,
FunctionalBoostFunction,
} from '../../types';

interface Props {
boost: Boost;
index: number;
name: string;
}

const functionOptions = Object.values(FunctionalBoostFunction).map((boostFunction) => ({
value: boostFunction,
text: FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP[boostFunction as FunctionalBoostFunction],
}));

const operationOptions = Object.values(BoostOperation).map((boostOperation) => ({
value: boostOperation,
text: BOOST_OPERATION_DISPLAY_MAP[boostOperation as BoostOperation],
}));

export const FunctionalBoostForm: React.FC<Props> = ({ boost, index, name }) => {
const { updateBoostSelectOption } = useActions(RelevanceTuningLogic);
return (
<>
<EuiFormRow
label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.funtional.functionDropDownLabel',
{
defaultMessage: 'Function',
}
)}
fullWidth
>
<EuiSelect
name={`function-${BoostType.Functional}${index}`}
options={functionOptions}
value={boost.function}
onChange={(e) =>
updateBoostSelectOption(name, index, 'function', e.target.value as BoostFunction)
}
fullWidth
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.funtional.operationDropDownLabel',
{
defaultMessage: 'Operation',
}
)}
fullWidth
>
<EuiSelect
name={`operation-${BoostType.Functional}${index}`}
options={operationOptions}
value={boost.operation}
onChange={(e) =>
updateBoostSelectOption(name, index, 'operation', e.target.value as BoostOperation)
}
fullWidth
/>
</EuiFormRow>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export { BoostItemContent } from './boost_item_content';
Loading

0 comments on commit d9d0790

Please sign in to comment.