[App Search] Create Curation view/functionality (#92560)
* Add server route and logic listener * [Misc] Remove 'Set' from 'deleteCurationSet' - to match createCuration - IMO, this language isn't necessary if we're splitting up Curations and CurationLogic - the context is fairly evident within a smaller and more modular logic file * Add CurationQueries component + accompanying CurationQueriesLogic & CurationQuery row * Add CurationCreation view
This commit is contained in:
parent
fd348d3f82
commit
0198607eb3
|
@ -0,0 +1,3 @@
|
|||
.curationQueryRow {
|
||||
margin-bottom: $euiSizeXS;
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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, setMockValues } from '../../../../../__mocks__';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { CurationQuery } from './curation_query';
|
||||
|
||||
import { CurationQueries } from './';
|
||||
|
||||
describe('CurationQueries', () => {
|
||||
const props = {
|
||||
queries: ['a', 'b', 'c'],
|
||||
onSubmit: jest.fn(),
|
||||
};
|
||||
const values = {
|
||||
queries: ['a', 'b', 'c'],
|
||||
hasEmptyQueries: false,
|
||||
hasOnlyOneQuery: false,
|
||||
};
|
||||
const actions = {
|
||||
addQuery: jest.fn(),
|
||||
editQuery: jest.fn(),
|
||||
deleteQuery: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockValues(values);
|
||||
setMockActions(actions);
|
||||
});
|
||||
|
||||
it('renders a CurationQuery row for each query', () => {
|
||||
const wrapper = shallow(<CurationQueries {...props} />);
|
||||
|
||||
expect(wrapper.find(CurationQuery)).toHaveLength(3);
|
||||
expect(wrapper.find(CurationQuery).at(0).prop('queryValue')).toEqual('a');
|
||||
expect(wrapper.find(CurationQuery).at(1).prop('queryValue')).toEqual('b');
|
||||
expect(wrapper.find(CurationQuery).at(2).prop('queryValue')).toEqual('c');
|
||||
});
|
||||
|
||||
it('calls editQuery when the CurationQuery value changes', () => {
|
||||
const wrapper = shallow(<CurationQueries {...props} />);
|
||||
wrapper.find(CurationQuery).at(0).simulate('change', 'new query value');
|
||||
|
||||
expect(actions.editQuery).toHaveBeenCalledWith(0, 'new query value');
|
||||
});
|
||||
|
||||
it('calls deleteQuery when the CurationQuery calls onDelete', () => {
|
||||
const wrapper = shallow(<CurationQueries {...props} />);
|
||||
wrapper.find(CurationQuery).at(2).simulate('delete');
|
||||
|
||||
expect(actions.deleteQuery).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('calls addQuery when the Add Query button is clicked', () => {
|
||||
const wrapper = shallow(<CurationQueries {...props} />);
|
||||
wrapper.find('[data-test-subj="addCurationQueryButton"]').simulate('click');
|
||||
|
||||
expect(actions.addQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables the add button if any query fields are empty', () => {
|
||||
setMockValues({
|
||||
...values,
|
||||
queries: ['a', '', 'c'],
|
||||
hasEmptyQueries: true,
|
||||
});
|
||||
const wrapper = shallow(<CurationQueries {...props} />);
|
||||
const button = wrapper.find('[data-test-subj="addCurationQueryButton"]');
|
||||
|
||||
expect(button.prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('calls the passed onSubmit callback when the submit button is clicked', () => {
|
||||
setMockValues({ ...values, queries: ['some query'] });
|
||||
const wrapper = shallow(<CurationQueries {...props} />);
|
||||
wrapper.find('[data-test-subj="submitCurationQueriesButton"]').simulate('click');
|
||||
|
||||
expect(props.onSubmit).toHaveBeenCalledWith(['some query']);
|
||||
});
|
||||
|
||||
it('disables the submit button if no query fields have been filled', () => {
|
||||
setMockValues({
|
||||
...values,
|
||||
queries: [''],
|
||||
hasOnlyOneQuery: true,
|
||||
hasEmptyQueries: true,
|
||||
});
|
||||
const wrapper = shallow(<CurationQueries {...props} />);
|
||||
const button = wrapper.find('[data-test-subj="submitCurationQueriesButton"]');
|
||||
|
||||
expect(button.prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { Curation } from '../../types';
|
||||
|
||||
import { CurationQueriesLogic } from './curation_queries_logic';
|
||||
import { CurationQuery } from './curation_query';
|
||||
import { filterEmptyQueries } from './utils';
|
||||
import './curation_queries.scss';
|
||||
|
||||
interface Props {
|
||||
queries: Curation['queries'];
|
||||
onSubmit(queries: Curation['queries']): void;
|
||||
submitButtonText?: string;
|
||||
}
|
||||
|
||||
export const CurationQueries: React.FC<Props> = ({
|
||||
queries: initialQueries,
|
||||
onSubmit,
|
||||
submitButtonText = i18n.translate('xpack.enterpriseSearch.actions.continue', {
|
||||
defaultMessage: 'Continue',
|
||||
}),
|
||||
}) => {
|
||||
const logic = CurationQueriesLogic({ queries: initialQueries });
|
||||
const { queries, hasEmptyQueries, hasOnlyOneQuery } = useValues(logic);
|
||||
const { addQuery, editQuery, deleteQuery } = useActions(logic);
|
||||
|
||||
return (
|
||||
<>
|
||||
{queries.map((query: string, index) => (
|
||||
<CurationQuery
|
||||
key={`query-${index}`}
|
||||
queryValue={query}
|
||||
onChange={(newValue) => editQuery(index, newValue)}
|
||||
onDelete={() => deleteQuery(index)}
|
||||
disableDelete={hasOnlyOneQuery}
|
||||
/>
|
||||
))}
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={addQuery}
|
||||
isDisabled={hasEmptyQueries}
|
||||
data-test-subj="addCurationQueryButton"
|
||||
>
|
||||
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.addQueryButtonLabel', {
|
||||
defaultMessage: 'Add query',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={hasOnlyOneQuery && hasEmptyQueries}
|
||||
onClick={() => onSubmit(filterEmptyQueries(queries))}
|
||||
data-test-subj="submitCurationQueriesButton"
|
||||
>
|
||||
{submitButtonText}
|
||||
</EuiButton>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { resetContext } from 'kea';
|
||||
|
||||
import { CurationQueriesLogic } from './curation_queries_logic';
|
||||
|
||||
describe('CurationQueriesLogic', () => {
|
||||
const MOCK_QUERIES = ['a', 'b', 'c'];
|
||||
|
||||
const DEFAULT_PROPS = { queries: MOCK_QUERIES };
|
||||
const DEFAULT_VALUES = {
|
||||
queries: MOCK_QUERIES,
|
||||
hasEmptyQueries: false,
|
||||
hasOnlyOneQuery: false,
|
||||
};
|
||||
|
||||
const mount = (props = {}) => {
|
||||
CurationQueriesLogic({ ...DEFAULT_PROPS, ...props });
|
||||
CurationQueriesLogic.mount();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
resetContext({});
|
||||
});
|
||||
|
||||
it('has expected default values passed from props', () => {
|
||||
mount();
|
||||
expect(CurationQueriesLogic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
// Should not mutate the original array
|
||||
expect(CurationQueriesLogic.values.queries).not.toBe(MOCK_QUERIES); // Would fail if we did not clone a new array
|
||||
});
|
||||
|
||||
describe('addQuery', () => {
|
||||
it('appends an empty string to the queries array', () => {
|
||||
mount();
|
||||
CurationQueriesLogic.actions.addQuery();
|
||||
|
||||
expect(CurationQueriesLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
hasEmptyQueries: true,
|
||||
queries: ['a', 'b', 'c', ''],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteQuery', () => {
|
||||
it('deletes the query string at the specified array index', () => {
|
||||
mount();
|
||||
CurationQueriesLogic.actions.deleteQuery(1);
|
||||
|
||||
expect(CurationQueriesLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
queries: ['a', 'c'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('editQuery', () => {
|
||||
it('edits the query string at the specified array index', () => {
|
||||
mount();
|
||||
CurationQueriesLogic.actions.editQuery(2, 'z');
|
||||
|
||||
expect(CurationQueriesLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
queries: ['a', 'b', 'z'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectors', () => {
|
||||
describe('hasEmptyQueries', () => {
|
||||
it('returns true if queries has any empty strings', () => {
|
||||
mount({ queries: ['', '', ''] });
|
||||
|
||||
expect(CurationQueriesLogic.values.hasEmptyQueries).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasOnlyOneQuery', () => {
|
||||
it('returns true if queries only has one item', () => {
|
||||
mount({ queries: ['test'] });
|
||||
|
||||
expect(CurationQueriesLogic.values.hasOnlyOneQuery).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { kea, MakeLogicType } from 'kea';
|
||||
|
||||
interface CurationQueriesValues {
|
||||
queries: string[];
|
||||
hasEmptyQueries: boolean;
|
||||
hasOnlyOneQuery: boolean;
|
||||
}
|
||||
|
||||
interface CurationQueriesActions {
|
||||
addQuery(): void;
|
||||
deleteQuery(indexToDelete: number): { indexToDelete: number };
|
||||
editQuery(index: number, newQueryValue: string): { index: number; newQueryValue: string };
|
||||
}
|
||||
|
||||
export const CurationQueriesLogic = kea<
|
||||
MakeLogicType<CurationQueriesValues, CurationQueriesActions>
|
||||
>({
|
||||
path: ['enterprise_search', 'app_search', 'curation_queries_logic'],
|
||||
actions: () => ({
|
||||
addQuery: true,
|
||||
deleteQuery: (indexToDelete) => ({ indexToDelete }),
|
||||
editQuery: (index, newQueryValue) => ({ index, newQueryValue }),
|
||||
}),
|
||||
reducers: ({ props }) => ({
|
||||
queries: [
|
||||
props.queries,
|
||||
{
|
||||
addQuery: (state) => [...state, ''],
|
||||
deleteQuery: (state, { indexToDelete }) => {
|
||||
const newState = [...state];
|
||||
newState.splice(indexToDelete, 1);
|
||||
return newState;
|
||||
},
|
||||
editQuery: (state, { index, newQueryValue }) => {
|
||||
const newState = [...state];
|
||||
newState[index] = newQueryValue;
|
||||
return newState;
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectors: {
|
||||
hasEmptyQueries: [(selectors) => [selectors.queries], (queries) => queries.indexOf('') >= 0],
|
||||
hasOnlyOneQuery: [(selectors) => [selectors.queries], (queries) => queries.length <= 1],
|
||||
},
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
|
||||
import { CurationQuery } from './curation_query';
|
||||
|
||||
describe('CurationQuery', () => {
|
||||
const props = {
|
||||
queryValue: 'some query',
|
||||
onChange: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
disableDelete: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<CurationQuery {...props} />);
|
||||
|
||||
expect(wrapper.find(EuiFieldText)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiFieldText).prop('value')).toEqual('some query');
|
||||
});
|
||||
|
||||
it('calls onChange when the input value changes', () => {
|
||||
const wrapper = shallow(<CurationQuery {...props} />);
|
||||
wrapper.find(EuiFieldText).simulate('change', { target: { value: 'new query value' } });
|
||||
|
||||
expect(props.onChange).toHaveBeenCalledWith('new query value');
|
||||
});
|
||||
|
||||
it('calls onDelete when the delete button is clicked', () => {
|
||||
const wrapper = shallow(<CurationQuery {...props} />);
|
||||
wrapper.find('[data-test-subj="deleteCurationQueryButton"]').simulate('click');
|
||||
|
||||
expect(props.onDelete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables the delete button if disableDelete is passed', () => {
|
||||
const wrapper = shallow(<CurationQuery {...props} disableDelete />);
|
||||
const button = wrapper.find('[data-test-subj="deleteCurationQueryButton"]');
|
||||
|
||||
expect(button.prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface Props {
|
||||
queryValue: string;
|
||||
onChange(newValue: string): void;
|
||||
onDelete(): void;
|
||||
disableDelete: boolean;
|
||||
}
|
||||
|
||||
export const CurationQuery: React.FC<Props> = ({
|
||||
queryValue,
|
||||
onChange,
|
||||
onDelete,
|
||||
disableDelete,
|
||||
}) => (
|
||||
<EuiFlexGroup className="curationQueryRow" alignItems="center" responsive={false} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
placeholder={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.queryPlaceholder',
|
||||
{ defaultMessage: 'Enter a query' }
|
||||
)}
|
||||
value={queryValue}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={onDelete}
|
||||
isDisabled={disableDelete}
|
||||
aria-label={i18n.translate('xpack.enterpriseSearch.actions.delete', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
data-test-subj="deleteCurationQueryButton"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
|
@ -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 { CurationQueries } from './curation_queries';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { filterEmptyQueries } from './utils';
|
||||
|
||||
describe('filterEmptyQueries', () => {
|
||||
it('filters out all empty strings from a queries array', () => {
|
||||
const queries = ['', 'a', '', 'b', '', 'c', ''];
|
||||
expect(filterEmptyQueries(queries)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 const filterEmptyQueries = (queries: string[]) => {
|
||||
return queries.filter((query) => query.length);
|
||||
};
|
|
@ -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 { CurationQueries } from './curation_queries';
|
|
@ -5,7 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__';
|
||||
import {
|
||||
LogicMounter,
|
||||
mockHttpValues,
|
||||
mockKibanaValues,
|
||||
mockFlashMessageHelpers,
|
||||
} from '../../../__mocks__';
|
||||
import '../../__mocks__/engine_logic.mock';
|
||||
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
|
@ -17,6 +22,7 @@ import { CurationsLogic } from './';
|
|||
describe('CurationsLogic', () => {
|
||||
const { mount } = new LogicMounter(CurationsLogic);
|
||||
const { http } = mockHttpValues;
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
const { clearFlashMessages, setSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers;
|
||||
|
||||
const MOCK_CURATIONS_RESPONSE = {
|
||||
|
@ -128,7 +134,7 @@ describe('CurationsLogic', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('deleteCurationSet', () => {
|
||||
describe('deleteCuration', () => {
|
||||
const confirmSpy = jest.spyOn(window, 'confirm');
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -140,7 +146,7 @@ describe('CurationsLogic', () => {
|
|||
mount();
|
||||
jest.spyOn(CurationsLogic.actions, 'loadCurations');
|
||||
|
||||
CurationsLogic.actions.deleteCurationSet('some-curation-id');
|
||||
CurationsLogic.actions.deleteCuration('some-curation-id');
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
||||
|
@ -155,7 +161,7 @@ describe('CurationsLogic', () => {
|
|||
http.delete.mockReturnValueOnce(Promise.reject('error'));
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.deleteCurationSet('some-curation-id');
|
||||
CurationsLogic.actions.deleteCuration('some-curation-id');
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
||||
|
@ -166,12 +172,39 @@ describe('CurationsLogic', () => {
|
|||
confirmSpy.mockImplementationOnce(() => false);
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.deleteCurationSet('some-curation-id');
|
||||
CurationsLogic.actions.deleteCuration('some-curation-id');
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
||||
expect(http.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCuration', () => {
|
||||
it('should make an API call and navigate to the new curation', async () => {
|
||||
http.post.mockReturnValueOnce(Promise.resolve({ id: 'some-cur-id' }));
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.createCuration(['some query']);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith('/api/app_search/engines/some-engine/curations', {
|
||||
body: '{"queries":["some query"]}',
|
||||
});
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/some-cur-id');
|
||||
});
|
||||
|
||||
it('handles errors', async () => {
|
||||
http.post.mockReturnValueOnce(Promise.reject('error'));
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.createCuration(['some query']);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,8 +15,10 @@ import {
|
|||
flashAPIErrors,
|
||||
} from '../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
import { updateMetaPageIndex } from '../../../shared/table_pagination';
|
||||
import { EngineLogic } from '../engine';
|
||||
import { ENGINE_CURATION_PATH } from '../../routes';
|
||||
import { EngineLogic, generateEnginePath } from '../engine';
|
||||
|
||||
import { DELETE_MESSAGE, SUCCESS_MESSAGE } from './constants';
|
||||
import { Curation, CurationsAPIResponse } from './types';
|
||||
|
@ -31,7 +33,8 @@ interface CurationsActions {
|
|||
onCurationsLoad(response: CurationsAPIResponse): CurationsAPIResponse;
|
||||
onPaginate(newPageIndex: number): { newPageIndex: number };
|
||||
loadCurations(): void;
|
||||
deleteCurationSet(id: string): string;
|
||||
deleteCuration(id: string): string;
|
||||
createCuration(queries: Curation['queries']): Curation['queries'];
|
||||
}
|
||||
|
||||
export const CurationsLogic = kea<MakeLogicType<CurationsValues, CurationsActions>>({
|
||||
|
@ -40,7 +43,8 @@ export const CurationsLogic = kea<MakeLogicType<CurationsValues, CurationsAction
|
|||
onCurationsLoad: ({ results, meta }) => ({ results, meta }),
|
||||
onPaginate: (newPageIndex) => ({ newPageIndex }),
|
||||
loadCurations: true,
|
||||
deleteCurationSet: (id) => id,
|
||||
deleteCuration: (id) => id,
|
||||
createCuration: (queries) => queries,
|
||||
}),
|
||||
reducers: () => ({
|
||||
dataLoading: [
|
||||
|
@ -82,7 +86,7 @@ export const CurationsLogic = kea<MakeLogicType<CurationsValues, CurationsAction
|
|||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
deleteCurationSet: async (id) => {
|
||||
deleteCuration: async (id) => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { engineName } = EngineLogic.values;
|
||||
clearFlashMessages();
|
||||
|
@ -97,5 +101,20 @@ export const CurationsLogic = kea<MakeLogicType<CurationsValues, CurationsAction
|
|||
}
|
||||
}
|
||||
},
|
||||
createCuration: async (queries) => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { engineName } = EngineLogic.values;
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
clearFlashMessages();
|
||||
|
||||
try {
|
||||
const response = await http.post(`/api/app_search/engines/${engineName}/curations`, {
|
||||
body: JSON.stringify({ queries }),
|
||||
});
|
||||
navigateToUrl(generateEnginePath(ENGINE_CURATION_PATH, { curationId: response.id }));
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -19,8 +19,8 @@ import {
|
|||
ENGINE_CURATION_ADD_RESULT_PATH,
|
||||
} from '../../routes';
|
||||
|
||||
import { CURATIONS_TITLE } from './constants';
|
||||
import { Curations } from './views';
|
||||
import { CURATIONS_TITLE, CREATE_NEW_CURATION_TITLE } from './constants';
|
||||
import { Curations, CurationCreation } from './views';
|
||||
|
||||
interface Props {
|
||||
engineBreadcrumb: BreadcrumbTrail;
|
||||
|
@ -35,8 +35,8 @@ export const CurationsRouter: React.FC<Props> = ({ engineBreadcrumb }) => {
|
|||
<Curations />
|
||||
</Route>
|
||||
<Route exact path={ENGINE_CURATIONS_NEW_PATH}>
|
||||
<SetPageChrome trail={[...CURATIONS_BREADCRUMB, 'Create a curation']} />
|
||||
TODO: Curation creation view
|
||||
<SetPageChrome trail={[...CURATIONS_BREADCRUMB, CREATE_NEW_CURATION_TITLE]} />
|
||||
<CurationCreation />
|
||||
</Route>
|
||||
<Route exact path={ENGINE_CURATION_PATH}>
|
||||
<SetPageChrome trail={[...CURATIONS_BREADCRUMB, 'curation queries']} />
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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__';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { CurationQueries } from '../components';
|
||||
|
||||
import { CurationCreation } from './curation_creation';
|
||||
|
||||
describe('CurationCreation', () => {
|
||||
const actions = {
|
||||
createCuration: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockActions(actions);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<CurationCreation />);
|
||||
|
||||
expect(wrapper.find(CurationQueries)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('calls createCuration on CurationQueries submit', () => {
|
||||
const wrapper = shallow(<CurationCreation />);
|
||||
wrapper.find(CurationQueries).simulate('submit', ['some query']);
|
||||
|
||||
expect(actions.createCuration).toHaveBeenCalledWith(['some query']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { EuiPageHeader, EuiPageContent, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FlashMessages } from '../../../../shared/flash_messages';
|
||||
|
||||
import { CurationQueries } from '../components';
|
||||
import { CREATE_NEW_CURATION_TITLE } from '../constants';
|
||||
import { CurationsLogic } from '../index';
|
||||
|
||||
export const CurationCreation: React.FC = () => {
|
||||
const { createCuration } = useActions(CurationsLogic);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageHeader pageTitle={CREATE_NEW_CURATION_TITLE} />
|
||||
<FlashMessages />
|
||||
<EuiPageContent>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.create.curationQueriesTitle',
|
||||
{ defaultMessage: 'Curation queries' }
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.create.curationQueriesDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Add one or multiple queries to curate. You will be able add or remove more queries later.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<CurationQueries queries={['']} onSubmit={(queries) => createCuration(queries)} />
|
||||
</EuiPageContent>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -51,7 +51,7 @@ describe('Curations', () => {
|
|||
|
||||
const actions = {
|
||||
loadCurations: jest.fn(),
|
||||
deleteCurationSet: jest.fn(),
|
||||
deleteCuration: jest.fn(),
|
||||
onPaginate: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -134,12 +134,12 @@ describe('Curations', () => {
|
|||
expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-2');
|
||||
});
|
||||
|
||||
it('delete action calls deleteCurationSet', () => {
|
||||
it('delete action calls deleteCuration', () => {
|
||||
wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').first().simulate('click');
|
||||
expect(actions.deleteCurationSet).toHaveBeenCalledWith('cur-id-1');
|
||||
expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-1');
|
||||
|
||||
wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').last().simulate('click');
|
||||
expect(actions.deleteCurationSet).toHaveBeenCalledWith('cur-id-2');
|
||||
expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -69,7 +69,7 @@ export const Curations: React.FC = () => {
|
|||
|
||||
export const CurationsTable: React.FC = () => {
|
||||
const { dataLoading, curations, meta } = useValues(CurationsLogic);
|
||||
const { onPaginate, deleteCurationSet } = useActions(CurationsLogic);
|
||||
const { onPaginate, deleteCuration } = useActions(CurationsLogic);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<Curation>> = [
|
||||
{
|
||||
|
@ -141,7 +141,7 @@ export const CurationsTable: React.FC = () => {
|
|||
type: 'icon',
|
||||
icon: 'trash',
|
||||
color: 'danger',
|
||||
onClick: (curation: Curation) => deleteCurationSet(curation.id),
|
||||
onClick: (curation: Curation) => deleteCuration(curation.id),
|
||||
'data-test-subj': 'CurationsTableDeleteButton',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export { Curations } from './curations';
|
||||
export { CurationCreation } from './curation_creation';
|
||||
|
|
|
@ -50,6 +50,63 @@ describe('curations routes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('POST /api/app_search/engines/{engineName}/curations', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRouter = new MockRouter({
|
||||
method: 'post',
|
||||
path: '/api/app_search/engines/{engineName}/curations',
|
||||
});
|
||||
|
||||
registerCurationsRoutes({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/as/engines/:engineName/curations/collection',
|
||||
});
|
||||
});
|
||||
|
||||
describe('validates', () => {
|
||||
it('with curation queries', () => {
|
||||
const request = {
|
||||
body: {
|
||||
queries: ['a', 'b', 'c'],
|
||||
},
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
|
||||
it('empty queries array', () => {
|
||||
const request = {
|
||||
body: {
|
||||
queries: [],
|
||||
},
|
||||
};
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
|
||||
it('empty query strings', () => {
|
||||
const request = {
|
||||
body: {
|
||||
queries: ['', '', ''],
|
||||
},
|
||||
};
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
|
||||
it('missing queries', () => {
|
||||
const request = { body: {} };
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/app_search/engines/{engineName}/curations/{curationId}', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
|
|
|
@ -31,6 +31,23 @@ export function registerCurationsRoutes({
|
|||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: '/api/app_search/engines/{engineName}/curations',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
engineName: schema.string(),
|
||||
}),
|
||||
body: schema.object({
|
||||
queries: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/as/engines/:engineName/curations/collection',
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
{
|
||||
path: '/api/app_search/engines/{engineName}/curations/{curationId}',
|
||||
|
|
Loading…
Reference in a new issue