[App Search] Convert Curations pages to new page template (#102835)

* Update CurationRouter

- Remove breadcrumbs set in router (will get set by page template)
- Set up a curation breadcrumb helper for DRYness
- Remove NotFound route - curation ID 404 handling will be used instead

* Convert Curations page to new page template
+ move Empty State from table to top level

* Convert Curation creation page to new page template

* Convert single Curation page to new page template

+ remove breadcrumb prop

* Update router

* [Polish] Copy changes from Davey

- see https://github.com/elastic/kibana/pull/101958/files

- Per https://elastic.github.io/eui/#/guidelines/writing we shouldn't be using "new", so I removed that also

* [UI polish] Add plus icon to create button

- To match other create buttons across app
This commit is contained in:
Constance 2021-06-22 17:35:00 -07:00 committed by GitHub
parent cf12c031cf
commit e582549500
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 110 additions and 114 deletions

View file

@ -18,7 +18,7 @@ export const CURATIONS_OVERVIEW_TITLE = i18n.translate(
);
export const CREATE_NEW_CURATION_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.create.title',
{ defaultMessage: 'Create new curation' }
{ defaultMessage: 'Create a curation' }
);
export const MANAGE_CURATION_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.manage.title',

View file

@ -8,16 +8,13 @@
import '../../../../__mocks__/shallow_useeffect.mock';
import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../../__mocks__/react_router';
import '../../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { EuiPageHeader } from '@elastic/eui';
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
import { Loading } from '../../../../shared/loading';
import { rerender } from '../../../../test_helpers';
import { rerender, getPageTitle, getPageHeaderActions } from '../../../../test_helpers';
jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() }));
import { CurationLogic } from './curation_logic';
@ -27,9 +24,6 @@ import { AddResultFlyout } from './results';
import { Curation } from './';
describe('Curation', () => {
const props = {
curationsBreadcrumb: ['Engines', 'some-engine', 'Curations'],
};
const values = {
dataLoading: false,
queries: ['query A', 'query B'],
@ -47,39 +41,34 @@ describe('Curation', () => {
});
it('renders', () => {
const wrapper = shallow(<Curation {...props} />);
const wrapper = shallow(<Curation />);
expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Manage curation');
expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([
...props.curationsBreadcrumb,
expect(getPageTitle(wrapper)).toEqual('Manage curation');
expect(wrapper.prop('pageChrome')).toEqual([
'Engines',
'some-engine',
'Curations',
'query A, query B',
]);
});
it('renders a loading component on page load', () => {
setMockValues({ ...values, dataLoading: true });
const wrapper = shallow(<Curation {...props} />);
expect(wrapper.find(Loading)).toHaveLength(1);
});
it('renders the add result flyout when open', () => {
setMockValues({ ...values, isFlyoutOpen: true });
const wrapper = shallow(<Curation {...props} />);
const wrapper = shallow(<Curation />);
expect(wrapper.find(AddResultFlyout)).toHaveLength(1);
});
it('initializes CurationLogic with a curationId prop from URL param', () => {
mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' });
shallow(<Curation {...props} />);
shallow(<Curation />);
expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' });
});
it('calls loadCuration on page load & whenever the curationId URL param changes', () => {
mockUseParams.mockReturnValueOnce({ curationId: 'cur-123456789' });
const wrapper = shallow(<Curation {...props} />);
const wrapper = shallow(<Curation />);
expect(actions.loadCuration).toHaveBeenCalledTimes(1);
mockUseParams.mockReturnValueOnce({ curationId: 'cur-987654321' });
@ -92,9 +81,8 @@ describe('Curation', () => {
let confirmSpy: jest.SpyInstance;
beforeAll(() => {
const wrapper = shallow(<Curation {...props} />);
const headerActions = wrapper.find(EuiPageHeader).prop('rightSideItems');
restoreDefaultsButton = shallow(headerActions![0] as React.ReactElement);
const wrapper = shallow(<Curation />);
restoreDefaultsButton = getPageHeaderActions(wrapper).childAt(0);
confirmSpy = jest.spyOn(window, 'confirm');
});

View file

@ -10,26 +10,19 @@ import { useParams } from 'react-router-dom';
import { useValues, useActions } from 'kea';
import { EuiPageHeader, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { FlashMessages } from '../../../../shared/flash_messages';
import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
import { Loading } from '../../../../shared/loading';
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../../constants';
import { AppSearchPageTemplate } from '../../layout';
import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants';
import { getCurationsBreadcrumbs } from '../utils';
import { CurationLogic } from './curation_logic';
import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents';
import { ActiveQuerySelect, ManageQueriesModal } from './queries';
import { AddResultLogic, AddResultFlyout } from './results';
interface Props {
curationsBreadcrumb: BreadcrumbTrail;
}
export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => {
export const Curation: React.FC = () => {
const { curationId } = useParams() as { curationId: string };
const { loadCuration, resetCuration } = useActions(CurationLogic({ curationId }));
const { dataLoading, queries } = useValues(CurationLogic({ curationId }));
@ -39,14 +32,12 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => {
loadCuration();
}, [curationId]);
if (dataLoading) return <Loading />;
return (
<>
<SetPageChrome trail={[...curationsBreadcrumb, queries.join(', ')]} />
<EuiPageHeader
pageTitle={MANAGE_CURATION_TITLE}
rightSideItems={[
<AppSearchPageTemplate
pageChrome={getCurationsBreadcrumbs([queries.join(', ')])}
pageHeader={{
pageTitle: MANAGE_CURATION_TITLE,
rightSideItems: [
<EuiButton
color="danger"
onClick={() => {
@ -55,10 +46,10 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => {
>
{RESTORE_DEFAULTS_BUTTON_LABEL}
</EuiButton>,
]}
responsive={false}
/>
],
}}
isLoading={dataLoading}
>
<EuiFlexGroup alignItems="flexEnd" gutterSize="xl" responsive={false}>
<EuiFlexItem>
<ActiveQuerySelect />
@ -69,7 +60,6 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => {
</EuiFlexGroup>
<EuiSpacer size="xl" />
<FlashMessages />
<PromotedDocuments />
<EuiSpacer />
@ -78,6 +68,6 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => {
<HiddenDocuments />
{isFlyoutOpen && <AddResultFlyout />}
</>
</AppSearchPageTemplate>
);
};

View file

@ -80,7 +80,7 @@ export const HiddenDocuments: React.FC = () => {
<h3>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyTitle',
{ defaultMessage: 'No documents are being hidden for this query' }
{ defaultMessage: "You haven't hidden any documents yet" }
)}
</h3>
}

View file

@ -19,6 +19,6 @@ describe('CurationsRouter', () => {
const wrapper = shallow(<CurationsRouter />);
expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(4);
expect(wrapper.find(Route)).toHaveLength(3);
});
});

View file

@ -8,38 +8,26 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { NotFound } from '../../../shared/not_found';
import {
ENGINE_CURATIONS_PATH,
ENGINE_CURATIONS_NEW_PATH,
ENGINE_CURATION_PATH,
} from '../../routes';
import { getEngineBreadcrumbs } from '../engine';
import { CURATIONS_TITLE, CREATE_NEW_CURATION_TITLE } from './constants';
import { Curation } from './curation';
import { Curations, CurationCreation } from './views';
export const CurationsRouter: React.FC = () => {
const CURATIONS_BREADCRUMB = getEngineBreadcrumbs([CURATIONS_TITLE]);
return (
<Switch>
<Route exact path={ENGINE_CURATIONS_PATH}>
<SetPageChrome trail={CURATIONS_BREADCRUMB} />
<Curations />
</Route>
<Route exact path={ENGINE_CURATIONS_NEW_PATH}>
<SetPageChrome trail={[...CURATIONS_BREADCRUMB, CREATE_NEW_CURATION_TITLE]} />
<CurationCreation />
</Route>
<Route path={ENGINE_CURATION_PATH}>
<Curation curationsBreadcrumb={CURATIONS_BREADCRUMB} />
</Route>
<Route>
<NotFound breadcrumbs={CURATIONS_BREADCRUMB} product={APP_SEARCH_PLUGIN} />
<Curation />
</Route>
</Switch>
);

View file

@ -5,7 +5,21 @@
* 2.0.
*/
import { convertToDate, addDocument, removeDocument } from './utils';
import '../../__mocks__/engine_logic.mock';
import { getCurationsBreadcrumbs, convertToDate, addDocument, removeDocument } from './utils';
describe('getCurationsBreadcrumbs', () => {
it('generates curation-prefixed breadcrumbs', () => {
expect(getCurationsBreadcrumbs()).toEqual(['Engines', 'some-engine', 'Curations']);
expect(getCurationsBreadcrumbs(['Some page'])).toEqual([
'Engines',
'some-engine',
'Curations',
'Some page',
]);
});
});
describe('convertToDate', () => {
it('converts the English-only server timestamps to a parseable Date', () => {

View file

@ -5,6 +5,14 @@
* 2.0.
*/
import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs';
import { getEngineBreadcrumbs } from '../engine';
import { CURATIONS_TITLE } from './constants';
export const getCurationsBreadcrumbs = (breadcrumbs: BreadcrumbTrail = []) =>
getEngineBreadcrumbs([CURATIONS_TITLE, ...breadcrumbs]);
// The server API feels us an English datestring, but we want to convert
// it to an actual Date() instance so that we can localize date formats.
export const convertToDate = (serverDateString: string): Date => {

View file

@ -6,6 +6,7 @@
*/
import { setMockActions } from '../../../../__mocks__/kea_logic';
import '../../../__mocks__/engine_logic.mock';
import React from 'react';

View file

@ -9,10 +9,10 @@ import React from 'react';
import { useActions } from 'kea';
import { EuiPageHeader, EuiPageContent, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { EuiPanel, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FlashMessages } from '../../../../shared/flash_messages';
import { AppSearchPageTemplate } from '../../layout';
import { MultiInputRows } from '../../multi_input_rows';
import {
@ -21,15 +21,17 @@ import {
QUERY_INPUTS_PLACEHOLDER,
} from '../constants';
import { CurationsLogic } from '../index';
import { getCurationsBreadcrumbs } from '../utils';
export const CurationCreation: React.FC = () => {
const { createCuration } = useActions(CurationsLogic);
return (
<>
<EuiPageHeader pageTitle={CREATE_NEW_CURATION_TITLE} />
<FlashMessages />
<EuiPageContent hasBorder>
<AppSearchPageTemplate
pageChrome={getCurationsBreadcrumbs([CREATE_NEW_CURATION_TITLE])}
pageHeader={{ pageTitle: CREATE_NEW_CURATION_TITLE }}
>
<EuiPanel hasBorder>
<EuiTitle>
<h2>
{i18n.translate(
@ -56,7 +58,7 @@ export const CurationCreation: React.FC = () => {
inputPlaceholder={QUERY_INPUTS_PLACEHOLDER}
onSubmit={(queries) => createCuration(queries)}
/>
</EuiPageContent>
</>
</EuiPanel>
</AppSearchPageTemplate>
);
};

View file

@ -6,17 +6,16 @@
*/
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 { EuiPageHeader, EuiBasicTable } from '@elastic/eui';
import { EuiBasicTable } from '@elastic/eui';
import { Loading } from '../../../../shared/loading';
import { mountWithIntl } from '../../../../test_helpers';
import { EmptyState } from '../components';
import { mountWithIntl, getPageTitle } from '../../../../test_helpers';
import { Curations, CurationsTable } from './curations';
@ -61,32 +60,34 @@ describe('Curations', () => {
it('renders', () => {
const wrapper = shallow(<Curations />);
expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Curated results');
expect(getPageTitle(wrapper)).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 />);
describe('loading state', () => {
it('renders a full-page loading state on initial page load', () => {
setMockValues({ ...values, dataLoading: true, curations: [] });
const wrapper = shallow(<Curations />);
expect(wrapper.find(Loading)).toHaveLength(1);
expect(wrapper.prop('isLoading')).toEqual(true);
});
it('does not re-render a full-page loading state after initial page load (uses component-level loading state instead)', () => {
setMockValues({ ...values, dataLoading: true, curations: [{}] });
const wrapper = shallow(<Curations />);
expect(wrapper.prop('isLoading')).toEqual(false);
});
});
it('calls loadCurations on page load', () => {
setMockValues({ ...values, myRole: {} }); // Required for AppSearchPageTemplate to load
mountWithIntl(<Curations />);
expect(actions.loadCurations).toHaveBeenCalledTimes(1);
});
describe('CurationsTable', () => {
it('renders an empty state', () => {
setMockValues({ ...values, curations: [] });
const table = shallow(<CurationsTable />).find(EuiBasicTable);
const noItemsMessage = table.prop('noItemsMessage') as React.ReactElement;
expect(noItemsMessage.type).toEqual(EmptyState);
});
it('passes loading prop based on dataLoading', () => {
setMockValues({ ...values, dataLoading: true });
const wrapper = shallow(<CurationsTable />);

View file

@ -9,25 +9,24 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
import { EuiBasicTable, EuiBasicTableColumn, EuiPageContent, EuiPageHeader } from '@elastic/eui';
import { EuiBasicTable, EuiBasicTableColumn, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants';
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 { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination';
import { ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH } from '../../../routes';
import { FormattedDateTime } from '../../../utils/formatted_date_time';
import { generateEnginePath } from '../../engine';
import { AppSearchPageTemplate } from '../../layout';
import { EmptyState } from '../components';
import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants';
import { CurationsLogic } from '../curations_logic';
import { Curation } from '../types';
import { convertToDate } from '../utils';
import { getCurationsBreadcrumbs, convertToDate } from '../utils';
export const Curations: React.FC = () => {
const { dataLoading, curations, meta } = useValues(CurationsLogic);
@ -37,23 +36,29 @@ export const Curations: React.FC = () => {
loadCurations();
}, [meta.page.current]);
if (dataLoading && !curations.length) return <Loading />;
return (
<>
<EuiPageHeader
pageTitle={CURATIONS_OVERVIEW_TITLE}
rightSideItems={[
<EuiButtonTo to={generateEnginePath(ENGINE_CURATIONS_NEW_PATH)} fill>
<AppSearchPageTemplate
pageChrome={getCurationsBreadcrumbs()}
pageHeader={{
pageTitle: CURATIONS_OVERVIEW_TITLE,
rightSideItems: [
<EuiButtonTo
to={generateEnginePath(ENGINE_CURATIONS_NEW_PATH)}
iconType="plusInCircle"
fill
>
{CREATE_NEW_CURATION_TITLE}
</EuiButtonTo>,
]}
/>
<EuiPageContent hasBorder>
<FlashMessages />
],
}}
isLoading={dataLoading && !curations.length}
isEmptyState={!curations.length}
emptyState={<EmptyState />}
>
<EuiPanel hasBorder>
<CurationsTable />
</EuiPageContent>
</>
</EuiPanel>
</AppSearchPageTemplate>
);
};
@ -139,7 +144,6 @@ export const CurationsTable: React.FC = () => {
responsive
hasActions
loading={dataLoading}
noItemsMessage={<EmptyState />}
pagination={{
...convertMetaToPagination(meta),
hidePerPageOptions: true,

View file

@ -129,6 +129,11 @@ export const EngineRouter: React.FC = () => {
<RelevanceTuning />
</Route>
)}
{canManageEngineCurations && (
<Route path={ENGINE_CURATIONS_PATH}>
<CurationsRouter />
</Route>
)}
{canManageEngineResultSettings && (
<Route path={ENGINE_RESULT_SETTINGS_PATH}>
<ResultSettings />
@ -146,11 +151,6 @@ export const EngineRouter: React.FC = () => {
)}
{/* TODO: Remove layout once page template migration is over */}
<Layout navigation={<AppSearchNav />}>
{canManageEngineCurations && (
<Route path={ENGINE_CURATIONS_PATH}>
<CurationsRouter />
</Route>
)}
{canManageEngineSynonyms && (
<Route path={ENGINE_SYNONYMS_PATH}>
<Synonyms />