mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[App Search] Automated Curations: Split Curations view into "Overview" and "Settings" tabs (#112488) (#112587)
Co-authored-by: Byron Hulcher <byronhulcher@gmail.com>
This commit is contained in:
parent
21bf2818ae
commit
cd89cabf6a
13 changed files with 491 additions and 207 deletions
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
* 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__/kea_logic';
|
||||||
|
import '../../../../__mocks__/react_router';
|
||||||
|
import '../../../__mocks__/engine_logic.mock';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { shallow, ReactWrapper } from 'enzyme';
|
||||||
|
|
||||||
|
import { EuiBasicTable } from '@elastic/eui';
|
||||||
|
|
||||||
|
import { mountWithIntl } from '../../../../test_helpers';
|
||||||
|
|
||||||
|
import { CurationsTable } from './curations_table';
|
||||||
|
|
||||||
|
describe('CurationsTable', () => {
|
||||||
|
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 = {
|
||||||
|
deleteCuration: jest.fn(),
|
||||||
|
onPaginate: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
setMockValues(values);
|
||||||
|
setMockActions(actions);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = mountWithIntl(<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('Jan 1, 1970 12:00 PM');
|
||||||
|
expect(tableContent).toContain('Jan 2, 1970 12:00 PM');
|
||||||
|
});
|
||||||
|
|
||||||
|
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 deleteCuration', () => {
|
||||||
|
wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').first().simulate('click');
|
||||||
|
expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-1');
|
||||||
|
|
||||||
|
wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').last().simulate('click');
|
||||||
|
expect(actions.deleteCuration).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,117 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants';
|
||||||
|
import { KibanaLogic } from '../../../../shared/kibana';
|
||||||
|
import { EuiLinkTo } from '../../../../shared/react_router_helpers';
|
||||||
|
import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination';
|
||||||
|
|
||||||
|
import { ENGINE_CURATION_PATH } from '../../../routes';
|
||||||
|
import { FormattedDateTime } from '../../../utils/formatted_date_time';
|
||||||
|
import { generateEnginePath } from '../../engine';
|
||||||
|
|
||||||
|
import { CurationsLogic } from '../curations_logic';
|
||||||
|
import { Curation } from '../types';
|
||||||
|
import { convertToDate } from '../utils';
|
||||||
|
|
||||||
|
export const CurationsTable: React.FC = () => {
|
||||||
|
const { dataLoading, curations, meta } = useValues(CurationsLogic);
|
||||||
|
const { onPaginate, deleteCuration } = 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',
|
||||||
|
render: (dateString: string) => <FormattedDateTime date={convertToDate(dateString)} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: '120px',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: EDIT_BUTTON_LABEL,
|
||||||
|
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: DELETE_BUTTON_LABEL,
|
||||||
|
description: i18n.translate(
|
||||||
|
'xpack.enterpriseSearch.appSearch.engine.curations.table.deleteTooltip',
|
||||||
|
{ defaultMessage: 'Delete curation' }
|
||||||
|
),
|
||||||
|
type: 'icon',
|
||||||
|
icon: 'trash',
|
||||||
|
color: 'danger',
|
||||||
|
onClick: (curation: Curation) => deleteCuration(curation.id),
|
||||||
|
'data-test-subj': 'CurationsTableDeleteButton',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiBasicTable
|
||||||
|
columns={columns}
|
||||||
|
items={curations}
|
||||||
|
responsive
|
||||||
|
hasActions
|
||||||
|
loading={dataLoading}
|
||||||
|
pagination={{
|
||||||
|
...convertMetaToPagination(meta),
|
||||||
|
hidePerPageOptions: true,
|
||||||
|
}}
|
||||||
|
onChange={handlePageChange(onPaginate)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,4 +5,5 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { CurationsTable } from './curations_table';
|
||||||
export { EmptyState } from './empty_state';
|
export { EmptyState } from './empty_state';
|
||||||
|
|
|
@ -50,6 +50,7 @@ describe('CurationsLogic', () => {
|
||||||
dataLoading: true,
|
dataLoading: true,
|
||||||
curations: [],
|
curations: [],
|
||||||
meta: DEFAULT_META,
|
meta: DEFAULT_META,
|
||||||
|
selectedPageTab: 'overview',
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -89,6 +90,19 @@ describe('CurationsLogic', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onSelectPageTab', () => {
|
||||||
|
it('should set the selected page tab', () => {
|
||||||
|
mount();
|
||||||
|
|
||||||
|
CurationsLogic.actions.onSelectPageTab('settings');
|
||||||
|
|
||||||
|
expect(CurationsLogic.values).toEqual({
|
||||||
|
...DEFAULT_VALUES,
|
||||||
|
selectedPageTab: 'settings',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('listeners', () => {
|
describe('listeners', () => {
|
||||||
|
|
|
@ -23,10 +23,13 @@ import { EngineLogic, generateEnginePath } from '../engine';
|
||||||
import { DELETE_MESSAGE, SUCCESS_MESSAGE } from './constants';
|
import { DELETE_MESSAGE, SUCCESS_MESSAGE } from './constants';
|
||||||
import { Curation, CurationsAPIResponse } from './types';
|
import { Curation, CurationsAPIResponse } from './types';
|
||||||
|
|
||||||
|
type CurationsPageTabs = 'overview' | 'settings';
|
||||||
|
|
||||||
interface CurationsValues {
|
interface CurationsValues {
|
||||||
dataLoading: boolean;
|
dataLoading: boolean;
|
||||||
curations: Curation[];
|
curations: Curation[];
|
||||||
meta: Meta;
|
meta: Meta;
|
||||||
|
selectedPageTab: CurationsPageTabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CurationsActions {
|
interface CurationsActions {
|
||||||
|
@ -35,6 +38,7 @@ interface CurationsActions {
|
||||||
loadCurations(): void;
|
loadCurations(): void;
|
||||||
deleteCuration(id: string): string;
|
deleteCuration(id: string): string;
|
||||||
createCuration(queries: Curation['queries']): Curation['queries'];
|
createCuration(queries: Curation['queries']): Curation['queries'];
|
||||||
|
onSelectPageTab(pageTab: CurationsPageTabs): { pageTab: CurationsPageTabs };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CurationsLogic = kea<MakeLogicType<CurationsValues, CurationsActions>>({
|
export const CurationsLogic = kea<MakeLogicType<CurationsValues, CurationsActions>>({
|
||||||
|
@ -45,8 +49,15 @@ export const CurationsLogic = kea<MakeLogicType<CurationsValues, CurationsAction
|
||||||
loadCurations: true,
|
loadCurations: true,
|
||||||
deleteCuration: (id) => id,
|
deleteCuration: (id) => id,
|
||||||
createCuration: (queries) => queries,
|
createCuration: (queries) => queries,
|
||||||
|
onSelectPageTab: (pageTab) => ({ pageTab }),
|
||||||
}),
|
}),
|
||||||
reducers: () => ({
|
reducers: () => ({
|
||||||
|
selectedPageTab: [
|
||||||
|
'overview',
|
||||||
|
{
|
||||||
|
onSelectPageTab: (_, { pageTab }) => pageTab,
|
||||||
|
},
|
||||||
|
],
|
||||||
dataLoading: [
|
dataLoading: [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,23 +5,23 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
|
import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
|
||||||
import '../../../../__mocks__/react_router';
|
import '../../../../__mocks__/react_router';
|
||||||
import '../../../__mocks__/engine_logic.mock';
|
import '../../../__mocks__/engine_logic.mock';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { shallow, ReactWrapper } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import { EuiBasicTable } from '@elastic/eui';
|
import { EuiTab } from '@elastic/eui';
|
||||||
|
|
||||||
import { mountWithIntl, getPageTitle } from '../../../../test_helpers';
|
import { mountWithIntl, getPageHeaderTabs, getPageTitle } from '../../../../test_helpers';
|
||||||
|
|
||||||
import { Curations, CurationsTable } from './curations';
|
import { Curations } from './curations';
|
||||||
|
import { CurationsOverview } from './curations_overview';
|
||||||
|
import { CurationsSettings } from './curations_settings';
|
||||||
|
|
||||||
describe('Curations', () => {
|
describe('Curations', () => {
|
||||||
const { navigateToUrl } = mockKibanaValues;
|
|
||||||
|
|
||||||
const values = {
|
const values = {
|
||||||
dataLoading: false,
|
dataLoading: false,
|
||||||
curations: [
|
curations: [
|
||||||
|
@ -43,12 +43,13 @@ describe('Curations', () => {
|
||||||
total_results: 2,
|
total_results: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
selectedPageTab: 'overview',
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
loadCurations: jest.fn(),
|
loadCurations: jest.fn(),
|
||||||
deleteCuration: jest.fn(),
|
|
||||||
onPaginate: jest.fn(),
|
onPaginate: jest.fn(),
|
||||||
|
onSelectPageTab: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -57,11 +58,38 @@ describe('Curations', () => {
|
||||||
setMockActions(actions);
|
setMockActions(actions);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders', () => {
|
it('renders with a set of tabs in the page header', () => {
|
||||||
const wrapper = shallow(<Curations />);
|
const wrapper = shallow(<Curations />);
|
||||||
|
|
||||||
expect(getPageTitle(wrapper)).toEqual('Curated results');
|
expect(getPageTitle(wrapper)).toEqual('Curated results');
|
||||||
expect(wrapper.find(CurationsTable)).toHaveLength(1);
|
|
||||||
|
const tabs = getPageHeaderTabs(wrapper).find(EuiTab);
|
||||||
|
|
||||||
|
tabs.at(0).simulate('click');
|
||||||
|
expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(1, 'overview');
|
||||||
|
|
||||||
|
tabs.at(1).simulate('click');
|
||||||
|
expect(actions.onSelectPageTab).toHaveBeenNthCalledWith(2, 'settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders an overview view', () => {
|
||||||
|
setMockValues({ ...values, selectedPageTab: 'overview' });
|
||||||
|
const wrapper = shallow(<Curations />);
|
||||||
|
const tabs = getPageHeaderTabs(wrapper).find(EuiTab);
|
||||||
|
|
||||||
|
expect(tabs.at(0).prop('isSelected')).toEqual(true);
|
||||||
|
|
||||||
|
expect(wrapper.find(CurationsOverview)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a settings view', () => {
|
||||||
|
setMockValues({ ...values, selectedPageTab: 'settings' });
|
||||||
|
const wrapper = shallow(<Curations />);
|
||||||
|
const tabs = getPageHeaderTabs(wrapper).find(EuiTab);
|
||||||
|
|
||||||
|
expect(tabs.at(1).prop('isSelected')).toEqual(true);
|
||||||
|
|
||||||
|
expect(wrapper.find(CurationsSettings)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loading state', () => {
|
describe('loading state', () => {
|
||||||
|
@ -86,91 +114,4 @@ describe('Curations', () => {
|
||||||
|
|
||||||
expect(actions.loadCurations).toHaveBeenCalledTimes(1);
|
expect(actions.loadCurations).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CurationsTable', () => {
|
|
||||||
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 = mountWithIntl(<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('Jan 1, 1970 12:00 PM');
|
|
||||||
expect(tableContent).toContain('Jan 2, 1970 12:00 PM');
|
|
||||||
});
|
|
||||||
|
|
||||||
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 deleteCuration', () => {
|
|
||||||
wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').first().simulate('click');
|
|
||||||
expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-1');
|
|
||||||
|
|
||||||
wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').last().simulate('click');
|
|
||||||
expect(actions.deleteCuration).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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,28 +9,47 @@ import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { useValues, useActions } from 'kea';
|
import { useValues, useActions } from 'kea';
|
||||||
|
|
||||||
import { EuiBasicTable, EuiBasicTableColumn, EuiPanel } from '@elastic/eui';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants';
|
import { EuiButtonTo } from '../../../../shared/react_router_helpers';
|
||||||
import { KibanaLogic } from '../../../../shared/kibana';
|
|
||||||
import { EuiButtonTo, EuiLinkTo } from '../../../../shared/react_router_helpers';
|
|
||||||
import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination';
|
|
||||||
|
|
||||||
import { ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH } from '../../../routes';
|
import { ENGINE_CURATIONS_NEW_PATH } from '../../../routes';
|
||||||
import { FormattedDateTime } from '../../../utils/formatted_date_time';
|
|
||||||
import { generateEnginePath } from '../../engine';
|
import { generateEnginePath } from '../../engine';
|
||||||
import { AppSearchPageTemplate } from '../../layout';
|
import { AppSearchPageTemplate } from '../../layout';
|
||||||
|
|
||||||
import { EmptyState } from '../components';
|
|
||||||
import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants';
|
import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants';
|
||||||
import { CurationsLogic } from '../curations_logic';
|
import { CurationsLogic } from '../curations_logic';
|
||||||
import { Curation } from '../types';
|
import { getCurationsBreadcrumbs } from '../utils';
|
||||||
import { getCurationsBreadcrumbs, convertToDate } from '../utils';
|
|
||||||
|
import { CurationsOverview } from './curations_overview';
|
||||||
|
import { CurationsSettings } from './curations_settings';
|
||||||
|
|
||||||
export const Curations: React.FC = () => {
|
export const Curations: React.FC = () => {
|
||||||
const { dataLoading, curations, meta } = useValues(CurationsLogic);
|
const { dataLoading, curations, meta, selectedPageTab } = useValues(CurationsLogic);
|
||||||
const { loadCurations } = useActions(CurationsLogic);
|
const { loadCurations, onSelectPageTab } = useActions(CurationsLogic);
|
||||||
|
|
||||||
|
const pageTabs = [
|
||||||
|
{
|
||||||
|
label: i18n.translate(
|
||||||
|
'xpack.enterpriseSearch.appSearch.engine.curations.overviewPageTabLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Overview',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
isSelected: selectedPageTab === 'overview',
|
||||||
|
onClick: () => onSelectPageTab('overview'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.translate(
|
||||||
|
'xpack.enterpriseSearch.appSearch.engine.curations.settingsPageTabLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Settings',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
isSelected: selectedPageTab === 'settings',
|
||||||
|
onClick: () => onSelectPageTab('settings'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCurations();
|
loadCurations();
|
||||||
|
@ -50,105 +69,12 @@ export const Curations: React.FC = () => {
|
||||||
{CREATE_NEW_CURATION_TITLE}
|
{CREATE_NEW_CURATION_TITLE}
|
||||||
</EuiButtonTo>,
|
</EuiButtonTo>,
|
||||||
],
|
],
|
||||||
|
tabs: pageTabs,
|
||||||
}}
|
}}
|
||||||
isLoading={dataLoading && !curations.length}
|
isLoading={dataLoading && !curations.length}
|
||||||
isEmptyState={!curations.length}
|
|
||||||
emptyState={<EmptyState />}
|
|
||||||
>
|
>
|
||||||
<EuiPanel hasBorder>
|
{selectedPageTab === 'overview' && <CurationsOverview />}
|
||||||
<CurationsTable />
|
{selectedPageTab === 'settings' && <CurationsSettings />}
|
||||||
</EuiPanel>
|
|
||||||
</AppSearchPageTemplate>
|
</AppSearchPageTemplate>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CurationsTable: React.FC = () => {
|
|
||||||
const { dataLoading, curations, meta } = useValues(CurationsLogic);
|
|
||||||
const { onPaginate, deleteCuration } = 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',
|
|
||||||
render: (dateString: string) => <FormattedDateTime date={convertToDate(dateString)} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
width: '120px',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
name: EDIT_BUTTON_LABEL,
|
|
||||||
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: DELETE_BUTTON_LABEL,
|
|
||||||
description: i18n.translate(
|
|
||||||
'xpack.enterpriseSearch.appSearch.engine.curations.table.deleteTooltip',
|
|
||||||
{ defaultMessage: 'Delete curation' }
|
|
||||||
),
|
|
||||||
type: 'icon',
|
|
||||||
icon: 'trash',
|
|
||||||
color: 'danger',
|
|
||||||
onClick: (curation: Curation) => deleteCuration(curation.id),
|
|
||||||
'data-test-subj': 'CurationsTableDeleteButton',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiBasicTable
|
|
||||||
columns={columns}
|
|
||||||
items={curations}
|
|
||||||
responsive
|
|
||||||
hasActions
|
|
||||||
loading={dataLoading}
|
|
||||||
pagination={{
|
|
||||||
...convertMetaToPagination(meta),
|
|
||||||
hidePerPageOptions: true,
|
|
||||||
}}
|
|
||||||
onChange={handlePageChange(onPaginate)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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 { setMockValues } from '../../../../__mocks__/kea_logic';
|
||||||
|
import '../../../../__mocks__/react_router';
|
||||||
|
import '../../../__mocks__/engine_logic.mock';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { CurationsTable, EmptyState } from '../components';
|
||||||
|
|
||||||
|
import { CurationsOverview } from './curations_overview';
|
||||||
|
|
||||||
|
describe('CurationsOverview', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders an empty message when there are no curations', () => {
|
||||||
|
setMockValues({ curations: [] });
|
||||||
|
const wrapper = shallow(<CurationsOverview />);
|
||||||
|
|
||||||
|
expect(wrapper.is(EmptyState)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a curations table when there are curations present', () => {
|
||||||
|
setMockValues({
|
||||||
|
curations: [
|
||||||
|
{
|
||||||
|
id: 'cur-id-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cur-id-2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const wrapper = shallow(<CurationsOverview />);
|
||||||
|
|
||||||
|
expect(wrapper.find(CurationsTable)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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 } from 'kea';
|
||||||
|
|
||||||
|
import { EuiPanel } from '@elastic/eui';
|
||||||
|
|
||||||
|
import { CurationsTable, EmptyState } from '../components';
|
||||||
|
import { CurationsLogic } from '../curations_logic';
|
||||||
|
|
||||||
|
export const CurationsOverview: React.FC = () => {
|
||||||
|
const { curations } = useValues(CurationsLogic);
|
||||||
|
|
||||||
|
return curations.length ? (
|
||||||
|
<EuiPanel hasBorder>
|
||||||
|
<CurationsTable />
|
||||||
|
</EuiPanel>
|
||||||
|
) : (
|
||||||
|
<EmptyState />
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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 '../../../../__mocks__/react_router';
|
||||||
|
import '../../../__mocks__/engine_logic.mock';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { CurationsSettings } from './curations_settings';
|
||||||
|
|
||||||
|
describe('CurationsSettings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders empty', () => {
|
||||||
|
const wrapper = shallow(<CurationsSettings />);
|
||||||
|
|
||||||
|
expect(wrapper.isEmptyRender()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
export const CurationsSettings: React.FC = () => {
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -9,7 +9,7 @@ import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
|
||||||
import { EuiPageHeaderProps } from '@elastic/eui';
|
import { EuiPageHeaderProps, EuiTab } from '@elastic/eui';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Given an AppSearchPageTemplate or WorkplaceSearchPageTemplate, these
|
* Given an AppSearchPageTemplate or WorkplaceSearchPageTemplate, these
|
||||||
|
@ -35,13 +35,30 @@ export const getPageHeaderActions = (wrapper: ShallowWrapper) => {
|
||||||
|
|
||||||
return shallow(
|
return shallow(
|
||||||
<div>
|
<div>
|
||||||
{actions.map((action: React.ReactNode, i) => (
|
{actions.map((action, i) => (
|
||||||
<Fragment key={i}>{action}</Fragment>
|
<Fragment key={i}>{action}</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPageHeaderTabs = (wrapper: ShallowWrapper) => {
|
||||||
|
// The tabs prop of EuiPageHeader takes an `Array<EuiTabProps>`
|
||||||
|
// instead of an array of EuiTab jsx components
|
||||||
|
// These are then rendered inside of EuiPageHeader as EuiTabs
|
||||||
|
// See https://elastic.github.io/eui/#/layout/page-header#tabs-in-the-page-header
|
||||||
|
|
||||||
|
const tabs = getPageHeader(wrapper).tabs || [];
|
||||||
|
|
||||||
|
return shallow(
|
||||||
|
<div>
|
||||||
|
{tabs.map((tabProps, i) => (
|
||||||
|
<EuiTab {...tabProps} key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getPageHeaderChildren = (wrapper: ShallowWrapper) => {
|
export const getPageHeaderChildren = (wrapper: ShallowWrapper) => {
|
||||||
const children = getPageHeader(wrapper).children || null;
|
const children = getPageHeader(wrapper).children || null;
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ export {
|
||||||
getPageDescription,
|
getPageDescription,
|
||||||
getPageHeaderActions,
|
getPageHeaderActions,
|
||||||
getPageHeaderChildren,
|
getPageHeaderChildren,
|
||||||
|
getPageHeaderTabs,
|
||||||
} from './get_page_header';
|
} from './get_page_header';
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue