[Discover][EuiDataGrid] Add document navigation to flyout (#94439)

- allows the user to navigate to the previous and next document of the list in the document flyout

Co-authored-by: Ryan Keairns <contactryank@gmail.com>
This commit is contained in:
Matthias Wilhelm 2021-03-23 16:53:35 +01:00 committed by GitHub
parent 76b55207f7
commit 6a85841289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 184 additions and 50 deletions

View file

@ -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;
}
}

View file

@ -324,12 +324,14 @@ export const DiscoverGrid = ({
<DiscoverGridFlyout
indexPattern={indexPattern}
hit={expandedDoc}
hits={rows}
// if default columns are used, dont make them part of the URL - the context state handling will take care to restore them
columns={defaultColumns ? [] : displayedColumns}
onFilter={onFilter}
onRemoveColumn={onRemoveColumn}
onAddColumn={onAddColumn}
onClose={() => setExpandedDoc(undefined)}
setExpandedDoc={setExpandedDoc}
services={services}
/>
)}

View file

@ -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(
<DiscoverGridFlyout
columns={['date']}
indexPattern={indexPatternMock}
hit={esHits[0]}
onAddColumn={jest.fn()}
onClose={onClose}
onFilter={jest.fn()}
onRemoveColumn={jest.fn()}
services={
({
filterManager: createFilterManagerMock(),
addBasePath: (path: string) => 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(<DiscoverGridFlyout {...props} />);
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(
<DiscoverGridFlyout
columns={['date']}
indexPattern={indexPatternWithTimefieldMock}
hit={esHits[0]}
onAddColumn={jest.fn()}
onClose={onClose}
onFilter={jest.fn()}
onRemoveColumn={jest.fn()}
services={
({
filterManager: createFilterManagerMock(),
addBasePath: (path: string) => `/base${path}`,
} as unknown) as DiscoverServices
}
/>
);
const props = getProps();
props.indexPattern = indexPatternWithTimefieldMock;
const component = mountWithIntl(<DiscoverGridFlyout {...props} />);
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(<DiscoverGridFlyout {...props} />);
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(<DiscoverGridFlyout {...props} />);
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(<DiscoverGridFlyout {...props} />);
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(<DiscoverGridFlyout {...props} />);
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(<DiscoverGridFlyout {...props} />);
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(<DiscoverGridFlyout {...props} />);
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(<DiscoverGridFlyout {...props} />);
findTestSubject(component, 'pagination-button-previous').simulate('click');
expect(props.setExpandedDoc).toHaveBeenCalledTimes(1);
expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4');
});
});

View file

@ -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<number>(() => (hits ? hits.length : 0), [hits]);
const activePage = useMemo<number>(() => {
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 (
<EuiPortal>
<EuiFlyout onClose={onClose} size="m" data-test-subj="docTableDetailsFlyout">
@ -67,20 +104,23 @@ export function DiscoverGridFlyout({
</EuiTitle>
<EuiSpacer size="s" />
<EuiFlexGroup responsive={false} gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText size="s">
<strong>
{i18n.translate('discover.grid.tableRow.viewText', {
defaultMessage: 'View:',
})}
</strong>
</EuiText>
</EuiFlexItem>
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiHideFor sizes={['xs', 's', 'm']}>
<EuiFlexItem grow={false}>
<EuiText size="s">
<strong>
{i18n.translate('discover.grid.tableRow.viewText', {
defaultMessage: 'View:',
})}
</strong>
</EuiText>
</EuiFlexItem>
</EuiHideFor>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType="document"
flush="left"
href={services.addBasePath(
`#/doc/${indexPattern.id}/${hit._index}?id=${encodeURIComponent(
hit._id as string
@ -98,6 +138,7 @@ export function DiscoverGridFlyout({
<EuiButtonEmpty
size="xs"
iconType="documents"
flush="left"
href={getContextUrl(
hit._id,
indexPattern.id,
@ -113,6 +154,21 @@ export function DiscoverGridFlyout({
</EuiButtonEmpty>
</EuiFlexItem>
)}
{activePage !== -1 && (
<EuiFlexItem>
<EuiPagination
aria-label={i18n.translate('discover.grid.flyout.documentNavigation', {
defaultMessage: 'Document navigation',
})}
pageCount={pageCount}
activePage={activePage}
onPageClick={setPage}
className="dscTable__flyoutDocumentNavigation"
compressed
data-test-subj="dscDocNavigation"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>