diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss index 4754c1700f28d..5bb6c01da5ad6 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss @@ -40,6 +40,10 @@ white-space: nowrap; } +.dscTable__flyoutDocumentNavigation { + justify-content: flex-end; +} + // We only truncate if the cell is not a control column. .euiDataGridHeader { .euiDataGridHeaderCell__content { @@ -78,3 +82,10 @@ .dscDiscoverGrid__descriptionListDescription { word-break: normal !important; } + +@include euiBreakpoint('xs', 's', 'm') { + // EUI issue to hide 'of' text https://github.com/elastic/eui/issues/4654 + .dscTable__flyoutDocumentNavigation .euiPagination__compressedText { + display: none; + } +} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index 380b4dc5e8e9a..20d7d80b498a8 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -324,12 +324,14 @@ export const DiscoverGrid = ({ setExpandedDoc(undefined)} + setExpandedDoc={setExpandedDoc} services={services} /> )} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx index b63aca85b1ec9..54620dff1f63f 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx @@ -21,51 +21,41 @@ import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_ describe('Discover flyout', function () { setDocViewsRegistry(new DocViewsRegistry()); - it('should be rendered correctly using an index pattern without timefield', async () => { + const getProps = () => { const onClose = jest.fn(); - const component = mountWithIntl( - path, - } as unknown) as DiscoverServices - } - /> - ); + const services = ({ + filterManager: createFilterManagerMock(), + addBasePath: (path: string) => `/base${path}`, + } as unknown) as DiscoverServices; + + return { + columns: ['date'], + indexPattern: indexPatternMock, + hit: esHits[0], + hits: esHits, + onAddColumn: jest.fn(), + onClose, + onFilter: jest.fn(), + onRemoveColumn: jest.fn(), + services, + setExpandedDoc: jest.fn(), + }; + }; + + it('should be rendered correctly using an index pattern without timefield', async () => { + const props = getProps(); + const component = mountWithIntl(); const url = findTestSubject(component, 'docTableRowAction').prop('href'); - expect(url).toMatchInlineSnapshot(`"#/doc/the-index-pattern-id/i?id=1"`); + expect(url).toMatchInlineSnapshot(`"/base#/doc/the-index-pattern-id/i?id=1"`); findTestSubject(component, 'euiFlyoutCloseButton').simulate('click'); - expect(onClose).toHaveBeenCalled(); + expect(props.onClose).toHaveBeenCalled(); }); it('should be rendered correctly using an index pattern with timefield', async () => { - const onClose = jest.fn(); - const component = mountWithIntl( - `/base${path}`, - } as unknown) as DiscoverServices - } - /> - ); + const props = getProps(); + props.indexPattern = indexPatternWithTimefieldMock; + const component = mountWithIntl(); const actions = findTestSubject(component, 'docTableRowAction'); expect(actions.length).toBe(2); @@ -76,6 +66,81 @@ describe('Discover flyout', function () { `"/base/app/discover#/context/index-pattern-with-timefield-id/1?_g=(filters:!())&_a=(columns:!(date),filters:!())"` ); findTestSubject(component, 'euiFlyoutCloseButton').simulate('click'); - expect(onClose).toHaveBeenCalled(); + expect(props.onClose).toHaveBeenCalled(); + }); + + it('displays document navigation when there is more than 1 doc available', async () => { + const props = getProps(); + const component = mountWithIntl(); + const docNav = findTestSubject(component, 'dscDocNavigation'); + expect(docNav.length).toBeTruthy(); + }); + + it('displays no document navigation when there are 0 docs available', async () => { + const props = getProps(); + props.hits = []; + const component = mountWithIntl(); + const docNav = findTestSubject(component, 'dscDocNavigation'); + expect(docNav.length).toBeFalsy(); + }); + + it('displays no document navigation when the expanded doc is not part of the given docs', async () => { + // scenario: you've expanded a doc, and in the next request differed docs where fetched + const props = getProps(); + props.hits = [ + { + _index: 'new', + _id: '1', + _score: 1, + _type: '_doc', + _source: { date: '2020-20-01T12:12:12.123', message: 'test1', bytes: 20 }, + }, + { + _index: 'new', + _id: '2', + _score: 1, + _type: '_doc', + _source: { date: '2020-20-01T12:12:12.124', name: 'test2', extension: 'jpg' }, + }, + ]; + const component = mountWithIntl(); + const docNav = findTestSubject(component, 'dscDocNavigation'); + expect(docNav.length).toBeFalsy(); + }); + + it('allows you to navigate to the next doc, if expanded doc is the first', async () => { + // scenario: you've expanded a doc, and in the next request different docs where fetched + const props = getProps(); + const component = mountWithIntl(); + findTestSubject(component, 'pagination-button-next').simulate('click'); + // we selected 1, so we'd expect 2 + expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('2'); + }); + + it('doesnt allow you to navigate to the previous doc, if expanded doc is the first', async () => { + // scenario: you've expanded a doc, and in the next request differed docs where fetched + const props = getProps(); + const component = mountWithIntl(); + findTestSubject(component, 'pagination-button-previous').simulate('click'); + expect(props.setExpandedDoc).toHaveBeenCalledTimes(0); + }); + + it('doesnt allow you to navigate to the next doc, if expanded doc is the last', async () => { + // scenario: you've expanded a doc, and in the next request differed docs where fetched + const props = getProps(); + props.hit = props.hits[props.hits.length - 1]; + const component = mountWithIntl(); + findTestSubject(component, 'pagination-button-next').simulate('click'); + expect(props.setExpandedDoc).toHaveBeenCalledTimes(0); + }); + + it('allows you to navigate to the previous doc, if expanded doc is the last', async () => { + // scenario: you've expanded a doc, and in the next request differed docs where fetched + const props = getProps(); + props.hit = props.hits[props.hits.length - 1]; + const component = mountWithIntl(); + findTestSubject(component, 'pagination-button-previous').simulate('click'); + expect(props.setExpandedDoc).toHaveBeenCalledTimes(1); + expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4'); }); }); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx index 5994892ca2d40..87b9c6243abd8 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, @@ -19,6 +19,8 @@ import { EuiText, EuiSpacer, EuiPortal, + EuiPagination, + EuiHideFor, } from '@elastic/eui'; import { DocViewer } from '../doc_viewer/doc_viewer'; import { IndexPattern } from '../../../kibana_services'; @@ -29,19 +31,34 @@ import { getContextUrl } from '../../helpers/get_context_url'; interface Props { columns: string[]; hit: ElasticSearchHit; + hits?: ElasticSearchHit[]; indexPattern: IndexPattern; onAddColumn: (column: string) => void; onClose: () => void; onFilter: DocViewFilterFn; onRemoveColumn: (column: string) => void; services: DiscoverServices; + setExpandedDoc: (doc: ElasticSearchHit) => void; } +type ElasticSearchHitWithRouting = ElasticSearchHit & { _routing?: string }; + +function getDocFingerprintId(doc: ElasticSearchHitWithRouting) { + const routing = doc._routing || ''; + return [doc._index, doc._id, routing].join('||'); +} + +function getIndexByDocId(hits: ElasticSearchHit[], id: string) { + return hits.findIndex((h) => { + return getDocFingerprintId(h) === id; + }); +} /** * Flyout displaying an expanded Elasticsearch document */ export function DiscoverGridFlyout({ hit, + hits, indexPattern, columns, onFilter, @@ -49,7 +66,27 @@ export function DiscoverGridFlyout({ onRemoveColumn, onAddColumn, services, + setExpandedDoc, }: Props) { + const pageCount = useMemo(() => (hits ? hits.length : 0), [hits]); + const activePage = useMemo(() => { + const id = getDocFingerprintId(hit); + if (!hits || pageCount <= 1) { + return -1; + } + + return getIndexByDocId(hits, id); + }, [hits, hit, pageCount]); + + const setPage = useCallback( + (pageIdx: number) => { + if (hits && hits[pageIdx]) { + setExpandedDoc(hits[pageIdx]); + } + }, + [hits, setExpandedDoc] + ); + return ( @@ -67,20 +104,23 @@ export function DiscoverGridFlyout({ - - - - - {i18n.translate('discover.grid.tableRow.viewText', { - defaultMessage: 'View:', - })} - - - + + + + + + {i18n.translate('discover.grid.tableRow.viewText', { + defaultMessage: 'View:', + })} + + + + )} + {activePage !== -1 && ( + + + + )}