Skip to content

Commit 521c336

Browse files
author
Constance
authored
[App Search] Curation: set up server routes, API calls, & bare-bones view (#94341)
* Add server side API routes & update types expected from server * Create CurationLogic with GET and PUT listeners - PUT is mostly placeholder for now, we'll actually use it later in future Curation PRs * Create Curation view component & page load effect * Update CurationsRouter to use new view + remove add_result route - Per design discussion w/ Davey, we'll be removing the standalone add result route in favor of an in-page flyout
1 parent 8fc5d8b commit 521c336

File tree

12 files changed

+542
-15
lines changed

12 files changed

+542
-15
lines changed

x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export const CREATE_NEW_CURATION_TITLE = i18n.translate(
1919
'xpack.enterpriseSearch.appSearch.engine.curations.create.title',
2020
{ defaultMessage: 'Create new curation' }
2121
);
22+
export const MANAGE_CURATION_TITLE = i18n.translate(
23+
'xpack.enterpriseSearch.appSearch.engine.curations.manage.title',
24+
{ defaultMessage: 'Manage curation' }
25+
);
2226

2327
export const DELETE_MESSAGE = i18n.translate(
2428
'xpack.enterpriseSearch.appSearch.engine.curations.deleteConfirmation',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import '../../../../__mocks__/react_router_history.mock';
9+
import '../../../../__mocks__/shallow_useeffect.mock';
10+
import { setMockActions, setMockValues, rerender } from '../../../../__mocks__';
11+
12+
import React from 'react';
13+
import { useParams } from 'react-router-dom';
14+
15+
import { shallow } from 'enzyme';
16+
17+
import { EuiPageHeader } from '@elastic/eui';
18+
19+
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
20+
import { Loading } from '../../../../shared/loading';
21+
22+
jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() }));
23+
import { CurationLogic } from './curation_logic';
24+
25+
import { Curation } from './';
26+
27+
describe('Curation', () => {
28+
const props = {
29+
curationsBreadcrumb: ['Engines', 'some-engine', 'Curations'],
30+
};
31+
const values = {
32+
dataLoading: false,
33+
curation: {
34+
id: 'cur-123456789',
35+
queries: ['query A', 'query B'],
36+
},
37+
};
38+
const actions = {
39+
loadCuration: jest.fn(),
40+
};
41+
42+
beforeEach(() => {
43+
jest.clearAllMocks();
44+
setMockValues(values);
45+
setMockActions(actions);
46+
});
47+
48+
it('renders', () => {
49+
const wrapper = shallow(<Curation {...props} />);
50+
51+
expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Manage curation');
52+
expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([
53+
...props.curationsBreadcrumb,
54+
'query A, query B',
55+
]);
56+
});
57+
58+
it('renders a loading component on page load', () => {
59+
setMockValues({ ...values, dataLoading: true });
60+
const wrapper = shallow(<Curation {...props} />);
61+
62+
expect(wrapper.find(Loading)).toHaveLength(1);
63+
});
64+
65+
it('initializes CurationLogic with a curationId prop from URL param', () => {
66+
(useParams as jest.Mock).mockReturnValueOnce({ curationId: 'hello-world' });
67+
shallow(<Curation {...props} />);
68+
69+
expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' });
70+
});
71+
72+
it('calls loadCuration on page load & whenever the curationId URL param changes', () => {
73+
(useParams as jest.Mock).mockReturnValueOnce({ curationId: 'cur-123456789' });
74+
const wrapper = shallow(<Curation {...props} />);
75+
expect(actions.loadCuration).toHaveBeenCalledTimes(1);
76+
77+
(useParams as jest.Mock).mockReturnValueOnce({ curationId: 'cur-987654321' });
78+
rerender(wrapper);
79+
expect(actions.loadCuration).toHaveBeenCalledTimes(2);
80+
});
81+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { useEffect } from 'react';
9+
import { useParams } from 'react-router-dom';
10+
11+
import { useValues, useActions } from 'kea';
12+
13+
import { EuiPageHeader, EuiSpacer } from '@elastic/eui';
14+
15+
import { FlashMessages } from '../../../../shared/flash_messages';
16+
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
17+
import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
18+
import { Loading } from '../../../../shared/loading';
19+
20+
import { MANAGE_CURATION_TITLE } from '../constants';
21+
22+
import { CurationLogic } from './curation_logic';
23+
24+
interface Props {
25+
curationsBreadcrumb: BreadcrumbTrail;
26+
}
27+
28+
export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => {
29+
const { curationId } = useParams() as { curationId: string };
30+
const { loadCuration } = useActions(CurationLogic({ curationId }));
31+
const { dataLoading, curation } = useValues(CurationLogic({ curationId }));
32+
33+
useEffect(() => {
34+
loadCuration();
35+
}, [curationId]);
36+
37+
if (dataLoading) return <Loading />;
38+
39+
return (
40+
<>
41+
<SetPageChrome trail={[...curationsBreadcrumb, curation.queries.join(', ')]} />
42+
<EuiPageHeader
43+
pageTitle={MANAGE_CURATION_TITLE}
44+
/* TODO: Restore defaults button */
45+
responsive={false}
46+
/>
47+
48+
{/* TODO: Active query switcher / Manage queries modal */}
49+
50+
<EuiSpacer size="xl" />
51+
<FlashMessages />
52+
53+
{/* TODO: PromotedDocuments section */}
54+
{/* TODO: OrganicDocuments section */}
55+
{/* TODO: HiddenDocuments section */}
56+
57+
{/* TODO: AddResult flyout */}
58+
</>
59+
);
60+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import {
9+
LogicMounter,
10+
mockHttpValues,
11+
mockKibanaValues,
12+
mockFlashMessageHelpers,
13+
} from '../../../../__mocks__';
14+
import '../../../__mocks__/engine_logic.mock';
15+
16+
import { nextTick } from '@kbn/test/jest';
17+
18+
import { CurationLogic } from './';
19+
20+
describe('CurationLogic', () => {
21+
const { mount } = new LogicMounter(CurationLogic);
22+
const { http } = mockHttpValues;
23+
const { navigateToUrl } = mockKibanaValues;
24+
const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers;
25+
26+
const MOCK_CURATION_RESPONSE = {
27+
id: 'cur-123456789',
28+
last_updated: 'some timestamp',
29+
queries: ['some search'],
30+
promoted: [{ id: 'some-promoted-document' }],
31+
organic: [
32+
{
33+
id: { raw: 'some-organic-document', snippet: null },
34+
_meta: { id: 'some-organic-document', engine: 'some-engine' },
35+
},
36+
],
37+
hidden: [{ id: 'some-hidden-document' }],
38+
};
39+
40+
const DEFAULT_VALUES = {
41+
dataLoading: true,
42+
curation: {
43+
id: '',
44+
last_updated: '',
45+
queries: [],
46+
promoted: [],
47+
organic: [],
48+
hidden: [],
49+
},
50+
};
51+
52+
beforeEach(() => {
53+
jest.clearAllMocks();
54+
});
55+
56+
it('has expected default values', () => {
57+
mount();
58+
expect(CurationLogic.values).toEqual(DEFAULT_VALUES);
59+
});
60+
61+
describe('actions', () => {
62+
describe('onCurationLoad', () => {
63+
it('should set curation state & dataLoading to false', () => {
64+
mount();
65+
66+
CurationLogic.actions.onCurationLoad(MOCK_CURATION_RESPONSE);
67+
68+
expect(CurationLogic.values).toEqual({
69+
...DEFAULT_VALUES,
70+
curation: MOCK_CURATION_RESPONSE,
71+
dataLoading: false,
72+
});
73+
});
74+
});
75+
});
76+
77+
describe('listeners', () => {
78+
describe('loadCuration', () => {
79+
it('should set dataLoading state', () => {
80+
mount({ dataLoading: false }, { curationId: 'cur-123456789' });
81+
82+
CurationLogic.actions.loadCuration();
83+
84+
expect(CurationLogic.values).toEqual({
85+
...DEFAULT_VALUES,
86+
dataLoading: true,
87+
});
88+
});
89+
90+
it('should make an API call and set curation state', async () => {
91+
http.get.mockReturnValueOnce(Promise.resolve(MOCK_CURATION_RESPONSE));
92+
mount({}, { curationId: 'cur-123456789' });
93+
jest.spyOn(CurationLogic.actions, 'onCurationLoad');
94+
95+
CurationLogic.actions.loadCuration();
96+
await nextTick();
97+
98+
expect(http.get).toHaveBeenCalledWith(
99+
'/api/app_search/engines/some-engine/curations/cur-123456789'
100+
);
101+
expect(CurationLogic.actions.onCurationLoad).toHaveBeenCalledWith(MOCK_CURATION_RESPONSE);
102+
});
103+
104+
it('handles errors/404s with a redirect to the Curations view', async () => {
105+
http.get.mockReturnValueOnce(Promise.reject('error'));
106+
mount({}, { curationId: 'cur-404' });
107+
108+
CurationLogic.actions.loadCuration();
109+
await nextTick();
110+
111+
expect(flashAPIErrors).toHaveBeenCalledWith('error', { isQueued: true });
112+
expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations');
113+
});
114+
});
115+
116+
describe('updateCuration', () => {
117+
beforeAll(() => jest.useFakeTimers());
118+
afterAll(() => jest.useRealTimers());
119+
120+
it('should make a PUT API call with queries and promoted/hidden IDs to update', async () => {
121+
http.put.mockReturnValueOnce(Promise.resolve(MOCK_CURATION_RESPONSE));
122+
mount({}, { curationId: 'cur-123456789' });
123+
jest.spyOn(CurationLogic.actions, 'onCurationLoad');
124+
125+
CurationLogic.actions.updateCuration();
126+
jest.runAllTimers();
127+
await nextTick();
128+
129+
expect(http.put).toHaveBeenCalledWith(
130+
'/api/app_search/engines/some-engine/curations/cur-123456789',
131+
{
132+
body: '{"queries":[],"query":"","promoted":[],"hidden":[]}', // Uses state currently in CurationLogic
133+
}
134+
);
135+
expect(CurationLogic.actions.onCurationLoad).toHaveBeenCalledWith(MOCK_CURATION_RESPONSE);
136+
});
137+
138+
it('should allow passing a custom queries param', async () => {
139+
http.put.mockReturnValueOnce(Promise.resolve(MOCK_CURATION_RESPONSE));
140+
mount({}, { curationId: 'cur-123456789' });
141+
jest.spyOn(CurationLogic.actions, 'onCurationLoad');
142+
143+
CurationLogic.actions.updateCuration({ queries: ['hello', 'world'] });
144+
jest.runAllTimers();
145+
await nextTick();
146+
147+
expect(http.put).toHaveBeenCalledWith(
148+
'/api/app_search/engines/some-engine/curations/cur-123456789',
149+
{
150+
body: '{"queries":["hello","world"],"query":"","promoted":[],"hidden":[]}',
151+
}
152+
);
153+
expect(CurationLogic.actions.onCurationLoad).toHaveBeenCalledWith(MOCK_CURATION_RESPONSE);
154+
});
155+
156+
it('handles errors', async () => {
157+
http.put.mockReturnValueOnce(Promise.reject('error'));
158+
mount({}, { curationId: 'cur-123456789' });
159+
160+
CurationLogic.actions.updateCuration();
161+
jest.runAllTimers();
162+
await nextTick();
163+
164+
expect(clearFlashMessages).toHaveBeenCalled();
165+
expect(flashAPIErrors).toHaveBeenCalledWith('error');
166+
});
167+
});
168+
});
169+
});

0 commit comments

Comments
 (0)