[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:
Constance 2021-02-25 14:07:05 -08:00 committed by GitHub
parent fd348d3f82
commit 0198607eb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 714 additions and 19 deletions

View file

@ -0,0 +1,3 @@
.curationQueryRow {
margin-bottom: $euiSizeXS;
}

View file

@ -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);
});
});

View file

@ -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>
</>
);
};

View file

@ -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);
});
});
});
});

View file

@ -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],
},
});

View file

@ -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);
});
});

View file

@ -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>
);

View file

@ -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';

View file

@ -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']);
});
});

View file

@ -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);
};

View file

@ -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';

View file

@ -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');
});
});
});
});

View file

@ -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);
}
},
}),
});

View file

@ -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']} />

View file

@ -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']);
});
});

View file

@ -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>
</>
);
};

View file

@ -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');
});
});
});

View file

@ -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',
},
],

View file

@ -6,3 +6,4 @@
*/
export { Curations } from './curations';
export { CurationCreation } from './curation_creation';

View file

@ -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;

View file

@ -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}',