mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Add server API routes * Add CurationsLogic file - w/ listeners for overview table only + types/constants setup * Add Curations overview + table & update router to show view * Test feedback - test names, unnecessary beforeAll mocks * i18n feedback Co-authored-by: Constance <constancecchen@users.noreply.github.com>
This commit is contained in:
parent
35cfcbef8f
commit
eaeb0ec9f1
11 changed files with 778 additions and 1 deletions
|
@ -11,3 +11,20 @@ export const CURATIONS_TITLE = i18n.translate(
|
|||
'xpack.enterpriseSearch.appSearch.engine.curations.title',
|
||||
{ defaultMessage: 'Curations' }
|
||||
);
|
||||
export const CURATIONS_OVERVIEW_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.overview.title',
|
||||
{ defaultMessage: 'Curated results' }
|
||||
);
|
||||
export const CREATE_NEW_CURATION_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.create.title',
|
||||
{ defaultMessage: 'Create new curation' }
|
||||
);
|
||||
|
||||
export const DELETE_MESSAGE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.deleteConfirmation',
|
||||
{ defaultMessage: 'Are you sure you want to remove this curation?' }
|
||||
);
|
||||
export const SUCCESS_MESSAGE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.deleteSuccessMessage',
|
||||
{ defaultMessage: 'Successfully removed curation.' }
|
||||
);
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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 { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__';
|
||||
import '../../__mocks__/engine_logic.mock';
|
||||
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
|
||||
import { DEFAULT_META } from '../../../shared/constants';
|
||||
|
||||
import { CurationsLogic } from './';
|
||||
|
||||
describe('CurationsLogic', () => {
|
||||
const { mount } = new LogicMounter(CurationsLogic);
|
||||
const { http } = mockHttpValues;
|
||||
const { clearFlashMessages, setSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers;
|
||||
|
||||
const MOCK_CURATIONS_RESPONSE = {
|
||||
meta: {
|
||||
page: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
total_results: 1,
|
||||
total_pages: 1,
|
||||
},
|
||||
},
|
||||
results: [
|
||||
{
|
||||
id: 'some-curation-id',
|
||||
last_updated: 'January 1, 1970 at 12:00PM',
|
||||
queries: ['some query'],
|
||||
promoted: [],
|
||||
hidden: [],
|
||||
organic: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
dataLoading: true,
|
||||
curations: [],
|
||||
meta: DEFAULT_META,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('has expected default values', () => {
|
||||
mount();
|
||||
expect(CurationsLogic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('onCurationsLoad', () => {
|
||||
it('should set curations and meta state, & dataLoading to false', () => {
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.onCurationsLoad(MOCK_CURATIONS_RESPONSE);
|
||||
|
||||
expect(CurationsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
curations: MOCK_CURATIONS_RESPONSE.results,
|
||||
meta: MOCK_CURATIONS_RESPONSE.meta,
|
||||
dataLoading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onPaginate', () => {
|
||||
it('should set meta.page.current state', () => {
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.onPaginate(3);
|
||||
|
||||
expect(CurationsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
meta: { page: { ...DEFAULT_VALUES.meta.page, current: 3 } },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
describe('loadCurations', () => {
|
||||
it('should set dataLoading state', () => {
|
||||
mount({ dataLoading: false });
|
||||
|
||||
CurationsLogic.actions.loadCurations();
|
||||
|
||||
expect(CurationsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
dataLoading: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should make an API call and set curations & meta state', async () => {
|
||||
http.get.mockReturnValueOnce(Promise.resolve(MOCK_CURATIONS_RESPONSE));
|
||||
mount();
|
||||
jest.spyOn(CurationsLogic.actions, 'onCurationsLoad');
|
||||
|
||||
CurationsLogic.actions.loadCurations();
|
||||
await nextTick();
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/curations', {
|
||||
query: {
|
||||
'page[current]': 1,
|
||||
'page[size]': 10,
|
||||
},
|
||||
});
|
||||
expect(CurationsLogic.actions.onCurationsLoad).toHaveBeenCalledWith(
|
||||
MOCK_CURATIONS_RESPONSE
|
||||
);
|
||||
});
|
||||
|
||||
it('handles errors', async () => {
|
||||
http.get.mockReturnValueOnce(Promise.reject('error'));
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.loadCurations();
|
||||
await nextTick();
|
||||
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCurationSet', () => {
|
||||
const confirmSpy = jest.spyOn(window, 'confirm');
|
||||
|
||||
beforeEach(() => {
|
||||
confirmSpy.mockImplementation(jest.fn(() => true));
|
||||
});
|
||||
|
||||
it('should make an API call and show a success message', async () => {
|
||||
http.delete.mockReturnValueOnce(Promise.resolve({}));
|
||||
mount();
|
||||
jest.spyOn(CurationsLogic.actions, 'loadCurations');
|
||||
|
||||
CurationsLogic.actions.deleteCurationSet('some-curation-id');
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
||||
expect(http.delete).toHaveBeenCalledWith(
|
||||
'/api/app_search/engines/some-engine/curations/some-curation-id'
|
||||
);
|
||||
expect(CurationsLogic.actions.loadCurations).toHaveBeenCalled();
|
||||
expect(setSuccessMessage).toHaveBeenCalledWith('Successfully removed curation.');
|
||||
});
|
||||
|
||||
it('handles errors', async () => {
|
||||
http.delete.mockReturnValueOnce(Promise.reject('error'));
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.deleteCurationSet('some-curation-id');
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('error');
|
||||
});
|
||||
|
||||
it('does nothing if the user cancels the confirmation prompt', async () => {
|
||||
confirmSpy.mockImplementationOnce(() => false);
|
||||
mount();
|
||||
|
||||
CurationsLogic.actions.deleteCurationSet('some-curation-id');
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
await nextTick();
|
||||
|
||||
expect(http.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
import { Meta } from '../../../../../common/types';
|
||||
import { DEFAULT_META } from '../../../shared/constants';
|
||||
|
||||
import {
|
||||
clearFlashMessages,
|
||||
setSuccessMessage,
|
||||
flashAPIErrors,
|
||||
} from '../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
import { EngineLogic } from '../engine';
|
||||
|
||||
import { DELETE_MESSAGE, SUCCESS_MESSAGE } from './constants';
|
||||
import { Curation, CurationsAPIResponse } from './types';
|
||||
|
||||
interface CurationsValues {
|
||||
dataLoading: boolean;
|
||||
curations: Curation[];
|
||||
meta: Meta;
|
||||
}
|
||||
|
||||
interface CurationsActions {
|
||||
onCurationsLoad(response: CurationsAPIResponse): CurationsAPIResponse;
|
||||
onPaginate(newPageIndex: number): { newPageIndex: number };
|
||||
loadCurations(): void;
|
||||
deleteCurationSet(id: string): string;
|
||||
}
|
||||
|
||||
export const CurationsLogic = kea<MakeLogicType<CurationsValues, CurationsActions>>({
|
||||
path: ['enterprise_search', 'app_search', 'curations_logic'],
|
||||
actions: () => ({
|
||||
onCurationsLoad: ({ results, meta }) => ({ results, meta }),
|
||||
onPaginate: (newPageIndex) => ({ newPageIndex }),
|
||||
loadCurations: true,
|
||||
deleteCurationSet: (id) => id,
|
||||
}),
|
||||
reducers: () => ({
|
||||
dataLoading: [
|
||||
true,
|
||||
{
|
||||
loadCurations: () => true,
|
||||
onCurationsLoad: () => false,
|
||||
},
|
||||
],
|
||||
curations: [
|
||||
[],
|
||||
{
|
||||
onCurationsLoad: (_, { results }) => results,
|
||||
},
|
||||
],
|
||||
meta: [
|
||||
DEFAULT_META,
|
||||
{
|
||||
onCurationsLoad: (_, { meta }) => meta,
|
||||
onPaginate: (state, { newPageIndex }) => {
|
||||
const newState = { page: { ...state.page } };
|
||||
newState.page.current = newPageIndex;
|
||||
return newState;
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions, values }) => ({
|
||||
loadCurations: async () => {
|
||||
const { meta } = values;
|
||||
const { http } = HttpLogic.values;
|
||||
const { engineName } = EngineLogic.values;
|
||||
|
||||
try {
|
||||
const response = await http.get(`/api/app_search/engines/${engineName}/curations`, {
|
||||
query: {
|
||||
'page[current]': meta.page.current,
|
||||
'page[size]': meta.page.size,
|
||||
},
|
||||
});
|
||||
actions.onCurationsLoad(response);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
deleteCurationSet: async (id) => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { engineName } = EngineLogic.values;
|
||||
clearFlashMessages();
|
||||
|
||||
if (window.confirm(DELETE_MESSAGE)) {
|
||||
try {
|
||||
await http.delete(`/api/app_search/engines/${engineName}/curations/${id}`);
|
||||
actions.loadCurations();
|
||||
setSuccessMessage(SUCCESS_MESSAGE);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '../../routes';
|
||||
|
||||
import { CURATIONS_TITLE } from './constants';
|
||||
import { Curations } from './views';
|
||||
|
||||
interface Props {
|
||||
engineBreadcrumb: BreadcrumbTrail;
|
||||
|
@ -31,7 +32,7 @@ export const CurationsRouter: React.FC<Props> = ({ engineBreadcrumb }) => {
|
|||
<Switch>
|
||||
<Route exact path={ENGINE_CURATIONS_PATH}>
|
||||
<SetPageChrome trail={CURATIONS_BREADCRUMB} />
|
||||
TODO: Curations overview
|
||||
<Curations />
|
||||
</Route>
|
||||
<Route exact path={ENGINE_CURATIONS_NEW_PATH}>
|
||||
<SetPageChrome trail={[...CURATIONS_BREADCRUMB, 'Create a curation']} />
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export { CURATIONS_TITLE } from './constants';
|
||||
export { CurationsRouter } from './curations_router';
|
||||
export { CurationsLogic } from './curations_logic';
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { Meta } from '../../../../../common/types';
|
||||
|
||||
export interface Curation {
|
||||
id: string;
|
||||
last_updated: string;
|
||||
queries: string[];
|
||||
promoted: object[];
|
||||
hidden: object[];
|
||||
organic: object[];
|
||||
}
|
||||
|
||||
export interface CurationsAPIResponse {
|
||||
results: Curation[];
|
||||
meta: Meta;
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__';
|
||||
import '../../../__mocks__/engine_logic.mock';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow, mount, ReactWrapper } from 'enzyme';
|
||||
|
||||
import { EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { Loading } from '../../../../shared/loading';
|
||||
|
||||
import { Curations, CurationsTable } from './curations';
|
||||
|
||||
describe('Curations', () => {
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
|
||||
const values = {
|
||||
dataLoading: false,
|
||||
curations: [
|
||||
{
|
||||
id: 'cur-id-1',
|
||||
last_updated: 'January 1, 1970 at 12:00PM',
|
||||
queries: ['hiking'],
|
||||
},
|
||||
{
|
||||
id: 'cur-id-2',
|
||||
last_updated: 'January 2, 1970 at 12:00PM',
|
||||
queries: ['mountains', 'valleys'],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
page: {
|
||||
current: 1,
|
||||
size: 10,
|
||||
total_results: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
loadCurations: jest.fn(),
|
||||
deleteCurationSet: jest.fn(),
|
||||
onPaginate: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockValues(values);
|
||||
setMockActions(actions);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<Curations />);
|
||||
|
||||
expect(wrapper.find('h1').text()).toEqual('Curated results');
|
||||
expect(wrapper.find(CurationsTable)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders a loading component on page load', () => {
|
||||
setMockValues({ ...values, dataLoading: true, curations: [] });
|
||||
const wrapper = shallow(<Curations />);
|
||||
|
||||
expect(wrapper.find(Loading)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('calls loadCurations on page load', () => {
|
||||
mount(<Curations />);
|
||||
|
||||
expect(actions.loadCurations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('CurationsTable', () => {
|
||||
it('renders an EuiEmptyPrompt if curations is empty', () => {
|
||||
setMockValues({ ...values, curations: [] });
|
||||
const wrapper = shallow(<CurationsTable />);
|
||||
|
||||
expect(wrapper.find(EuiBasicTable).prop('noItemsMessage').type).toEqual(EuiEmptyPrompt);
|
||||
});
|
||||
|
||||
it('passes loading prop based on dataLoading', () => {
|
||||
setMockValues({ ...values, dataLoading: true });
|
||||
const wrapper = shallow(<CurationsTable />);
|
||||
|
||||
expect(wrapper.find(EuiBasicTable).prop('loading')).toEqual(true);
|
||||
});
|
||||
|
||||
describe('populated table render', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
beforeAll(() => {
|
||||
wrapper = mount(<CurationsTable />);
|
||||
});
|
||||
|
||||
it('renders queries and last updated columns', () => {
|
||||
const tableContent = wrapper.find(EuiBasicTable).text();
|
||||
|
||||
expect(tableContent).toContain('Queries');
|
||||
expect(tableContent).toContain('hiking');
|
||||
expect(tableContent).toContain('mountains, valleys');
|
||||
|
||||
expect(tableContent).toContain('Last updated');
|
||||
expect(tableContent).toContain('January 1, 1970 at 12:00PM');
|
||||
expect(tableContent).toContain('January 2, 1970 at 12:00PM');
|
||||
});
|
||||
|
||||
it('renders queries with curation links', () => {
|
||||
expect(
|
||||
wrapper.find('EuiLinkTo[data-test-subj="CurationsTableQueriesLink"]').first().prop('to')
|
||||
).toEqual('/engines/some-engine/curations/cur-id-1');
|
||||
|
||||
expect(
|
||||
wrapper.find('EuiLinkTo[data-test-subj="CurationsTableQueriesLink"]').last().prop('to')
|
||||
).toEqual('/engines/some-engine/curations/cur-id-2');
|
||||
});
|
||||
|
||||
describe('action column', () => {
|
||||
it('edit action navigates to curation link', () => {
|
||||
wrapper.find('[data-test-subj="CurationsTableEditButton"]').first().simulate('click');
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-1');
|
||||
|
||||
wrapper.find('[data-test-subj="CurationsTableEditButton"]').last().simulate('click');
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-2');
|
||||
});
|
||||
|
||||
it('delete action calls deleteCurationSet', () => {
|
||||
wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').first().simulate('click');
|
||||
expect(actions.deleteCurationSet).toHaveBeenCalledWith('cur-id-1');
|
||||
|
||||
wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').last().simulate('click');
|
||||
expect(actions.deleteCurationSet).toHaveBeenCalledWith('cur-id-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('passes pagination props from meta.page', () => {
|
||||
setMockValues({
|
||||
...values,
|
||||
meta: {
|
||||
page: {
|
||||
current: 5,
|
||||
size: 10,
|
||||
total_results: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = shallow(<CurationsTable />);
|
||||
|
||||
expect(wrapper.find(EuiBasicTable).prop('pagination')).toEqual({
|
||||
pageIndex: 4,
|
||||
pageSize: 10,
|
||||
totalItemCount: 50,
|
||||
hidePerPageOptions: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onPaginate on pagination change', () => {
|
||||
const wrapper = shallow(<CurationsTable />);
|
||||
wrapper.find(EuiBasicTable).simulate('change', { page: { index: 0 } });
|
||||
|
||||
expect(actions.onPaginate).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
|
||||
import { useValues, useActions } from 'kea';
|
||||
|
||||
import {
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiPageContent,
|
||||
EuiTitle,
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FlashMessages } from '../../../../shared/flash_messages';
|
||||
import { KibanaLogic } from '../../../../shared/kibana';
|
||||
import { Loading } from '../../../../shared/loading';
|
||||
import { EuiButtonTo, EuiLinkTo } from '../../../../shared/react_router_helpers';
|
||||
import { ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH } from '../../../routes';
|
||||
import { generateEnginePath } from '../../engine';
|
||||
|
||||
import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants';
|
||||
import { CurationsLogic } from '../curations_logic';
|
||||
import { Curation } from '../types';
|
||||
|
||||
export const Curations: React.FC = () => {
|
||||
const { dataLoading, curations, meta } = useValues(CurationsLogic);
|
||||
const { loadCurations } = useActions(CurationsLogic);
|
||||
|
||||
useEffect(() => {
|
||||
loadCurations();
|
||||
}, [meta.page.current]);
|
||||
|
||||
if (dataLoading && !curations.length) return <Loading />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>{CURATIONS_OVERVIEW_TITLE}</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiButtonTo to={generateEnginePath(ENGINE_CURATIONS_NEW_PATH)} fill>
|
||||
{CREATE_NEW_CURATION_TITLE}
|
||||
</EuiButtonTo>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<FlashMessages />
|
||||
<CurationsTable />
|
||||
</EuiPageContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CurationsTable: React.FC = () => {
|
||||
const { dataLoading, curations, meta } = useValues(CurationsLogic);
|
||||
const { onPaginate, deleteCurationSet } = useActions(CurationsLogic);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<Curation>> = [
|
||||
{
|
||||
field: 'queries',
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.table.column.queries',
|
||||
{ defaultMessage: 'Queries' }
|
||||
),
|
||||
render: (queries: Curation['queries'], curation: Curation) => (
|
||||
<EuiLinkTo
|
||||
data-test-subj="CurationsTableQueriesLink"
|
||||
to={generateEnginePath(ENGINE_CURATION_PATH, { curationId: curation.id })}
|
||||
>
|
||||
{queries.join(', ')}
|
||||
</EuiLinkTo>
|
||||
),
|
||||
width: '40%',
|
||||
truncateText: true,
|
||||
mobileOptions: {
|
||||
header: true,
|
||||
// Note: the below props are valid props per https://elastic.github.io/eui/#/tabular-content/tables (Responsive tables), but EUI's types have a bug reporting it as an error
|
||||
// @ts-ignore
|
||||
enlarge: true,
|
||||
width: '100%',
|
||||
truncateText: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'last_updated',
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.table.column.lastUpdated',
|
||||
{ defaultMessage: 'Last updated' }
|
||||
),
|
||||
width: '30%',
|
||||
dataType: 'string',
|
||||
},
|
||||
{
|
||||
width: '120px',
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.table.editAction',
|
||||
{ defaultMessage: 'Edit' }
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.table.editTooltip',
|
||||
{ defaultMessage: 'Edit curation' }
|
||||
),
|
||||
type: 'icon',
|
||||
icon: 'pencil',
|
||||
color: 'primary',
|
||||
onClick: (curation: Curation) => {
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
const url = generateEnginePath(ENGINE_CURATION_PATH, { curationId: curation.id });
|
||||
navigateToUrl(url);
|
||||
},
|
||||
'data-test-subj': 'CurationsTableEditButton',
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.table.deleteAction',
|
||||
{ defaultMessage: 'Delete' }
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.table.deleteTooltip',
|
||||
{ defaultMessage: 'Delete curation' }
|
||||
),
|
||||
type: 'icon',
|
||||
icon: 'trash',
|
||||
color: 'danger',
|
||||
onClick: (curation: Curation) => deleteCurationSet(curation.id),
|
||||
'data-test-subj': 'CurationsTableDeleteButton',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
columns={columns}
|
||||
items={curations}
|
||||
responsive
|
||||
hasActions
|
||||
loading={dataLoading}
|
||||
noItemsMessage={
|
||||
<EuiEmptyPrompt
|
||||
iconType="pin"
|
||||
title={
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.table.empty.noCurationsTitle',
|
||||
{ defaultMessage: 'No curations yet' }
|
||||
)}
|
||||
</h4>
|
||||
}
|
||||
/>
|
||||
}
|
||||
pagination={{
|
||||
pageIndex: meta.page.current - 1,
|
||||
pageSize: meta.page.size,
|
||||
totalItemCount: meta.page.total_results,
|
||||
hidePerPageOptions: true,
|
||||
}}
|
||||
onChange={({ page }: { page: { index: number } }) => {
|
||||
const { index } = page;
|
||||
onPaginate(index + 1); // Note on paging - App Search's API pages start at 1, EuiBasicTables' pages start at 0
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 { Curations } from './curations';
|
|
@ -10,6 +10,69 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks_
|
|||
import { registerCurationsRoutes } from './curations';
|
||||
|
||||
describe('curations routes', () => {
|
||||
describe('GET /api/app_search/engines/{engineName}/curations', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRouter = new MockRouter({
|
||||
method: 'get',
|
||||
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 pagination query params', () => {
|
||||
const request = {
|
||||
query: {
|
||||
'page[current]': 1,
|
||||
'page[size]': 10,
|
||||
},
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
|
||||
it('missing query params', () => {
|
||||
const request = { query: {} };
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/app_search/engines/{engineName}/curations/{curationId}', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRouter = new MockRouter({
|
||||
method: 'delete',
|
||||
path: '/api/app_search/engines/{engineName}/curations/{curationId}',
|
||||
});
|
||||
|
||||
registerCurationsRoutes({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/as/engines/:engineName/curations/:curationId',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/app_search/engines/{engineName}/curations/find_or_create', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
|
|
|
@ -13,6 +13,39 @@ export function registerCurationsRoutes({
|
|||
router,
|
||||
enterpriseSearchRequestHandler,
|
||||
}: RouteDependencies) {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/app_search/engines/{engineName}/curations',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
engineName: schema.string(),
|
||||
}),
|
||||
query: schema.object({
|
||||
'page[current]': schema.number(),
|
||||
'page[size]': schema.number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/as/engines/:engineName/curations/collection',
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
{
|
||||
path: '/api/app_search/engines/{engineName}/curations/{curationId}',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
engineName: schema.string(),
|
||||
curationId: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/as/engines/:engineName/curations/:curationId',
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/api/app_search/engines/{engineName}/curations/find_or_create',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue