[Discover][DocViewer] Convert EuiTable to EuiDataGrid. Enable up to 500 fields per page. (#175787)

- Closes https://github.com/elastic/kibana/issues/174745

## Summary

This PR converts Doc Viewer table into EuiDataGrid to use its actions
functionality.

<img width="703" alt="Screenshot 2024-05-17 at 20 18 44"
src="10d8a7b0-8fe1-4908-a11d-5fd374eed4c3">
<img width="577" alt="Screenshot 2024-05-17 at 18 17 49"
src="7e6f05ce-9690-48ab-84c0-f8776e360f83">
<img width="490" alt="Screenshot 2024-05-17 at 18 18 05"
src="b36c64de-419d-425c-9890-8bc346059c1a">
<img width="871" alt="Screenshot 2024-05-22 at 15 22 31"
src="92c894f3-91f8-445c-b6fb-ba8842dd3b23">


## Testing

Some cases to check while testing:
- varios value formats
- legacy table vs data grid
- doc viewer flyout vs Single Document page

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Andrea Del Rio <delrio.andre@gmail.com>
Co-authored-by: Davis McPhee <davismcphee@hotmail.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
Julia Rechkunova 2024-06-19 15:31:44 +02:00 committed by GitHub
parent c0b65d605c
commit 05723d4775
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1885 additions and 1336 deletions

View file

@ -27,7 +27,3 @@ export {
export { FieldIcon, type FieldIconProps, getFieldIconProps } from './src/components/field_icon';
export { FieldDescription, type FieldDescriptionProps } from './src/components/field_description';
export {
FieldDescriptionIconButton,
type FieldDescriptionIconButtonProps,
} from './src/components/field_description_icon_button';

View file

@ -1,34 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { FieldDescriptionIconButton } from './field_description_icon_button';
import { render, screen } from '@testing-library/react';
describe('FieldDescriptionIconButton', () => {
it('should render correctly when no custom description', async () => {
const { container } = render(<FieldDescriptionIconButton field={{ name: 'bytes' }} />);
expect(container).toBeEmptyDOMElement();
});
it('should render correctly with a short custom description', async () => {
const customDescription = 'test this desc';
render(<FieldDescriptionIconButton field={{ name: 'bytes', customDescription }} />);
expect(screen.queryByTestId('fieldDescription-bytes')).toBeNull();
screen.queryByTestId('fieldDescriptionPopoverButton-bytes')?.click();
expect(screen.queryByTestId('fieldDescription-bytes')).toHaveTextContent(customDescription);
});
it('should render correctly with a long custom description', async () => {
const customDescription = 'test this long desc '.repeat(8).trim();
render(<FieldDescriptionIconButton field={{ name: 'bytes', customDescription }} />);
expect(screen.queryByTestId('fieldDescription-bytes')).toBeNull();
screen.queryByTestId('fieldDescriptionPopoverButton-bytes')?.click();
expect(screen.queryByTestId('fieldDescription-bytes')).toHaveTextContent(customDescription);
});
});

View file

@ -1,60 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { EuiButtonIcon, EuiPopover, EuiPopoverProps, useEuiTheme } from '@elastic/eui';
import { FieldDescription, FieldDescriptionProps } from '../field_description';
export type FieldDescriptionIconButtonProps = Pick<EuiPopoverProps, 'css'> & {
field: FieldDescriptionProps['field'];
};
export const FieldDescriptionIconButton: React.FC<FieldDescriptionIconButtonProps> = ({
field,
...otherProps
}) => {
const { euiTheme } = useEuiTheme();
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
if (!field?.customDescription) {
return null;
}
const buttonTitle = i18n.translate('fieldUtils.fieldDescriptionIconButtonTitle', {
defaultMessage: 'View field description',
});
return (
<span>
<EuiPopover
{...otherProps}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
panelProps={{
css: css`
max-width: ${euiTheme.base * 20}px;
`,
}}
button={
<EuiButtonIcon
iconType="iInCircle"
title={buttonTitle}
aria-label={buttonTitle}
size="xs"
data-test-subj={`fieldDescriptionPopoverButton-${field.name}`}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
/>
}
>
<FieldDescription field={field} truncate={false} />
</EuiPopover>
</span>
);
};

View file

@ -46,6 +46,8 @@ export class DocViewerTab extends React.Component<Props, State> {
!isEqual(nextProps.renderProps.hit.raw.highlight, this.props.renderProps.hit.raw.highlight) ||
nextProps.id !== this.props.id ||
!isEqual(nextProps.renderProps.columns, this.props.renderProps.columns) ||
nextProps.renderProps.decreaseAvailableHeightBy !==
this.props.renderProps.decreaseAvailableHeightBy ||
nextState.hasError
);
}

View file

@ -1,268 +1,349 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FieldName renders a custom description icon 1`] = `
Array [
<div
class="euiFlexGroup emotion-euiFlexGroup-s-flexStart-flexStart-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldIcon emotion-euiFlexItem-growZero"
>
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis1"
>
<span
data-euiicon-type="tokenString"
title="String"
>
String
</span>
</span>
</div>,
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
class="euiFlexGroup kbnDocViewer__fieldIconContainer emotion-euiFlexGroup-s-flexStart-center-row"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span>
test
</span>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span>
<div
class="euiPopover emotion-euiPopover-inline-block"
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis1"
>
<button
aria-label="View field description"
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
data-test-subj="fieldDescriptionPopoverButton-bytes"
title="View field description"
type="button"
<span
data-euiicon-type="tokenString"
title="String"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="iInCircle"
/>
</button>
</div>
</span>
String
</span>
</span>
</div>
</div>
</div>,
]
</div>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-center-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
data-test-subj="tableDocViewRow-test-name"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
>
<span>
test
</span>
</span>
</div>
</div>
</div>
</div>
`;
exports[`FieldName renders a geo field 1`] = `
Array [
<div
class="euiFlexGroup emotion-euiFlexGroup-s-flexStart-flexStart-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldIcon emotion-euiFlexItem-growZero"
>
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis5"
>
<span
data-euiicon-type="tokenGeo"
title="Geo point"
>
Geo point
</span>
</span>
</div>,
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
class="euiFlexGroup kbnDocViewer__fieldIconContainer emotion-euiFlexGroup-s-flexStart-center-row"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span>
test.test.test
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis5"
>
<span
data-euiicon-type="tokenGeo"
title="Geo point"
>
Geo point
</span>
</span>
</span>
</div>
</div>
</div>,
]
</div>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-center-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
data-test-subj="tableDocViewRow-test.test.test-name"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
>
<span>
test.test.test
</span>
</span>
</div>
</div>
</div>
</div>
`;
exports[`FieldName renders a number field by providing a field record 1`] = `
Array [
<div
class="euiFlexGroup emotion-euiFlexGroup-s-flexStart-flexStart-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldIcon emotion-euiFlexItem-growZero"
>
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis0"
>
<span
data-euiicon-type="tokenNumber"
title="Number"
>
Number
</span>
</span>
</div>,
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
class="euiFlexGroup kbnDocViewer__fieldIconContainer emotion-euiFlexGroup-s-flexStart-center-row"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span>
test.test.test
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis0"
>
<span
data-euiicon-type="tokenNumber"
title="Number"
>
Number
</span>
</span>
</span>
</div>
</div>
</div>,
]
</div>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-center-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
data-test-subj="tableDocViewRow-test.test.test-name"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
>
<span>
test.test.test
</span>
</span>
</div>
</div>
</div>
</div>
`;
exports[`FieldName renders a string field by providing fieldType and fieldName 1`] = `
Array [
<div
class="euiFlexGroup emotion-euiFlexGroup-s-flexStart-flexStart-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldIcon emotion-euiFlexItem-growZero"
>
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis1"
>
<span
data-euiicon-type="tokenString"
title="String"
>
String
</span>
</span>
</div>,
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
class="euiFlexGroup kbnDocViewer__fieldIconContainer emotion-euiFlexGroup-s-flexStart-center-row"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span>
test
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis1"
>
<span
data-euiicon-type="tokenString"
title="String"
>
String
</span>
</span>
</span>
</div>
</div>
</div>,
]
</div>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-center-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
data-test-subj="tableDocViewRow-test-name"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
>
<span>
test
</span>
</span>
</div>
</div>
</div>
</div>
`;
exports[`FieldName renders unknown field 1`] = `
Array [
<div
class="euiFlexGroup emotion-euiFlexGroup-s-flexStart-flexStart-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldIcon emotion-euiFlexItem-growZero"
>
<span
class="euiToken kbnFieldIcon emotion-euiToken-circle-light-s-gray"
>
<span
data-euiicon-type="questionInCircle"
title="Unknown field"
>
Unknown field
</span>
</span>
</div>,
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
class="euiFlexGroup kbnDocViewer__fieldIconContainer emotion-euiFlexGroup-s-flexStart-center-row"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span>
test.test.test
<span
class="euiToken kbnFieldIcon emotion-euiToken-circle-light-s-gray"
>
<span
data-euiicon-type="questionInCircle"
title="Unknown field"
>
Unknown field
</span>
</span>
</span>
</div>
</div>
</div>,
]
</div>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-center-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
data-test-subj="tableDocViewRow-test.test.test-name"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
>
<span>
test.test.test
</span>
</span>
</div>
</div>
</div>
</div>
`;
exports[`FieldName renders when mapping is provided 1`] = `
Array [
<div
class="euiFlexGroup emotion-euiFlexGroup-s-flexStart-flexStart-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldIcon emotion-euiFlexItem-growZero"
>
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis0"
>
<span
data-euiicon-type="tokenNumber"
title="Number"
>
Number
</span>
</span>
</div>,
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
class="euiFlexGroup kbnDocViewer__fieldIconContainer emotion-euiFlexGroup-s-flexStart-center-row"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span>
bytes
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis0"
>
<span
data-euiicon-type="tokenNumber"
title="Number"
>
Number
</span>
</span>
</span>
</div>
</div>
</div>,
]
</div>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-center-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
data-test-subj="tableDocViewRow-test-name"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
>
<span>
bytes
</span>
</span>
</div>
</div>
</div>
</div>
`;
exports[`FieldName renders with a search highlight 1`] = `
Array [
<div
class="euiFlexGroup emotion-euiFlexGroup-s-flexStart-flexStart-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldIcon emotion-euiFlexItem-growZero"
>
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis0"
>
<span
data-euiicon-type="tokenNumber"
title="Number"
>
Number
</span>
</span>
</div>,
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-flexStart-row"
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
class="euiFlexGroup kbnDocViewer__fieldIconContainer emotion-euiFlexGroup-s-flexStart-center-row"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span>
<mark
class="euiMark emotion-euiMark-hasScreenReaderHelpText"
<span
class="euiToken kbnFieldIcon emotion-euiToken-square-light-s-euiColorVis0"
>
<span
data-euiicon-type="tokenNumber"
title="Number"
>
te
</mark>
st.test.test
Number
</span>
</span>
</span>
</div>
</div>
</div>,
]
</div>
<div
class="euiFlexItem emotion-euiFlexItem-grow-1"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-wrap-none-flexStart-center-row"
>
<div
class="euiFlexItem kbnDocViewer__fieldName eui-textBreakAll emotion-euiFlexItem-growZero"
data-test-subj="tableDocViewRow-test.test.test-name"
>
<span
class="euiToolTipAnchor eui-textBreakAll emotion-euiToolTipAnchor-inlineBlock"
>
<span>
<mark
class="euiMark emotion-euiMark-hasScreenReaderHelpText"
>
te
</mark>
st.test.test
</span>
</span>
</div>
</div>
</div>
</div>
`;

View file

@ -1,12 +1,20 @@
.kbnDocViewer__fieldIcon {
.kbnDocViewer__fieldIconContainer {
padding-top: $euiSizeXS * 1.5;
line-height: $euiSize;
}
.kbnDocViewer__fieldName {
line-height: $euiLineHeight;
padding: $euiSizeXS;
padding-left: 0;
line-height: $euiLineHeight;
.euiDataGridRowCell__popover & {
font-size: $euiFontSizeS;
line-height: $euiLineHeight;
}
}
.kbnDocViewer__multiFieldBadge {
@include euiFont;
margin: $euiSizeXS 0;
}

View file

@ -6,15 +6,22 @@
* Side Public License, v 1.
*/
import React, { Fragment } from 'react';
import React from 'react';
import './field_name.scss';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiHighlight } from '@elastic/eui';
import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
EuiHighlight,
EuiIcon,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { FieldIcon, FieldIconProps } from '@kbn/react-field';
import type { DataViewField } from '@kbn/data-views-plugin/public';
import { getDataViewFieldSubtypeMulti } from '@kbn/es-query';
import { FieldDescriptionIconButton, getFieldTypeName } from '@kbn/field-utils';
import { getFieldTypeName } from '@kbn/field-utils';
interface Props {
fieldName: string;
@ -23,6 +30,7 @@ interface Props {
fieldIconProps?: Omit<FieldIconProps, 'type'>;
scripted?: boolean;
highlight?: string;
isPinned?: boolean;
}
export function FieldName({
@ -32,6 +40,7 @@ export function FieldName({
fieldIconProps,
scripted = false,
highlight = '',
isPinned = false,
}: Props) {
const typeName = getFieldTypeName(fieldType);
const displayName =
@ -41,54 +50,76 @@ export function FieldName({
const isMultiField = !!subTypeMulti?.multi;
return (
<Fragment>
<EuiFlexItem grow={false} className="kbnDocViewer__fieldIcon">
<FieldIcon type={fieldType!} label={typeName} scripted={scripted} {...fieldIconProps} />
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="flexStart">
<EuiFlexItem grow={false}>
<EuiFlexGroup
gutterSize="s"
responsive={false}
alignItems="center"
direction="row"
wrap={false}
className="kbnDocViewer__fieldIconContainer"
>
<EuiFlexItem grow={false}>
<FieldIcon type={fieldType!} label={typeName} scripted={scripted} {...fieldIconProps} />
</EuiFlexItem>
{isPinned && (
<EuiFlexItem grow={false}>
<EuiIcon
type="pinFilled"
size="s"
aria-label={i18n.translate('unifiedDocViewer.pinnedFieldTooltipContent', {
defaultMessage: 'Pinned field',
})}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexGroup gutterSize="none" responsive={false} alignItems="flexStart" direction="row">
<EuiFlexItem className="kbnDocViewer__fieldName eui-textBreakAll" grow={false}>
<EuiToolTip
position="top"
content={tooltip}
delay="long"
anchorClassName="eui-textBreakAll"
<EuiFlexItem>
<EuiFlexGroup gutterSize="none" responsive={false} alignItems="center" direction="row" wrap>
<EuiFlexItem
className="kbnDocViewer__fieldName eui-textBreakAll"
grow={false}
data-test-subj={`tableDocViewRow-${fieldName}-name`}
>
<EuiHighlight search={highlight}>{displayName}</EuiHighlight>
</EuiToolTip>
</EuiFlexItem>
{fieldMapping?.customDescription ? (
<EuiFlexItem grow={false}>
<FieldDescriptionIconButton field={fieldMapping} />
</EuiFlexItem>
) : null}
{isMultiField && (
<EuiToolTip
position="top"
delay="long"
content={i18n.translate(
'unifiedDocViewer.fieldChooser.discoverField.multiFieldTooltipContent',
{
defaultMessage: 'Multi-fields can have multiple values per field',
}
)}
>
<EuiBadge
title=""
className="kbnDocViewer__multiFieldBadge"
color="default"
data-test-subj={`tableDocViewRow-${fieldName}-multifieldBadge`}
<EuiToolTip
position="top"
content={tooltip}
delay="long"
anchorClassName="eui-textBreakAll"
>
<FormattedMessage
id="unifiedDocViewer.fieldChooser.discoverField.multiField"
defaultMessage="multi-field"
/>
</EuiBadge>
</EuiToolTip>
)}
</EuiFlexGroup>
</Fragment>
<EuiHighlight search={highlight}>{displayName}</EuiHighlight>
</EuiToolTip>
</EuiFlexItem>
{isMultiField && (
<EuiToolTip
position="top"
delay="long"
content={i18n.translate(
'unifiedDocViewer.fieldChooser.discoverField.multiFieldTooltipContent',
{
defaultMessage: 'Multi-fields can have multiple values per field',
}
)}
>
<EuiBadge
title=""
className="kbnDocViewer__multiFieldBadge"
color="default"
data-test-subj={`tableDocViewRow-${fieldName}-multifieldBadge`}
>
<FormattedMessage
id="unifiedDocViewer.fieldChooser.discoverField.multiField"
defaultMessage="multi-field"
/>
</EuiBadge>
</EuiToolTip>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -49,6 +49,7 @@ export interface DocViewRenderProps {
onAddColumn?: (columnName: string) => void;
onRemoveColumn?: (columnName: string) => void;
docViewsRegistry?: DocViewsRegistry | ((prevRegistry: DocViewsRegistry) => DocViewsRegistry);
decreaseAvailableHeightBy?: number;
}
export type DocViewerComponent = React.FC<DocViewRenderProps>;
export type DocViewRenderFn = (
@ -83,7 +84,7 @@ export interface FieldRecordLegacy {
action: {
isActive: boolean;
onFilter?: DocViewFilterFn;
onToggleColumn: (field: string) => void;
onToggleColumn: ((field: string) => void) | undefined;
flattenedField: unknown;
};
field: {

View file

@ -73,6 +73,8 @@ export function getInternalStateContainer() {
setDataView: (prevState: InternalState) => (nextDataView: DataView) => ({
...prevState,
dataView: nextDataView,
expandedDoc:
nextDataView?.id !== prevState.dataView?.id ? undefined : prevState.expandedDoc,
}),
setIsDataViewLoading: (prevState: InternalState) => (loading: boolean) => ({
...prevState,
@ -130,6 +132,7 @@ export function getInternalStateContainer() {
resetOnSavedSearchChange: (prevState: InternalState) => () => ({
...prevState,
overriddenVisContextAfterInvalidation: undefined,
expandedDoc: undefined,
}),
},
{},

View file

@ -161,13 +161,13 @@ describe('Discover flyout', function () {
it('displays document navigation when there is more than 1 doc available', async () => {
const { component } = await mountComponent({ dataView: dataViewWithTimefieldMock });
const docNav = findTestSubject(component, 'dscDocNavigation');
const docNav = findTestSubject(component, 'docViewerFlyoutNavigation');
expect(docNav.length).toBeTruthy();
});
it('displays no document navigation when there are 0 docs available', async () => {
const { component } = await mountComponent({ records: [], expandedHit: esHitsMock[0] });
const docNav = findTestSubject(component, 'dscDocNavigation');
const docNav = findTestSubject(component, 'docViewerFlyoutNavigation');
expect(docNav.length).toBeFalsy();
});
@ -190,7 +190,7 @@ describe('Discover flyout', function () {
},
].map((hit) => buildDataTableRecord(hit, dataViewMock));
const { component } = await mountComponent({ records, expandedHit: esHitsMock[0] });
const docNav = findTestSubject(component, 'dscDocNavigation');
const docNav = findTestSubject(component, 'docViewerFlyoutNavigation');
expect(docNav.length).toBeFalsy();
});
@ -230,19 +230,19 @@ describe('Discover flyout', function () {
it('allows navigating with arrow keys through documents', async () => {
const { component, props } = await mountComponent({});
findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' });
findTestSubject(component, 'docViewerFlyout').simulate('keydown', { key: 'ArrowRight' });
expect(props.setExpandedDoc).toHaveBeenCalledWith(expect.objectContaining({ id: 'i::2::' }));
component.setProps({ ...props, hit: props.hits[1] });
findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowLeft' });
findTestSubject(component, 'docViewerFlyout').simulate('keydown', { key: 'ArrowLeft' });
expect(props.setExpandedDoc).toHaveBeenCalledWith(expect.objectContaining({ id: 'i::1::' }));
});
it('should not navigate with keypresses when already at the border of documents', async () => {
const { component, props } = await mountComponent({});
findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowLeft' });
findTestSubject(component, 'docViewerFlyout').simulate('keydown', { key: 'ArrowLeft' });
expect(props.setExpandedDoc).not.toHaveBeenCalled();
component.setProps({ ...props, hit: props.hits[props.hits.length - 1] });
findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' });
findTestSubject(component, 'docViewerFlyout').simulate('keydown', { key: 'ArrowRight' });
expect(props.setExpandedDoc).not.toHaveBeenCalled();
});
@ -267,7 +267,7 @@ describe('Discover flyout', function () {
});
const singleDocumentView = findTestSubject(component, 'docTableRowAction');
expect(singleDocumentView.length).toBeFalsy();
const flyoutTitle = findTestSubject(component, 'docTableRowDetailsTitle');
const flyoutTitle = findTestSubject(component, 'docViewerRowDetailsTitle');
expect(flyoutTitle.text()).toBe('Result');
});
@ -279,7 +279,7 @@ describe('Discover flyout', function () {
const { component } = await mountComponent({});
const titleNode = findTestSubject(component, 'docTableRowDetailsTitle');
const titleNode = findTestSubject(component, 'docViewerRowDetailsTitle');
expect(titleNode.text()).toBe(customTitle);
});
@ -503,7 +503,7 @@ describe('Discover flyout', function () {
processRecord: (record) => services.profilesManager.resolveDocumentProfile({ record }),
});
const { component } = await mountComponent({ records, services });
const title = findTestSubject(component, 'docTableRowDetailsTitle');
const title = findTestSubject(component, 'docViewerRowDetailsTitle');
expect(title.text()).toBe('Document #new::1::');
const content = findTestSubject(component, 'kbnDocViewer');
expect(content.text()).toBe('Mock tab');

View file

@ -6,40 +6,22 @@
* Side Public License, v 1.
*/
import React, { useMemo, useCallback } from 'react';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import React, { useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutResizable,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTitle,
EuiSpacer,
EuiPortal,
EuiPagination,
keys,
EuiButtonEmpty,
useEuiTheme,
useIsWithinMinBreakpoint,
} from '@elastic/eui';
import { Filter, Query, AggregateQuery, isOfAggregateQueryType } from '@kbn/es-query';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import type { DataTableColumnsMeta } from '@kbn/unified-data-table';
import type { DocViewsRegistry } from '@kbn/unified-doc-viewer';
import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { UnifiedDocViewerFlyout } from '@kbn/unified-doc-viewer-plugin/public';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { useFlyoutActions } from './use_flyout_actions';
import { useDiscoverCustomization } from '../../customizations';
import { DiscoverGridFlyoutActions } from './discover_grid_flyout_actions';
import { useProfileAccessor } from '../../context_awareness';
export const FLYOUT_WIDTH_KEY = 'discover:flyoutWidth';
export interface DiscoverGridFlyoutProps {
savedSearchId?: string;
filters?: Filter[];
@ -56,13 +38,6 @@ export interface DiscoverGridFlyoutProps {
setExpandedDoc: (doc?: DataTableRecord) => void;
}
function getIndexByDocId(hits: DataTableRecord[], id: string) {
return hits.findIndex((h) => {
return h.id === id;
});
}
export const FLYOUT_WIDTH_KEY = 'discover:flyoutWidth';
/**
* Flyout displaying an expanded Elasticsearch document
*/
@ -83,100 +58,26 @@ export function DiscoverGridFlyout({
}: DiscoverGridFlyoutProps) {
const services = useDiscoverServices();
const flyoutCustomization = useDiscoverCustomization('flyout');
const { euiTheme } = useEuiTheme();
const isXlScreen = useIsWithinMinBreakpoint('xl');
const DEFAULT_WIDTH = euiTheme.base * 34;
const defaultWidth = flyoutCustomization?.size ?? DEFAULT_WIDTH; // Give enough room to search bar to not wrap
const [flyoutWidth, setFlyoutWidth] = useLocalStorage(FLYOUT_WIDTH_KEY, defaultWidth);
const minWidth = euiTheme.base * 24;
const maxWidth = euiTheme.breakpoint.xl;
const isEsqlQuery = isOfAggregateQueryType(query);
const isESQLQuery = isOfAggregateQueryType(query);
// Get actual hit with updated highlighted searches
const actualHit = useMemo(() => hits?.find(({ id }) => id === hit?.id) || hit, [hit, hits]);
const pageCount = useMemo<number>(() => (hits ? hits.length : 0), [hits]);
const activePage = useMemo<number>(() => {
const id = hit.id;
if (!hits || pageCount <= 1) {
return -1;
}
return getIndexByDocId(hits, id);
}, [hits, hit, pageCount]);
const setPage = useCallback(
(index: number) => {
if (hits && hits[index]) {
setExpandedDoc(hits[index]);
}
},
[hits, setExpandedDoc]
);
const onKeyDown = useCallback(
(ev: React.KeyboardEvent) => {
const nodeName = get(ev, 'target.nodeName', null);
if (typeof nodeName === 'string' && nodeName.toLowerCase() === 'input') {
return;
}
if (ev.key === keys.ARROW_LEFT || ev.key === keys.ARROW_RIGHT) {
ev.preventDefault();
ev.stopPropagation();
setPage(activePage + (ev.key === keys.ARROW_RIGHT ? 1 : -1));
}
},
[activePage, setPage]
);
const { flyoutActions } = useFlyoutActions({
actions: flyoutCustomization?.actions,
dataView,
rowIndex: hit.raw._index,
rowId: hit.raw._id,
rowIndex: actualHit.raw._index,
rowId: actualHit.raw._id,
columns,
filters,
savedSearchId,
});
const addColumn = useCallback(
(columnName: string) => {
onAddColumn(columnName);
services.toastNotifications.addSuccess(
i18n.translate('discover.grid.flyout.toastColumnAdded', {
defaultMessage: `Column ''{columnName}'' was added`,
values: { columnName },
})
);
},
[onAddColumn, services.toastNotifications]
);
const removeColumn = useCallback(
(columnName: string) => {
onRemoveColumn(columnName);
services.toastNotifications.addSuccess(
i18n.translate('discover.grid.flyout.toastColumnRemoved', {
defaultMessage: `Column ''{columnName}'' was removed`,
values: { columnName },
})
);
},
[onRemoveColumn, services.toastNotifications]
);
const defaultFlyoutTitle = isEsqlQuery
? i18n.translate('discover.grid.tableRow.docViewerEsqlDetailHeading', {
defaultMessage: 'Result',
})
: i18n.translate('discover.grid.tableRow.docViewerDetailHeading', {
defaultMessage: 'Document',
});
const getDocViewerAccessor = useProfileAccessor('getDocViewer', {
record: actualHit,
});
const docViewer = useMemo(() => {
const getDocViewer = getDocViewerAccessor(() => ({
title: flyoutCustomization?.title ?? defaultFlyoutTitle,
title: flyoutCustomization?.title,
docViewsRegistry: (registry: DocViewsRegistry) =>
typeof flyoutCustomization?.docViewsRegistry === 'function'
? flyoutCustomization.docViewsRegistry(registry)
@ -184,125 +85,33 @@ export function DiscoverGridFlyout({
}));
return getDocViewer({ record: actualHit });
}, [defaultFlyoutTitle, flyoutCustomization, getDocViewerAccessor, actualHit]);
const renderDefaultContent = useCallback(
() => (
<UnifiedDocViewer
columns={columns}
columnsMeta={columnsMeta}
dataView={dataView}
filter={onFilter}
hit={actualHit}
onAddColumn={addColumn}
onRemoveColumn={removeColumn}
textBasedHits={isEsqlQuery ? hits : undefined}
docViewsRegistry={docViewer.docViewsRegistry}
/>
),
[
actualHit,
addColumn,
columns,
columnsMeta,
dataView,
hits,
isEsqlQuery,
onFilter,
removeColumn,
docViewer.docViewsRegistry,
]
);
const contentActions = useMemo(
() => ({
filter: onFilter,
onAddColumn: addColumn,
onRemoveColumn: removeColumn,
}),
[onFilter, addColumn, removeColumn]
);
const bodyContent = flyoutCustomization?.Content ? (
<flyoutCustomization.Content
actions={contentActions}
doc={actualHit}
renderDefaultContent={renderDefaultContent}
/>
) : (
renderDefaultContent()
);
}, [flyoutCustomization, getDocViewerAccessor, actualHit]);
return (
<EuiPortal>
<EuiFlyoutResizable
className="DiscoverFlyout" // used to override the z-index of the flyout from SecuritySolution
onClose={onClose}
type="push"
size={flyoutWidth}
pushMinBreakpoint="xl"
data-test-subj="docTableDetailsFlyout"
onKeyDown={onKeyDown}
ownFocus={true}
minWidth={minWidth}
maxWidth={maxWidth}
onResize={setFlyoutWidth}
css={{
maxWidth: `${isXlScreen ? `calc(100vw - ${DEFAULT_WIDTH}px)` : '90vw'} !important`,
}}
paddingSize="m"
>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup
direction="row"
alignItems="center"
gutterSize="m"
responsive={false}
wrap={true}
>
<EuiFlexItem grow={false}>
<EuiTitle
size="xs"
data-test-subj="docTableRowDetailsTitle"
css={css`
white-space: nowrap;
`}
>
<h2>{docViewer.title}</h2>
</EuiTitle>
</EuiFlexItem>
{activePage !== -1 && (
<EuiFlexItem data-test-subj={`dscDocNavigationPage-${activePage}`}>
<EuiPagination
aria-label={i18n.translate('discover.grid.flyout.documentNavigation', {
defaultMessage: 'Document navigation',
})}
pageCount={pageCount}
activePage={activePage}
onPageClick={setPage}
compressed
data-test-subj="dscDocNavigation"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
{isEsqlQuery || !flyoutActions.length ? null : (
<>
<EuiSpacer size="s" />
<DiscoverGridFlyoutActions flyoutActions={flyoutActions} />
</>
)}
</EuiFlyoutHeader>
<EuiFlyoutBody>{bodyContent}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
{i18n.translate('discover.grid.flyout.close', {
defaultMessage: 'Close',
})}
</EuiButtonEmpty>
</EuiFlyoutFooter>
</EuiFlyoutResizable>
</EuiPortal>
<UnifiedDocViewerFlyout
flyoutTitle={docViewer.title}
flyoutDefaultWidth={flyoutCustomization?.size}
flyoutActions={
!isESQLQuery && flyoutActions.length > 0 ? (
<DiscoverGridFlyoutActions flyoutActions={flyoutActions} />
) : null
}
flyoutWidthLocalStorageKey={FLYOUT_WIDTH_KEY}
FlyoutCustomBody={flyoutCustomization?.Content}
services={services}
docViewsRegistry={docViewer.docViewsRegistry}
isEsqlQuery={isESQLQuery}
hit={hit}
hits={hits}
dataView={dataView}
columns={columns}
columnsMeta={columnsMeta}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
onClose={onClose}
onFilter={onFilter}
setExpandedDoc={setExpandedDoc}
/>
);
}

View file

@ -113,16 +113,16 @@ describe('Doc table row component', () => {
describe('details row', () => {
it('should be empty by default', () => {
const component = mountComponent(defaultProps);
expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeFalsy();
expect(findTestSubject(component, 'docViewerRowDetailsTitle').exists()).toBeFalsy();
});
it('should expand the detail row when the toggle arrow is clicked', () => {
const component = mountComponent(defaultProps);
const toggleButton = findTestSubject(component, 'docTableExpandToggleColumn');
expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeFalsy();
expect(findTestSubject(component, 'docViewerRowDetailsTitle').exists()).toBeFalsy();
toggleButton.simulate('click');
expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeTruthy();
expect(findTestSubject(component, 'docViewerRowDetailsTitle').exists()).toBeTruthy();
});
it('should hide the single/surrounding views for ES|QL mode', () => {
@ -133,7 +133,7 @@ describe('Doc table row component', () => {
const component = mountComponent(props);
const toggleButton = findTestSubject(component, 'docTableExpandToggleColumn');
toggleButton.simulate('click');
expect(findTestSubject(component, 'docTableRowDetailsTitle').text()).toBe('Expanded result');
expect(findTestSubject(component, 'docViewerRowDetailsTitle').text()).toBe('Expanded result');
expect(findTestSubject(component, 'docTableRowAction').length).toBeFalsy();
});
});

View file

@ -58,7 +58,7 @@ export const TableRowDetails = ({
<EuiIcon type="folderOpen" size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTitle size="xxs" data-test-subj="docTableRowDetailsTitle">
<EuiTitle size="xxs" data-test-subj="docViewerRowDetailsTitle">
<h4>
{isEsqlMode && (
<FormattedMessage

View file

@ -66,7 +66,7 @@ export const createContextAwarenessMocks = () => {
const recordId = params.record.id;
const prevValue = prev(params);
return {
title: `${prevValue.title} #${recordId}`,
title: `${prevValue.title ?? 'Document'} #${recordId}`,
docViewsRegistry: (registry) => {
registry.add({
id: 'doc_view_mock',

View file

@ -11,7 +11,7 @@ import type { DocViewsRegistry } from '@kbn/unified-doc-viewer';
import type { DataTableRecord } from '@kbn/discover-utils';
export interface DocViewerExtension {
title: string;
title: string | undefined;
docViewsRegistry: (prevRegistry: DocViewsRegistry) => DocViewsRegistry;
}

View file

@ -24,7 +24,6 @@ export type {
DiscoverCustomization,
DiscoverCustomizationService,
FlyoutCustomization,
FlyoutContentProps,
SearchBarCustomization,
UnifiedHistogramCustomization,
TopNavCustomization,

View file

@ -62,7 +62,7 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
) => (
<RowViewer
dataView={props.dataView}
toastNotifications={props.core.notifications}
notifications={props.core.notifications}
hit={hit}
hits={displayedRows}
columns={displayedColumns}

View file

@ -59,7 +59,7 @@ describe('RowViewer', () => {
<KibanaContextProvider services={services}>
<RowViewer
dataView={dataView as unknown as DataView}
toastNotifications={
notifications={
{
toasts: {
addSuccess: jest.fn(),
@ -88,7 +88,7 @@ describe('RowViewer', () => {
const closeFlyoutSpy = jest.fn();
renderComponent(closeFlyoutSpy);
await waitFor(() => {
userEvent.click(screen.getByTestId('esqlRowDetailsFlyoutCloseBtn'));
userEvent.click(screen.getByTestId('docViewerFlyoutCloseButton'));
expect(closeFlyoutSpy).toHaveBeenCalled();
});
});
@ -106,14 +106,14 @@ describe('RowViewer', () => {
},
} as unknown as DataTableRecord);
await waitFor(() => {
expect(screen.getByTestId('esqlTableRowNavigation')).toBeInTheDocument();
expect(screen.getByTestId('docViewerFlyoutNavigation')).toBeInTheDocument();
});
});
it('doesnt display row navigation when there is only 1 row available', async () => {
renderComponent();
await waitFor(() => {
expect(screen.queryByTestId('esqlTableRowNavigation')).not.toBeInTheDocument();
expect(screen.queryByTestId('docViewerFlyoutNavigation')).not.toBeInTheDocument();
});
});
});

View file

@ -6,34 +6,15 @@
* Side Public License, v 1.
*/
import React, { useMemo, useCallback } from 'react';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import React, { useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutResizable,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTitle,
EuiPortal,
EuiPagination,
keys,
EuiButtonEmpty,
useEuiTheme,
useIsWithinMinBreakpoint,
} from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DataTableColumnsMeta } from '@kbn/unified-data-table';
import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { UnifiedDocViewerFlyout } from '@kbn/unified-doc-viewer-plugin/public';
import { NotificationsStart } from '@kbn/core-notifications-browser';
export interface RowViewerProps {
toastNotifications?: NotificationsStart;
notifications?: NotificationsStart;
columns: string[];
columnsMeta?: DataTableColumnsMeta;
hit: DataTableRecord;
@ -46,12 +27,6 @@ export interface RowViewerProps {
setExpandedDoc: (doc?: DataTableRecord) => void;
}
function getIndexByDocId(hits: DataTableRecord[], id: string) {
return hits.findIndex((h) => {
return h.id === id;
});
}
export const FLYOUT_WIDTH_KEY = 'esqlTable:flyoutWidth';
/**
* Flyout displaying an expanded ES|QL row
@ -62,172 +37,32 @@ export function RowViewer({
dataView,
columns,
columnsMeta,
toastNotifications,
notifications,
flyoutType = 'push',
onClose,
onRemoveColumn,
onAddColumn,
setExpandedDoc,
}: RowViewerProps) {
const { euiTheme } = useEuiTheme();
const isXlScreen = useIsWithinMinBreakpoint('xl');
const DEFAULT_WIDTH = euiTheme.base * 34;
const defaultWidth = DEFAULT_WIDTH;
const [flyoutWidth, setFlyoutWidth] = useLocalStorage(FLYOUT_WIDTH_KEY, defaultWidth);
const minWidth = euiTheme.base * 24;
const maxWidth = euiTheme.breakpoint.xl;
const actualHit = useMemo(() => hits?.find(({ id }) => id === hit?.id) || hit, [hit, hits]);
const pageCount = useMemo<number>(() => (hits ? hits.length : 0), [hits]);
const activePage = useMemo<number>(() => {
const id = hit.id;
if (!hits || pageCount <= 1) {
return -1;
}
return getIndexByDocId(hits, id);
}, [hits, hit, pageCount]);
const setPage = useCallback(
(index: number) => {
if (hits && hits[index]) {
setExpandedDoc(hits[index]);
}
},
[hits, setExpandedDoc]
);
const onKeyDown = useCallback(
(ev: React.KeyboardEvent) => {
const nodeName = get(ev, 'target.nodeName', null);
if (typeof nodeName === 'string' && nodeName.toLowerCase() === 'input') {
return;
}
if (ev.key === keys.ARROW_LEFT || ev.key === keys.ARROW_RIGHT) {
ev.preventDefault();
ev.stopPropagation();
setPage(activePage + (ev.key === keys.ARROW_RIGHT ? 1 : -1));
}
},
[activePage, setPage]
);
const addColumn = useCallback(
(columnName: string) => {
onAddColumn(columnName);
toastNotifications?.toasts?.addSuccess?.(
i18n.translate('esqlDataGrid.grid.flyout.toastColumnAdded', {
defaultMessage: `Column '{columnName}' was added`,
values: { columnName },
})
);
},
[onAddColumn, toastNotifications]
);
const removeColumn = useCallback(
(columnName: string) => {
onRemoveColumn(columnName);
toastNotifications?.toasts?.addSuccess?.(
i18n.translate('esqlDataGrid.grid.flyout.toastColumnRemoved', {
defaultMessage: `Column '{columnName}' was removed`,
values: { columnName },
})
);
},
[onRemoveColumn, toastNotifications]
);
const renderDefaultContent = useCallback(
() => (
<UnifiedDocViewer
columns={columns}
columnsMeta={columnsMeta}
dataView={dataView}
hit={actualHit}
onAddColumn={addColumn}
onRemoveColumn={removeColumn}
textBasedHits={hits}
/>
),
[actualHit, addColumn, columns, columnsMeta, dataView, hits, removeColumn]
);
const bodyContent = renderDefaultContent();
const services = useMemo(() => ({ toastNotifications: notifications?.toasts }), [notifications]);
return (
<EuiPortal>
<EuiFlyoutResizable
onClose={onClose}
type={flyoutType}
size={flyoutWidth}
pushMinBreakpoint="xl"
data-test-subj="esqlRowDetailsFlyout"
onKeyDown={onKeyDown}
ownFocus={true}
minWidth={minWidth}
maxWidth={maxWidth}
onResize={setFlyoutWidth}
css={{
maxWidth: `${isXlScreen ? `calc(100vw - ${DEFAULT_WIDTH}px)` : '90vw'} !important`,
}}
paddingSize="m"
>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup
direction="row"
alignItems="center"
gutterSize="m"
responsive={false}
wrap={true}
>
<EuiFlexItem grow={false}>
<EuiTitle
size="xs"
data-test-subj="docTableRowDetailsTitle"
css={css`
white-space: nowrap;
`}
>
<h2>
{i18n.translate('esqlDataGrid.grid.tableRow.docViewerEsqlDetailHeading', {
defaultMessage: 'Result',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
{activePage !== -1 && (
<EuiFlexItem data-test-subj={`esqlTableRowNavigation-${activePage}`}>
<EuiPagination
aria-label={i18n.translate('esqlDataGrid.grid.flyout.rowNavigation', {
defaultMessage: 'Row navigation',
})}
pageCount={pageCount}
activePage={activePage}
onPageClick={setPage}
compressed
data-test-subj="esqlTableRowNavigation"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>{bodyContent}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButtonEmpty
iconType="cross"
onClick={onClose}
flush="left"
data-test-subj="esqlRowDetailsFlyoutCloseBtn"
>
{i18n.translate('esqlDataGrid.grid.flyout.close', {
defaultMessage: 'Close',
})}
</EuiButtonEmpty>
</EuiFlyoutFooter>
</EuiFlyoutResizable>
</EuiPortal>
<UnifiedDocViewerFlyout
data-test-subj="esqlRowDetailsFlyout"
flyoutWidthLocalStorageKey={FLYOUT_WIDTH_KEY}
flyoutType={flyoutType}
services={services}
isEsqlQuery={true}
hit={hit}
hits={hits}
dataView={dataView}
columns={columns}
columnsMeta={columnsMeta}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
onClose={onClose}
setExpandedDoc={setExpandedDoc}
/>
);
}

View file

@ -22,10 +22,9 @@
"@kbn/core",
"@kbn/ui-actions-plugin",
"@kbn/field-formats-plugin",
"@kbn/i18n",
"@kbn/unified-doc-viewer-plugin",
"@kbn/core-notifications-browser",
"@kbn/shared-ux-utility"
"@kbn/shared-ux-utility",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,311 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useMemo, useCallback, type ComponentType } from 'react';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutResizable,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTitle,
EuiSpacer,
EuiPortal,
EuiPagination,
keys,
EuiButtonEmpty,
useEuiTheme,
useIsWithinMinBreakpoint,
EuiFlyoutProps,
} from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import type { DataTableColumnsMeta } from '@kbn/unified-data-table';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import type { ToastsStart } from '@kbn/core-notifications-browser';
import type { DocViewFilterFn, DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
import { UnifiedDocViewer } from '../lazy_doc_viewer';
export interface UnifiedDocViewerFlyoutProps {
'data-test-subj'?: string;
flyoutTitle?: string;
flyoutDefaultWidth?: EuiFlyoutProps['size'];
flyoutActions?: React.ReactNode;
flyoutType?: 'push' | 'overlay';
flyoutWidthLocalStorageKey?: string;
FlyoutCustomBody?: ComponentType<{
actions: Pick<DocViewRenderProps, 'filter' | 'onAddColumn' | 'onRemoveColumn'>;
doc: DataTableRecord;
renderDefaultContent: () => React.ReactNode;
}>;
services: {
toastNotifications?: ToastsStart;
};
docViewsRegistry?: DocViewRenderProps['docViewsRegistry'];
isEsqlQuery: boolean;
columns: string[];
columnsMeta?: DataTableColumnsMeta;
hit: DataTableRecord;
hits?: DataTableRecord[];
dataView: DataView;
onAddColumn: (column: string) => void;
onClose: () => void;
onFilter?: DocViewFilterFn;
onRemoveColumn: (column: string) => void;
setExpandedDoc: (doc?: DataTableRecord) => void;
}
function getIndexByDocId(hits: DataTableRecord[], id: string) {
return hits.findIndex((h) => {
return h.id === id;
});
}
export const FLYOUT_WIDTH_KEY = 'unifiedDocViewer:flyoutWidth';
/**
* Flyout displaying an expanded row details
*/
export function UnifiedDocViewerFlyout({
'data-test-subj': dataTestSubj,
flyoutTitle,
flyoutActions,
flyoutDefaultWidth,
flyoutType,
flyoutWidthLocalStorageKey,
FlyoutCustomBody,
services,
docViewsRegistry,
isEsqlQuery,
hit,
hits,
dataView,
columns,
columnsMeta,
onFilter,
onClose,
onRemoveColumn,
onAddColumn,
setExpandedDoc,
}: UnifiedDocViewerFlyoutProps) {
const { euiTheme } = useEuiTheme();
const isXlScreen = useIsWithinMinBreakpoint('xl');
const DEFAULT_WIDTH = euiTheme.base * 34;
const defaultWidth = flyoutDefaultWidth ?? DEFAULT_WIDTH; // Give enough room to search bar to not wrap
const [flyoutWidth, setFlyoutWidth] = useLocalStorage(
flyoutWidthLocalStorageKey ?? FLYOUT_WIDTH_KEY,
defaultWidth
);
const minWidth = euiTheme.base * 24;
const maxWidth = euiTheme.breakpoint.xl;
// Get actual hit with updated highlighted searches
const actualHit = useMemo(() => hits?.find(({ id }) => id === hit?.id) || hit, [hit, hits]);
const pageCount = useMemo<number>(() => (hits ? hits.length : 0), [hits]);
const activePage = useMemo<number>(() => {
const id = hit.id;
if (!hits || pageCount <= 1) {
return -1;
}
return getIndexByDocId(hits, id);
}, [hits, hit, pageCount]);
const setPage = useCallback(
(index: number) => {
if (hits && hits[index]) {
setExpandedDoc(hits[index]);
}
},
[hits, setExpandedDoc]
);
const onKeyDown = useCallback(
(ev: React.KeyboardEvent) => {
const nodeClasses = get(ev, 'target.className', '');
if (typeof nodeClasses === 'string' && nodeClasses.includes('euiDataGrid')) {
// ignore events triggered from the data grid
return;
}
const nodeName = get(ev, 'target.nodeName', null);
if (typeof nodeName === 'string' && nodeName.toLowerCase() === 'input') {
// ignore events triggered from the search input
return;
}
if (ev.key === keys.ARROW_LEFT || ev.key === keys.ARROW_RIGHT) {
ev.preventDefault();
ev.stopPropagation();
setPage(activePage + (ev.key === keys.ARROW_RIGHT ? 1 : -1));
}
},
[activePage, setPage]
);
const addColumn = useCallback(
(columnName: string) => {
onAddColumn(columnName);
services.toastNotifications?.addSuccess(
i18n.translate('unifiedDocViewer.flyout.toastColumnAdded', {
defaultMessage: `Column ''{columnName}'' was added`,
values: { columnName },
})
);
},
[onAddColumn, services.toastNotifications]
);
const removeColumn = useCallback(
(columnName: string) => {
onRemoveColumn(columnName);
services.toastNotifications?.addSuccess(
i18n.translate('unifiedDocViewer.flyout.toastColumnRemoved', {
defaultMessage: `Column ''{columnName}'' was removed`,
values: { columnName },
})
);
},
[onRemoveColumn, services.toastNotifications]
);
const renderDefaultContent = useCallback(
() => (
<UnifiedDocViewer
columns={columns}
columnsMeta={columnsMeta}
dataView={dataView}
filter={onFilter}
hit={actualHit}
onAddColumn={addColumn}
onRemoveColumn={removeColumn}
textBasedHits={isEsqlQuery ? hits : undefined}
docViewsRegistry={docViewsRegistry}
decreaseAvailableHeightBy={80} // flyout footer height
/>
),
[
actualHit,
addColumn,
columns,
columnsMeta,
dataView,
hits,
isEsqlQuery,
onFilter,
removeColumn,
docViewsRegistry,
]
);
const contentActions = useMemo(
() => ({
filter: onFilter,
onAddColumn: addColumn,
onRemoveColumn: removeColumn,
}),
[onFilter, addColumn, removeColumn]
);
const bodyContent = FlyoutCustomBody ? (
<FlyoutCustomBody
actions={contentActions}
doc={actualHit}
renderDefaultContent={renderDefaultContent}
/>
) : (
renderDefaultContent()
);
const defaultFlyoutTitle = isEsqlQuery
? i18n.translate('unifiedDocViewer.flyout.docViewerEsqlDetailHeading', {
defaultMessage: 'Result',
})
: i18n.translate('unifiedDocViewer.flyout.docViewerDetailHeading', {
defaultMessage: 'Document',
});
const currentFlyoutTitle = flyoutTitle ?? defaultFlyoutTitle;
return (
<EuiPortal>
<EuiFlyoutResizable
className="DiscoverFlyout" // used to override the z-index of the flyout from SecuritySolution
onClose={onClose}
type={flyoutType ?? 'push'}
size={flyoutWidth}
pushMinBreakpoint="xl"
data-test-subj={dataTestSubj ?? 'docViewerFlyout'}
onKeyDown={onKeyDown}
ownFocus={true}
minWidth={minWidth}
maxWidth={maxWidth}
onResize={setFlyoutWidth}
css={{
maxWidth: `${isXlScreen ? `calc(100vw - ${DEFAULT_WIDTH}px)` : '90vw'} !important`,
}}
paddingSize="m"
>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup
direction="row"
alignItems="center"
gutterSize="m"
responsive={false}
wrap={true}
>
<EuiFlexItem grow={false}>
<EuiTitle
size="xs"
data-test-subj="docViewerRowDetailsTitle"
css={css`
white-space: nowrap;
`}
>
<h2>{currentFlyoutTitle}</h2>
</EuiTitle>
</EuiFlexItem>
{activePage !== -1 && (
<EuiFlexItem data-test-subj={`docViewerFlyoutNavigationPage-${activePage}`}>
<EuiPagination
aria-label={i18n.translate('unifiedDocViewer.flyout.documentNavigation', {
defaultMessage: 'Document navigation',
})}
pageCount={pageCount}
activePage={activePage}
onPageClick={setPage}
compressed
data-test-subj="docViewerFlyoutNavigation"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
{isEsqlQuery || !flyoutActions ? null : (
<>
<EuiSpacer size="s" />
{flyoutActions}
</>
)}
</EuiFlyoutHeader>
<EuiFlyoutBody>{bodyContent}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButtonEmpty
iconType="cross"
onClick={onClose}
flush="left"
data-test-subj="docViewerFlyoutCloseButton"
>
{i18n.translate('unifiedDocViewer.flyout.closeButtonLabel', {
defaultMessage: 'Close',
})}
</EuiButtonEmpty>
</EuiFlyoutFooter>
</EuiFlyoutResizable>
</EuiPortal>
);
}

View file

@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
export {
FieldDescriptionIconButton,
type FieldDescriptionIconButtonProps,
} from './field_description_icon_button';
import { UnifiedDocViewerFlyout } from './doc_viewer_flyout';
// Required for usage in React.lazy
// eslint-disable-next-line import/no-default-export
export default UnifiedDocViewerFlyout;

View file

@ -7,8 +7,7 @@
*/
import { monaco } from '@kbn/monaco';
import { getHeight } from './get_height';
import { MARGIN_BOTTOM } from './source';
import { getHeight, DEFAULT_MARGIN_BOTTOM } from './get_height';
describe('getHeight', () => {
Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 500 });
@ -32,28 +31,31 @@ describe('getHeight', () => {
test('when using document explorer, returning the available height in the flyout', () => {
const monacoMock = getMonacoMock(500, 0);
const height = getHeight(monacoMock, true);
expect(height).toBe(500 - MARGIN_BOTTOM);
const height = getHeight(monacoMock, true, DEFAULT_MARGIN_BOTTOM);
expect(height).toBe(484);
const heightCustom = getHeight(monacoMock, true, 80);
expect(heightCustom).toBe(420);
});
test('when using document explorer, returning the available height in the flyout has a minimun guarenteed height', () => {
const monacoMock = getMonacoMock(500);
const height = getHeight(monacoMock, true);
const height = getHeight(monacoMock, true, DEFAULT_MARGIN_BOTTOM);
expect(height).toBe(400);
});
test('when using classic table, its displayed inline without scrolling', () => {
const monacoMock = getMonacoMock(100);
const height = getHeight(monacoMock, false);
const height = getHeight(monacoMock, false, DEFAULT_MARGIN_BOTTOM);
expect(height).toBe(1020);
});
test('when using classic table, limited height > 500 lines to allow scrolling', () => {
const monacoMock = getMonacoMock(1000);
const height = getHeight(monacoMock, false);
const height = getHeight(monacoMock, false, DEFAULT_MARGIN_BOTTOM);
expect(height).toBe(5020);
});
});

View file

@ -6,9 +6,29 @@
* Side Public License, v 1.
*/
import { monaco } from '@kbn/monaco';
import { MARGIN_BOTTOM, MAX_LINES_CLASSIC_TABLE, MIN_HEIGHT } from './source';
import { MAX_LINES_CLASSIC_TABLE, MIN_HEIGHT } from './source';
export function getHeight(editor: monaco.editor.IStandaloneCodeEditor, useDocExplorer: boolean) {
// Displayed margin of the tab content to the window bottom
export const DEFAULT_MARGIN_BOTTOM = 16;
export function getTabContentAvailableHeight(
elementRef: HTMLElement | undefined,
decreaseAvailableHeightBy: number
): number {
if (!elementRef) {
return 0;
}
// assign a good height filling the available space of the document flyout
const position = elementRef.getBoundingClientRect();
return window.innerHeight - position.top - decreaseAvailableHeightBy;
}
export function getHeight(
editor: monaco.editor.IStandaloneCodeEditor,
useDocExplorer: boolean,
decreaseAvailableHeightBy: number
) {
const editorElement = editor?.getDomNode();
if (!editorElement) {
return 0;
@ -16,9 +36,7 @@ export function getHeight(editor: monaco.editor.IStandaloneCodeEditor, useDocExp
let result;
if (useDocExplorer) {
// assign a good height filling the available space of the document flyout
const position = editorElement.getBoundingClientRect();
result = window.innerHeight - position.top - MARGIN_BOTTOM;
result = getTabContentAvailableHeight(editorElement, decreaseAvailableHeightBy);
} else {
// takes care of the classic table, display a maximum of 500 lines
// why not display it all? Due to performance issues when the browser needs to render it all

View file

@ -18,7 +18,7 @@ import { ElasticRequestState } from '@kbn/unified-doc-viewer';
import { isLegacyTableEnabled, SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils';
import { getUnifiedDocViewerServices } from '../../plugin';
import { useEsDocSearch } from '../../hooks';
import { getHeight } from './get_height';
import { getHeight, DEFAULT_MARGIN_BOTTOM } from './get_height';
import { JSONCodeEditorCommonMemoized } from '../json_code_editor';
interface SourceViewerProps {
@ -28,6 +28,7 @@ interface SourceViewerProps {
textBasedHits?: DataTableRecord[];
hasLineNumbers: boolean;
width?: number;
decreaseAvailableHeightBy?: number;
requestState?: ElasticRequestState;
onRefresh: () => void;
}
@ -35,8 +36,6 @@ interface SourceViewerProps {
// Ihe number of lines displayed without scrolling used for classic table, which renders the component
// inline limitation was necessary to enable virtualized scrolling, which improves performance
export const MAX_LINES_CLASSIC_TABLE = 500;
// Displayed margin of the code editor to the window bottom when rendered in the document explorer flyout
export const MARGIN_BOTTOM = 80; // DocViewer flyout has a footer
// Minimum height for the source content to guarantee minimum space when the flyout is scrollable.
export const MIN_HEIGHT = 400;
@ -47,6 +46,7 @@ export const DocViewerSource = ({
width,
hasLineNumbers,
textBasedHits,
decreaseAvailableHeightBy,
onRefresh,
}: SourceViewerProps) => {
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor>();
@ -85,7 +85,11 @@ export const DocViewerSource = ({
return;
}
const height = getHeight(editor, useDocExplorer);
const height = getHeight(
editor,
useDocExplorer,
decreaseAvailableHeightBy ?? DEFAULT_MARGIN_BOTTOM
);
if (height === 0) {
return;
}
@ -95,7 +99,7 @@ export const DocViewerSource = ({
} else {
setEditorHeight(height);
}
}, [editor, jsonValue, useDocExplorer, setEditorHeight]);
}, [editor, jsonValue, useDocExplorer, setEditorHeight, decreaseAvailableHeightBy]);
const loadingState = (
<div className="sourceViewer__loading">

View file

@ -0,0 +1,351 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TableActions getFieldCellActions should render correctly for undefined functions 1`] = `
Array [
<ToggleColumn
Component={[Function]}
row={
Object {
"action": Object {
"flattenedField": "flattenedField",
"onFilter": [MockFunction],
"onToggleColumn": [MockFunction],
},
"field": Object {
"displayName": "message",
"field": "message",
"fieldMapping": Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
"customDescription": undefined,
"customLabel": undefined,
"defaultFormatter": undefined,
"esTypes": undefined,
"lang": undefined,
"name": "message",
"readFromDocValues": false,
"script": undefined,
"scripted": false,
"searchable": true,
"subType": undefined,
"type": "keyword",
},
"fieldType": "keyword",
"onTogglePinned": [MockFunction],
"pinned": true,
"scripted": false,
},
"value": Object {
"formattedValue": "test",
"ignored": undefined,
},
}
}
/>,
<PinToggle
Component={[Function]}
row={
Object {
"action": Object {
"flattenedField": "flattenedField",
"onFilter": [MockFunction],
"onToggleColumn": [MockFunction],
},
"field": Object {
"displayName": "message",
"field": "message",
"fieldMapping": Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
"customDescription": undefined,
"customLabel": undefined,
"defaultFormatter": undefined,
"esTypes": undefined,
"lang": undefined,
"name": "message",
"readFromDocValues": false,
"script": undefined,
"scripted": false,
"searchable": true,
"subType": undefined,
"type": "keyword",
},
"fieldType": "keyword",
"onTogglePinned": [MockFunction],
"pinned": true,
"scripted": false,
},
"value": Object {
"formattedValue": "test",
"ignored": undefined,
},
}
}
/>,
]
`;
exports[`TableActions getFieldCellActions should render correctly for undefined functions 2`] = `
Array [
<PinToggle
Component={[Function]}
row={
Object {
"action": Object {
"flattenedField": "flattenedField",
"onFilter": [MockFunction],
"onToggleColumn": [MockFunction],
},
"field": Object {
"displayName": "message",
"field": "message",
"fieldMapping": Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
"customDescription": undefined,
"customLabel": undefined,
"defaultFormatter": undefined,
"esTypes": undefined,
"lang": undefined,
"name": "message",
"readFromDocValues": false,
"script": undefined,
"scripted": false,
"searchable": true,
"subType": undefined,
"type": "keyword",
},
"fieldType": "keyword",
"onTogglePinned": [MockFunction],
"pinned": true,
"scripted": false,
},
"value": Object {
"formattedValue": "test",
"ignored": undefined,
},
}
}
/>,
]
`;
exports[`TableActions getFieldCellActions should render the panels correctly for defined onFilter function 1`] = `
Array [
<FilterExist
Component={[Function]}
row={
Object {
"action": Object {
"flattenedField": "flattenedField",
"onFilter": [MockFunction],
"onToggleColumn": [MockFunction],
},
"field": Object {
"displayName": "message",
"field": "message",
"fieldMapping": Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
"customDescription": undefined,
"customLabel": undefined,
"defaultFormatter": undefined,
"esTypes": undefined,
"lang": undefined,
"name": "message",
"readFromDocValues": false,
"script": undefined,
"scripted": false,
"searchable": true,
"subType": undefined,
"type": "keyword",
},
"fieldType": "keyword",
"onTogglePinned": [MockFunction],
"pinned": true,
"scripted": false,
},
"value": Object {
"formattedValue": "test",
"ignored": undefined,
},
}
}
/>,
<ToggleColumn
Component={[Function]}
row={
Object {
"action": Object {
"flattenedField": "flattenedField",
"onFilter": [MockFunction],
"onToggleColumn": [MockFunction],
},
"field": Object {
"displayName": "message",
"field": "message",
"fieldMapping": Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
"customDescription": undefined,
"customLabel": undefined,
"defaultFormatter": undefined,
"esTypes": undefined,
"lang": undefined,
"name": "message",
"readFromDocValues": false,
"script": undefined,
"scripted": false,
"searchable": true,
"subType": undefined,
"type": "keyword",
},
"fieldType": "keyword",
"onTogglePinned": [MockFunction],
"pinned": true,
"scripted": false,
},
"value": Object {
"formattedValue": "test",
"ignored": undefined,
},
}
}
/>,
<PinToggle
Component={[Function]}
row={
Object {
"action": Object {
"flattenedField": "flattenedField",
"onFilter": [MockFunction],
"onToggleColumn": [MockFunction],
},
"field": Object {
"displayName": "message",
"field": "message",
"fieldMapping": Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
"customDescription": undefined,
"customLabel": undefined,
"defaultFormatter": undefined,
"esTypes": undefined,
"lang": undefined,
"name": "message",
"readFromDocValues": false,
"script": undefined,
"scripted": false,
"searchable": true,
"subType": undefined,
"type": "keyword",
},
"fieldType": "keyword",
"onTogglePinned": [MockFunction],
"pinned": true,
"scripted": false,
},
"value": Object {
"formattedValue": "test",
"ignored": undefined,
},
}
}
/>,
]
`;
exports[`TableActions getFieldValueCellActions should render correctly for undefined functions 1`] = `Array []`;
exports[`TableActions getFieldValueCellActions should render the panels correctly for defined onFilter function 1`] = `
Array [
<FilterIn
Component={[Function]}
row={
Object {
"action": Object {
"flattenedField": "flattenedField",
"onFilter": [MockFunction],
"onToggleColumn": [MockFunction],
},
"field": Object {
"displayName": "message",
"field": "message",
"fieldMapping": Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
"customDescription": undefined,
"customLabel": undefined,
"defaultFormatter": undefined,
"esTypes": undefined,
"lang": undefined,
"name": "message",
"readFromDocValues": false,
"script": undefined,
"scripted": false,
"searchable": true,
"subType": undefined,
"type": "keyword",
},
"fieldType": "keyword",
"onTogglePinned": [MockFunction],
"pinned": true,
"scripted": false,
},
"value": Object {
"formattedValue": "test",
"ignored": undefined,
},
}
}
/>,
<FilterOut
Component={[Function]}
row={
Object {
"action": Object {
"flattenedField": "flattenedField",
"onFilter": [MockFunction],
"onToggleColumn": [MockFunction],
},
"field": Object {
"displayName": "message",
"field": "message",
"fieldMapping": Object {
"aggregatable": true,
"conflictDescriptions": undefined,
"count": 0,
"customDescription": undefined,
"customLabel": undefined,
"defaultFormatter": undefined,
"esTypes": undefined,
"lang": undefined,
"name": "message",
"readFromDocValues": false,
"script": undefined,
"scripted": false,
"searchable": true,
"subType": undefined,
"type": "keyword",
},
"fieldType": "keyword",
"onTogglePinned": [MockFunction],
"pinned": true,
"scripted": false,
},
"value": Object {
"formattedValue": "test",
"ignored": undefined,
},
}
}
/>,
]
`;

View file

@ -37,19 +37,19 @@ export const DocViewerLegacyTable = ({
const tableColumns = useMemo(() => {
return !hideActionsColumn ? [ACTIONS_COLUMN, ...MAIN_COLUMNS] : MAIN_COLUMNS;
}, [hideActionsColumn]);
const onToggleColumn = useCallback(
(field: string) => {
if (!onRemoveColumn || !onAddColumn || !columns) {
return;
}
const onToggleColumn = useMemo(() => {
if (!onRemoveColumn || !onAddColumn || !columns) {
return undefined;
}
return (field: string) => {
if (columns.includes(field)) {
onRemoveColumn(field);
} else {
onAddColumn(field);
}
},
[onRemoveColumn, onAddColumn, columns]
);
};
}, [onRemoveColumn, onAddColumn, columns]);
const onSetRowProps = useCallback(({ field: { field } }: FieldRecordLegacy) => {
return {

View file

@ -20,7 +20,7 @@ interface TableActionsProps {
flattenedField: unknown;
fieldMapping?: DataViewField;
onFilter: DocViewFilterFn;
onToggleColumn: (field: string) => void;
onToggleColumn: ((field: string) => void) | undefined;
ignoredValue: boolean;
}
@ -47,11 +47,13 @@ export const TableActions = ({
onClick={() => onFilter(fieldMapping, flattenedField, '-')}
/>
)}
<DocViewTableRowBtnToggleColumn
active={isActive}
fieldname={field}
onClick={() => onToggleColumn(field)}
/>
{onToggleColumn && (
<DocViewTableRowBtnToggleColumn
active={isActive}
fieldname={field}
onClick={() => onToggleColumn(field)}
/>
)}
{onFilter && (
<DocViewTableRowBtnFilterExists
disabled={!fieldMapping || !fieldMapping.filterable}

View file

@ -61,4 +61,27 @@
line-height: $euiLineHeight;
color: $euiColorFullShade;
vertical-align: top;
.euiDataGridRowCell__popover & {
font-size: $euiFontSizeS;
}
}
.kbnDocViewer__fieldsGrid {
&.euiDataGrid--noControls.euiDataGrid--bordersHorizontal .euiDataGridHeaderCell {
border-top: none;
}
&.euiDataGrid--headerUnderline .euiDataGridHeaderCell {
border-bottom: $euiBorderThin;
}
&.euiDataGrid--rowHoverHighlight .euiDataGridRow:hover {
background-color: tintOrShade($euiColorLightShade, 50%, 0);
}
& .euiDataGridRowCell--firstColumn .euiDataGridRowCell__content {
padding-top: 0;
padding-bottom: 0;
}
}

View file

@ -8,26 +8,23 @@
import './table.scss';
import React, { useCallback, useMemo, useState } from 'react';
import useWindowSize from 'react-use/lib/useWindowSize';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFieldSearch,
EuiSpacer,
EuiTable,
EuiTableBody,
EuiTableRowCell,
EuiTableRow,
EuiTableHeader,
EuiTableHeaderCell,
EuiText,
EuiTablePagination,
EuiSelectableMessage,
EuiDataGrid,
EuiDataGridProps,
EuiDataGridCellPopoverElementProps,
EuiI18n,
useEuiTheme,
EuiText,
EuiCallOut,
useResizeObserver,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { debounce } from 'lodash';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { getFieldIconType } from '@kbn/field-utils/src/utils/get_field_icon_type';
@ -40,37 +37,61 @@ import {
usePager,
} from '@kbn/discover-utils';
import {
FieldDescription,
fieldNameWildcardMatcher,
getFieldSearchMatchingHighlight,
getTextBasedColumnIconType,
} from '@kbn/field-utils';
import type { DocViewRenderProps, FieldRecordLegacy } from '@kbn/unified-doc-viewer/types';
import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
import { FieldName } from '@kbn/unified-doc-viewer';
import { getUnifiedDocViewerServices } from '../../plugin';
import { TableFieldValue } from './table_cell_value';
import { TableActions } from './table_cell_actions';
import {
type TableRow,
getFieldCellActions,
getFieldValueCellActions,
getFilterExistsDisabledWarning,
getFilterInOutPairDisabledWarning,
} from './table_cell_actions';
import {
DEFAULT_MARGIN_BOTTOM,
getTabContentAvailableHeight,
} from '../doc_viewer_source/get_height';
export interface FieldRecord {
action: Omit<FieldRecordLegacy['action'], 'isActive'>;
field: {
pinned: boolean;
onTogglePinned: (field: string) => void;
} & FieldRecordLegacy['field'];
value: FieldRecordLegacy['value'];
}
export type FieldRecord = TableRow;
interface ItemsEntry {
pinnedItems: FieldRecord[];
restItems: FieldRecord[];
}
const MOBILE_OPTIONS = { header: false };
const PAGE_SIZE_OPTIONS = [25, 50, 100];
const MIN_NAME_COLUMN_WIDTH = 150;
const MAX_NAME_COLUMN_WIDTH = 350;
const PAGE_SIZE_OPTIONS = [25, 50, 100, 250, 500];
const DEFAULT_PAGE_SIZE = 25;
const PINNED_FIELDS_KEY = 'discover:pinnedFields';
const PAGE_SIZE = 'discover:pageSize';
const SEARCH_TEXT = 'discover:searchText';
const GRID_COLUMN_FIELD_NAME = 'name';
const GRID_COLUMN_FIELD_VALUE = 'value';
const GRID_PROPS: Pick<EuiDataGridProps, 'columnVisibility' | 'rowHeightsOptions' | 'gridStyle'> = {
columnVisibility: {
visibleColumns: ['name', 'value'],
setVisibleColumns: () => null,
},
rowHeightsOptions: { defaultHeight: 'auto' },
gridStyle: {
border: 'horizontal',
stripes: true,
rowHover: 'highlight',
header: 'underline',
cellPadding: 'm',
fontSize: 's',
},
};
const getPinnedFields = (dataViewId: string, storage: Storage): string[] => {
const pinnedFieldsEntry = storage.get(PINNED_FIELDS_KEY);
if (
@ -114,18 +135,12 @@ export const DocViewerTable = ({
columnsMeta,
hit,
dataView,
hideActionsColumn,
filter,
decreaseAvailableHeightBy,
onAddColumn,
onRemoveColumn,
}: DocViewRenderProps) => {
const { euiTheme } = useEuiTheme();
const [ref, setRef] = useState<HTMLDivElement | HTMLSpanElement | null>(null);
const dimensions = useResizeObserver(ref);
const showActionsInsideTableCell = dimensions?.width
? dimensions.width > euiTheme.breakpoint.m
: false;
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
const { fieldFormats, storage, uiSettings } = getUnifiedDocViewerServices();
const showMultiFields = uiSettings.get(SHOW_MULTIFIELDS);
const currentDataViewId = dataView.id!;
@ -147,19 +162,18 @@ export const DocViewerTable = ({
const mapping = useCallback((name: string) => dataView.fields.getByName(name), [dataView.fields]);
const onToggleColumn = useCallback(
(field: string) => {
if (!onRemoveColumn || !onAddColumn || !columns) {
return;
}
const onToggleColumn = useMemo(() => {
if (!onRemoveColumn || !onAddColumn || !columns) {
return undefined;
}
return (field: string) => {
if (columns.includes(field)) {
onRemoveColumn(field);
} else {
onAddColumn(field);
}
},
[onRemoveColumn, onAddColumn, columns]
);
};
}, [onRemoveColumn, onAddColumn, columns]);
const onTogglePinned = useCallback(
(field: string) => {
@ -173,6 +187,15 @@ export const DocViewerTable = ({
[currentDataViewId, pinnedFields, storage]
);
const onSearch = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newSearchText = event.currentTarget.value;
updateSearchText(newSearchText, storage);
setSearchText(newSearchText);
},
[storage]
);
const fieldToItem = useCallback(
(field: string, isPinned: boolean) => {
const fieldMapping = mapping(field);
@ -193,7 +216,6 @@ export const DocViewerTable = ({
action: {
onToggleColumn,
onFilter: filter,
isActive: !!columns?.includes(field),
flattenedField: flattened[field],
},
field: {
@ -223,7 +245,6 @@ export const DocViewerTable = ({
hit,
onToggleColumn,
filter,
columns,
columnsMeta,
flattened,
onTogglePinned,
@ -231,15 +252,6 @@ export const DocViewerTable = ({
]
);
const handleOnChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newSearchText = event.currentTarget.value;
updateSearchText(newSearchText, storage);
setSearchText(newSearchText);
},
[storage]
);
const { pinnedItems, restItems } = Object.keys(flattened)
.sort((fieldA, fieldB) => {
const mappingA = mapping(fieldA);
@ -278,11 +290,12 @@ export const DocViewerTable = ({
}
);
const { curPageIndex, pageSize, totalPages, startIndex, changePageIndex, changePageSize } =
usePager({
initialPageSize: getPageSize(storage),
totalItems: restItems.length,
});
const rows = useMemo(() => [...pinnedItems, ...restItems], [pinnedItems, restItems]);
const { curPageIndex, pageSize, totalPages, changePageIndex, changePageSize } = usePager({
initialPageSize: getPageSize(storage),
totalItems: rows.length,
});
const showPagination = totalPages !== 0;
const onChangePageSize = useCallback(
@ -293,126 +306,160 @@ export const DocViewerTable = ({
[changePageSize, storage]
);
const headers = [
!hideActionsColumn && (
<EuiTableHeaderCell
key="header-cell-actions"
align="left"
width={showActionsInsideTableCell && filter ? 150 : 62}
isSorted={false}
>
<EuiText size="xs">
<strong>
<FormattedMessage
id="unifiedDocViewer.fieldChooser.discoverField.actions"
defaultMessage="Actions"
/>
</strong>
</EuiText>
</EuiTableHeaderCell>
),
<EuiTableHeaderCell key="header-cell-name" align="left" width="30%" isSorted={false}>
<EuiText size="xs">
<strong>
<FormattedMessage
id="unifiedDocViewer.fieldChooser.discoverField.name"
defaultMessage="Field"
/>
</strong>
</EuiText>
</EuiTableHeaderCell>,
<EuiTableHeaderCell key="header-cell-value" align="left" isSorted={false}>
<EuiText size="xs">
<strong>
<FormattedMessage
id="unifiedDocViewer.fieldChooser.discoverField.value"
defaultMessage="Value"
/>
</strong>
</EuiText>
</EuiTableHeaderCell>,
];
const renderRows = useCallback(
(items: FieldRecord[]) => {
return items.map(
({
action: { flattenedField, onFilter },
field: { field, fieldMapping, fieldType, scripted, pinned },
value: { formattedValue, ignored },
}: FieldRecord) => {
return (
<EuiTableRow key={field} className="kbnDocViewer__tableRow" isSelected={pinned}>
{!hideActionsColumn && (
<EuiTableRowCell
key={field + '-actions'}
align={showActionsInsideTableCell ? 'left' : 'center'}
width={showActionsInsideTableCell ? undefined : 62}
className="kbnDocViewer__tableActionsCell"
textOnly={false}
mobileOptions={MOBILE_OPTIONS}
>
<TableActions
mode={showActionsInsideTableCell ? 'inline' : 'as_popover'}
field={field}
pinned={pinned}
fieldMapping={fieldMapping}
flattenedField={flattenedField}
onFilter={onFilter}
onToggleColumn={onToggleColumn}
ignoredValue={!!ignored}
onTogglePinned={onTogglePinned}
/>
</EuiTableRowCell>
)}
<EuiTableRowCell
key={field + '-field-name'}
align="left"
width="30%"
className="kbnDocViewer__tableFieldNameCell"
textOnly={false}
mobileOptions={MOBILE_OPTIONS}
>
<FieldName
fieldName={field}
fieldType={fieldType}
fieldMapping={fieldMapping}
scripted={scripted}
highlight={getFieldSearchMatchingHighlight(
fieldMapping?.displayName ?? field,
searchText
)}
/>
</EuiTableRowCell>
<EuiTableRowCell
key={field + '-value'}
align="left"
className="kbnDocViewer__tableValueCell"
textOnly={false}
mobileOptions={MOBILE_OPTIONS}
>
<TableFieldValue
field={field}
formattedValue={formattedValue}
rawValue={flattenedField}
ignoreReason={ignored}
/>
</EuiTableRowCell>
</EuiTableRow>
);
const pagination = useMemo(() => {
return showPagination
? {
onChangeItemsPerPage: onChangePageSize,
onChangePage: changePageIndex,
pageIndex: curPageIndex,
pageSize,
pageSizeOptions: PAGE_SIZE_OPTIONS,
}
);
},
[hideActionsColumn, showActionsInsideTableCell, onToggleColumn, onTogglePinned, searchText]
: undefined;
}, [showPagination, curPageIndex, pageSize, onChangePageSize, changePageIndex]);
const fieldCellActions = useMemo(
() => getFieldCellActions({ rows, filter, onToggleColumn }),
[rows, filter, onToggleColumn]
);
const fieldValueCellActions = useMemo(
() => getFieldValueCellActions({ rows, filter }),
[rows, filter]
);
const rowElements = [
...renderRows(pinnedItems),
...renderRows(restItems.slice(startIndex, pageSize + startIndex)),
];
useWindowSize(); // trigger re-render on window resize to recalculate the grid container height
const { width: containerWidth } = useResizeObserver(containerRef);
const gridColumns: EuiDataGridProps['columns'] = useMemo(
() => [
{
id: GRID_COLUMN_FIELD_NAME,
displayAsText: i18n.translate('unifiedDocViewer.fieldChooser.discoverField.name', {
defaultMessage: 'Field',
}),
initialWidth: Math.min(
Math.max(Math.round(containerWidth * 0.3), MIN_NAME_COLUMN_WIDTH),
MAX_NAME_COLUMN_WIDTH
),
actions: false,
visibleCellActions: 3,
cellActions: fieldCellActions,
},
{
id: GRID_COLUMN_FIELD_VALUE,
displayAsText: i18n.translate('unifiedDocViewer.fieldChooser.discoverField.value', {
defaultMessage: 'Value',
}),
actions: false,
visibleCellActions: 2,
cellActions: fieldValueCellActions,
},
],
[fieldCellActions, fieldValueCellActions, containerWidth]
);
const renderCellValue: EuiDataGridProps['renderCellValue'] = useCallback(
({ rowIndex, columnId, isDetails }) => {
const row = rows[rowIndex];
if (!row) {
return null;
}
const {
action: { flattenedField },
field: { field, fieldMapping, fieldType, scripted, pinned },
value: { formattedValue, ignored },
} = row;
if (columnId === 'name') {
return (
<div>
<FieldName
fieldName={field}
fieldType={fieldType}
fieldMapping={fieldMapping}
scripted={scripted}
highlight={getFieldSearchMatchingHighlight(
fieldMapping?.displayName ?? field,
searchText
)}
isPinned={pinned}
/>
{isDetails && fieldMapping?.customDescription ? (
<div>
<FieldDescription field={fieldMapping} truncate={false} />
</div>
) : null}
</div>
);
}
if (columnId === 'value') {
return (
<TableFieldValue
field={field}
formattedValue={formattedValue}
rawValue={flattenedField}
ignoreReason={ignored}
/>
);
}
return null;
},
[rows, searchText]
);
const renderCellPopover = useCallback(
(props: EuiDataGridCellPopoverElementProps) => {
const { columnId, children, cellActions, rowIndex } = props;
const row = rows[rowIndex];
let warningMessage: string | undefined;
if (columnId === GRID_COLUMN_FIELD_VALUE) {
warningMessage = getFilterInOutPairDisabledWarning(row);
} else if (columnId === GRID_COLUMN_FIELD_NAME) {
warningMessage = getFilterExistsDisabledWarning(row);
}
return (
<>
<EuiText size="s">{children}</EuiText>
{cellActions}
{Boolean(warningMessage) && (
<div>
<EuiSpacer size="xs" />
<EuiCallOut title={warningMessage} color="warning" size="s" />
</div>
)}
</>
);
},
[rows]
);
const containerHeight = containerRef
? getTabContentAvailableHeight(containerRef, decreaseAvailableHeightBy ?? DEFAULT_MARGIN_BOTTOM)
: 0;
return (
<EuiFlexGroup direction="column" gutterSize="none" responsive={false} ref={setRef}>
<EuiFlexGroup
ref={setContainerRef}
direction="column"
gutterSize="none"
responsive={false}
css={
containerHeight
? css`
height: ${containerHeight}px;
`
: css`
display: block;
`
}
>
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
</EuiFlexItem>
@ -421,14 +468,14 @@ export const DocViewerTable = ({
<EuiFieldSearch
aria-label={searchPlaceholder}
fullWidth
onChange={handleOnChange}
onChange={onSearch}
placeholder={searchPlaceholder}
value={searchText}
data-test-subj="unifiedDocViewerFieldsSearchInput"
/>
</EuiFlexItem>
{rowElements.length === 0 ? (
{rows.length === 0 ? (
<EuiSelectableMessage style={{ minHeight: 300 }}>
<p>
<EuiI18n
@ -438,29 +485,32 @@ export const DocViewerTable = ({
</p>
</EuiSelectableMessage>
) : (
<EuiFlexItem grow={false}>
<EuiTable responsiveBreakpoint={false}>
<EuiTableHeader>{headers}</EuiTableHeader>
<EuiTableBody>{rowElements}</EuiTableBody>
</EuiTable>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiSpacer size="m" />
</EuiFlexItem>
{showPagination && (
<EuiFlexItem grow={false}>
<EuiTablePagination
activePage={curPageIndex}
itemsPerPage={pageSize}
itemsPerPageOptions={PAGE_SIZE_OPTIONS}
pageCount={totalPages}
onChangeItemsPerPage={onChangePageSize}
onChangePage={changePageIndex}
/>
</EuiFlexItem>
<>
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
</EuiFlexItem>
<EuiFlexItem
grow={Boolean(containerHeight)}
css={css`
min-block-size: 0;
display: block;
`}
>
<EuiDataGrid
{...GRID_PROPS}
aria-label={i18n.translate('unifiedDocViewer.fieldsTable.ariaLabel', {
defaultMessage: 'Field values',
})}
className="kbnDocViewer__fieldsGrid"
columns={gridColumns}
toolbarVisibility={false}
rowCount={rows.length}
renderCellValue={renderCellValue}
renderCellPopover={renderCellPopover}
pagination={pagination}
/>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
);

View file

@ -6,56 +6,82 @@
* Side Public License, v 1.
*/
import React from 'react';
import { render, screen } from '@testing-library/react';
import { TableActions } from './table_cell_actions';
import { getFieldCellActions, getFieldValueCellActions, TableRow } from './table_cell_actions';
import { DataViewField } from '@kbn/data-views-plugin/common';
describe('TableActions', () => {
it('should render the panels correctly for undefined onFilter function', () => {
render(
<TableActions
mode="inline"
field="message"
pinned={false}
fieldMapping={undefined}
flattenedField="message"
onFilter={undefined}
onToggleColumn={jest.fn()}
ignoredValue={false}
onTogglePinned={jest.fn()}
/>
);
expect(screen.queryByTestId('addFilterForValueButton-message')).not.toBeInTheDocument();
expect(screen.queryByTestId('addFilterOutValueButton-message')).not.toBeInTheDocument();
expect(screen.queryByTestId('addExistsFilterButton-message')).not.toBeInTheDocument();
expect(screen.getByTestId('toggleColumnButton-message')).not.toBeDisabled();
expect(screen.getByTestId('togglePinFilterButton-message')).not.toBeDisabled();
const rows: TableRow[] = [
{
action: {
onFilter: jest.fn(),
flattenedField: 'flattenedField',
onToggleColumn: jest.fn(),
},
field: {
pinned: true,
onTogglePinned: jest.fn(),
field: 'message',
fieldMapping: new DataViewField({
type: 'keyword',
name: 'message',
searchable: true,
aggregatable: true,
}),
fieldType: 'keyword',
displayName: 'message',
scripted: false,
},
value: {
ignored: undefined,
formattedValue: 'test',
},
},
];
const Component = () => <div>Component</div>;
const EuiCellParams = {
Component,
rowIndex: 0,
colIndex: 0,
columnId: 'test',
isExpanded: false,
};
describe('getFieldCellActions', () => {
it('should render correctly for undefined functions', () => {
expect(
getFieldCellActions({ rows, filter: undefined, onToggleColumn: jest.fn() }).map((item) =>
item(EuiCellParams)
)
).toMatchSnapshot();
expect(
getFieldCellActions({ rows, filter: undefined, onToggleColumn: undefined }).map((item) =>
item(EuiCellParams)
)
).toMatchSnapshot();
});
it('should render the panels correctly for defined onFilter function', () => {
expect(
getFieldCellActions({ rows, filter: jest.fn(), onToggleColumn: jest.fn() }).map((item) =>
item(EuiCellParams)
)
).toMatchSnapshot();
});
});
it('should render the panels correctly for defined onFilter function', () => {
render(
<TableActions
mode="inline"
field="message"
pinned={false}
fieldMapping={
{
name: 'message',
type: 'string',
filterable: true,
} as DataViewField
}
flattenedField="message"
onFilter={jest.fn()}
onToggleColumn={jest.fn()}
ignoredValue={false}
onTogglePinned={jest.fn()}
/>
);
expect(screen.getByTestId('addFilterForValueButton-message')).not.toBeDisabled();
expect(screen.getByTestId('addFilterOutValueButton-message')).not.toBeDisabled();
expect(screen.getByTestId('addExistsFilterButton-message')).not.toBeDisabled();
expect(screen.getByTestId('toggleColumnButton-message')).not.toBeDisabled();
expect(screen.getByTestId('togglePinFilterButton-message')).not.toBeDisabled();
describe('getFieldValueCellActions', () => {
it('should render correctly for undefined functions', () => {
expect(
getFieldValueCellActions({ rows, filter: undefined }).map((item) => item(EuiCellParams))
).toMatchSnapshot();
});
it('should render the panels correctly for defined onFilter function', () => {
expect(
getFieldValueCellActions({ rows, filter: jest.fn() }).map((item) => item(EuiCellParams))
).toMatchSnapshot();
});
});
});

View file

@ -6,125 +6,210 @@
* Side Public License, v 1.
*/
import React, { useCallback, useState } from 'react';
import {
EuiButtonIcon,
EuiContextMenu,
EuiPopover,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
} from '@elastic/eui';
import React from 'react';
import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { DataViewField } from '@kbn/data-views-plugin/public';
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { DocViewFilterFn, FieldRecordLegacy } from '@kbn/unified-doc-viewer/types';
export interface TableRow {
action: Omit<FieldRecordLegacy['action'], 'isActive'>;
field: {
pinned: boolean;
onTogglePinned: (field: string) => void;
} & FieldRecordLegacy['field'];
value: FieldRecordLegacy['value'];
}
interface TableActionsProps {
mode?: 'inline' | 'as_popover';
field: string;
pinned: boolean;
flattenedField: unknown;
fieldMapping?: DataViewField;
onFilter?: DocViewFilterFn;
onToggleColumn: (field: string) => void;
ignoredValue: boolean;
onTogglePinned: (field: string) => void;
Component: EuiDataGridColumnCellActionProps['Component'];
row: TableRow | undefined; // as we pass `rows[rowIndex]` it's safer to assume that `row` prop can be undefined
}
interface PanelItem {
name: string;
'aria-label': string;
toolTipContent?: string;
disabled?: boolean;
'data-test-subj': string;
icon: string;
onClick: () => void;
export function isFilterInOutPairDisabled(row: TableRow | undefined): boolean {
if (!row) {
return false;
}
const {
action: { onFilter },
field: { fieldMapping },
value: { ignored },
} = row;
return Boolean(onFilter && (!fieldMapping || !fieldMapping.filterable || ignored));
}
export const TableActions = ({
mode = 'as_popover',
pinned,
field,
fieldMapping,
flattenedField,
onToggleColumn,
onFilter,
ignoredValue,
onTogglePinned,
}: TableActionsProps) => {
const [isOpen, setIsOpen] = useState(false);
const openActionsLabel = i18n.translate('unifiedDocViewer.docView.table.actions.open', {
defaultMessage: 'Open actions',
});
const actionsLabel = i18n.translate('unifiedDocViewer.docView.table.actions.label', {
defaultMessage: 'Actions',
});
export function getFilterInOutPairDisabledWarning(row: TableRow | undefined): string | undefined {
if (!row || !isFilterInOutPairDisabled(row)) {
return undefined;
}
const {
field: { fieldMapping },
value: { ignored },
} = row;
if (ignored) {
return i18n.translate(
'unifiedDocViewer.docViews.table.ignoredValuesCanNotBeSearchedWarningMessage',
{
defaultMessage: 'Ignored values cannot be searched',
}
);
}
return !fieldMapping
? i18n.translate(
'unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedWarningMessage',
{
defaultMessage: 'Unindexed fields cannot be searched',
}
)
: undefined;
}
export const FilterIn: React.FC<TableActionsProps> = ({ Component, row }) => {
if (!row) {
return null;
}
const {
action: { onFilter, flattenedField },
field: { field, fieldMapping },
} = row;
// Filters pair
const filtersPairDisabled = !fieldMapping || !fieldMapping.filterable || ignoredValue;
const filterAddLabel = i18n.translate(
'unifiedDocViewer.docViews.table.filterForValueButtonTooltip',
{
defaultMessage: 'Filter for value',
}
);
const filterAddAriaLabel = i18n.translate(
'unifiedDocViewer.docViews.table.filterForValueButtonAriaLabel',
{ defaultMessage: 'Filter for value' }
if (!onFilter) {
return null;
}
return (
<Component
data-test-subj={`addFilterForValueButton-${field}`}
iconType="plusInCircle"
disabled={isFilterInOutPairDisabled(row)}
title={filterAddLabel}
flush="left"
onClick={() => onFilter(fieldMapping, flattenedField, '+')}
>
{filterAddLabel}
</Component>
);
};
export const FilterOut: React.FC<TableActionsProps> = ({ Component, row }) => {
if (!row) {
return null;
}
const {
action: { onFilter, flattenedField },
field: { field, fieldMapping },
} = row;
// Filters pair
const filterOutLabel = i18n.translate(
'unifiedDocViewer.docViews.table.filterOutValueButtonTooltip',
{
defaultMessage: 'Filter out value',
}
);
const filterOutAriaLabel = i18n.translate(
'unifiedDocViewer.docViews.table.filterOutValueButtonAriaLabel',
{ defaultMessage: 'Filter out value' }
if (!onFilter) {
return null;
}
return (
<Component
data-test-subj={`addFilterOutValueButton-${field}`}
iconType="minusInCircle"
disabled={isFilterInOutPairDisabled(row)}
title={filterOutLabel}
flush="left"
onClick={() => onFilter(fieldMapping, flattenedField, '-')}
>
{filterOutLabel}
</Component>
);
const filtersPairToolTip =
(filtersPairDisabled &&
i18n.translate('unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip', {
defaultMessage: 'Unindexed fields or ignored values cannot be searched',
})) ||
undefined;
};
export function isFilterExistsDisabled(row: TableRow | undefined): boolean {
if (!row) {
return false;
}
const {
action: { onFilter },
field: { fieldMapping },
} = row;
return Boolean(onFilter && (!fieldMapping || !fieldMapping.filterable || fieldMapping.scripted));
}
export function getFilterExistsDisabledWarning(row: TableRow | undefined): string | undefined {
if (!row || !isFilterExistsDisabled(row)) {
return undefined;
}
const {
field: { fieldMapping },
} = row;
return fieldMapping?.scripted
? i18n.translate(
'unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsWarningMessage',
{
defaultMessage: 'Unable to filter for presence of scripted fields',
}
)
: undefined;
}
export const FilterExist: React.FC<TableActionsProps> = ({ Component, row }) => {
if (!row) {
return null;
}
const {
action: { onFilter },
field: { field },
} = row;
// Filter exists
const filterExistsLabel = i18n.translate(
'unifiedDocViewer.docViews.table.filterForFieldPresentButtonTooltip',
{ defaultMessage: 'Filter for field present' }
);
const filterExistsAriaLabel = i18n.translate(
'unifiedDocViewer.docViews.table.filterForFieldPresentButtonAriaLabel',
{ defaultMessage: 'Filter for field present' }
);
const filtersExistsDisabled = !fieldMapping || !fieldMapping.filterable;
const filtersExistsToolTip =
(filtersExistsDisabled &&
(fieldMapping && fieldMapping.scripted
? i18n.translate(
'unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip',
{
defaultMessage: 'Unable to filter for presence of scripted fields',
}
)
: i18n.translate(
'unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip',
{
defaultMessage: 'Unable to filter for presence of meta fields',
}
))) ||
undefined;
// Toggle columns
const toggleColumnsLabel = i18n.translate(
'unifiedDocViewer.docViews.table.toggleColumnInTableButtonTooltip',
{ defaultMessage: 'Toggle column in table' }
);
const toggleColumnsAriaLabel = i18n.translate(
'unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel',
{ defaultMessage: 'Toggle column in table' }
if (!onFilter) {
return null;
}
return (
<Component
data-test-subj={`addExistsFilterButton-${field}`}
iconType="filter"
disabled={isFilterExistsDisabled(row)}
title={filterExistsLabel}
flush="left"
onClick={() => onFilter('_exists_', field, '+')}
>
{filterExistsLabel}
</Component>
);
};
export const PinToggle: React.FC<TableActionsProps> = ({ Component, row }) => {
if (!row) {
return null;
}
const {
field: { field, pinned, onTogglePinned },
} = row;
// Pinned
const pinnedLabel = pinned
@ -134,128 +219,101 @@ export const TableActions = ({
: i18n.translate('unifiedDocViewer.docViews.table.pinFieldLabel', {
defaultMessage: 'Pin field',
});
const pinnedAriaLabel = pinned
? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldAriaLabel', {
defaultMessage: 'Unpin field',
})
: i18n.translate('unifiedDocViewer.docViews.table.pinFieldAriaLabel', {
defaultMessage: 'Pin field',
});
const pinnedIconType = pinned ? 'pinFilled' : 'pin';
const toggleOpenPopover = useCallback(() => setIsOpen((current) => !current), []);
const closePopover = useCallback(() => setIsOpen(false), []);
const togglePinned = useCallback(() => onTogglePinned(field), [field, onTogglePinned]);
const onClickAction = useCallback(
(callback: () => void) => () => {
callback();
closePopover();
},
[closePopover]
);
let panelItems: PanelItem[] = [
{
name: toggleColumnsLabel,
'aria-label': toggleColumnsAriaLabel,
'data-test-subj': `toggleColumnButton-${field}`,
icon: 'listAdd',
onClick: onClickAction(onToggleColumn.bind({}, field)),
},
{
name: pinnedLabel,
'aria-label': pinnedAriaLabel,
icon: pinnedIconType,
'data-test-subj': `togglePinFilterButton-${field}`,
onClick: onClickAction(togglePinned),
},
];
if (onFilter) {
panelItems = [
{
name: filterAddLabel,
'aria-label': filterAddAriaLabel,
toolTipContent: filtersPairToolTip,
icon: 'plusInCircle',
disabled: filtersPairDisabled,
'data-test-subj': `addFilterForValueButton-${field}`,
onClick: onClickAction(onFilter.bind({}, fieldMapping, flattenedField, '+')),
},
{
name: filterOutLabel,
'aria-label': filterOutAriaLabel,
toolTipContent: filtersPairToolTip,
icon: 'minusInCircle',
disabled: filtersPairDisabled,
'data-test-subj': `addFilterOutValueButton-${field}`,
onClick: onClickAction(onFilter.bind({}, fieldMapping, flattenedField, '-')),
},
{
name: filterExistsLabel,
'aria-label': filterExistsAriaLabel,
toolTipContent: filtersExistsToolTip,
icon: 'filter',
disabled: filtersExistsDisabled,
'data-test-subj': `addExistsFilterButton-${field}`,
onClick: onClickAction(onFilter.bind({}, '_exists_', field, '+')),
},
...panelItems,
];
}
const panels = [
{
id: 0,
title: actionsLabel,
items: panelItems,
},
];
if (mode === 'inline') {
return (
<EuiFlexGroup
responsive={false}
gutterSize="xs"
className="kbnDocViewer__buttons"
data-test-subj={`fieldActionsGroup-${field}`}
>
{panels[0].items.map((item) => (
<EuiFlexItem key={item.icon} grow={false}>
<EuiToolTip content={item.name}>
<EuiButtonIcon
className="kbnDocViewer__actionButton"
data-test-subj={item['data-test-subj']}
aria-label={item['aria-label']}
iconType={item.icon}
iconSize="s"
disabled={item.disabled}
onClick={item.onClick}
/>
</EuiToolTip>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
}
return (
<EuiPopover
button={
<EuiButtonIcon
data-test-subj={`openFieldActionsButton-${field}`}
aria-label={openActionsLabel}
onClick={toggleOpenPopover}
iconType="boxesHorizontal"
color="text"
/>
}
isOpen={isOpen}
closePopover={closePopover}
display="block"
panelPaddingSize="none"
<Component
data-test-subj={`togglePinFilterButton-${field}`}
iconType={pinnedIconType}
title={pinnedLabel}
flush="left"
onClick={() => onTogglePinned(field)}
>
<EuiContextMenu initialPanelId={0} size="s" panels={panels} />
</EuiPopover>
{pinnedLabel}
</Component>
);
};
export const ToggleColumn: React.FC<TableActionsProps> = ({ Component, row }) => {
if (!row) {
return null;
}
const {
action: { onToggleColumn },
field: { field },
} = row;
if (!onToggleColumn) {
return null;
}
// Toggle column
const toggleColumnLabel = i18n.translate(
'unifiedDocViewer.docViews.table.toggleColumnTableButtonTooltip',
{
defaultMessage: 'Toggle column in table',
}
);
return (
<Component
data-test-subj={`toggleColumnButton-${field}`}
iconType="listAdd"
title={toggleColumnLabel}
flush="left"
onClick={() => onToggleColumn(field)}
>
{toggleColumnLabel}
</Component>
);
};
export function getFieldCellActions({
rows,
filter,
onToggleColumn,
}: {
rows: TableRow[];
filter?: DocViewFilterFn;
onToggleColumn: ((field: string) => void) | undefined;
}) {
return [
...(filter
? [
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
return <FilterExist row={rows[rowIndex]} Component={Component} />;
},
]
: []),
...(onToggleColumn
? [
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
return <ToggleColumn row={rows[rowIndex]} Component={Component} />;
},
]
: []),
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
return <PinToggle row={rows[rowIndex]} Component={Component} />;
},
];
}
export function getFieldValueCellActions({
rows,
filter,
}: {
rows: TableRow[];
filter?: DocViewFilterFn;
}) {
return filter
? [
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
return <FilterIn row={rows[rowIndex]} Component={Component} />;
},
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
return <FilterOut row={rows[rowIndex]} Component={Component} />;
},
]
: [];
}

View file

@ -7,6 +7,7 @@
*/
export * from './doc_viewer';
export * from './doc_viewer_flyout';
export * from './doc_viewer_source';
export * from './doc_viewer_table';
export * from './json_code_editor';

View file

@ -0,0 +1,20 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/src/services/types';
import { EuiDelayRender, EuiSkeletonText } from '@elastic/eui';
const LazyUnifiedDocViewer = React.lazy(() => import('./doc_viewer'));
export const UnifiedDocViewer = withSuspense<DocViewRenderProps>(
LazyUnifiedDocViewer,
<EuiDelayRender delay={300}>
<EuiSkeletonText />
</EuiDelayRender>
);

View file

@ -0,0 +1,17 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
import type { UnifiedDocViewerFlyoutProps } from './doc_viewer_flyout/doc_viewer_flyout';
const LazyUnifiedDocViewerFlyout = React.lazy(() => import('./doc_viewer_flyout'));
export const UnifiedDocViewerFlyout = withSuspense<UnifiedDocViewerFlyoutProps>(
LazyUnifiedDocViewerFlyout,
<></>
);

View file

@ -9,7 +9,6 @@
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
import { EuiDelayRender, EuiSkeletonText } from '@elastic/eui';
import { DocViewRenderProps } from '@kbn/unified-doc-viewer/src/services/types';
import type { JsonCodeEditorProps } from './components';
import { UnifiedDocViewerPublicPlugin } from './plugin';
@ -26,14 +25,8 @@ export const JsonCodeEditor = withSuspense<JsonCodeEditorProps>(
</EuiDelayRender>
);
const LazyUnifiedDocViewer = React.lazy(() => import('./components/doc_viewer'));
export const UnifiedDocViewer = withSuspense<DocViewRenderProps>(
LazyUnifiedDocViewer,
<EuiDelayRender delay={300}>
<EuiSkeletonText />
</EuiDelayRender>
);
export { useEsDocSearch } from './hooks';
export { UnifiedDocViewer } from './components/lazy_doc_viewer';
export { UnifiedDocViewerFlyout } from './components/lazy_doc_viewer_flyout';
export const plugin = () => new UnifiedDocViewerPublicPlugin();

View file

@ -99,7 +99,7 @@ export class UnifiedDocViewerPublicPlugin
defaultMessage: 'JSON',
}),
order: 20,
component: ({ hit, dataView, textBasedHits }) => {
component: ({ hit, dataView, textBasedHits, decreaseAvailableHeightBy }) => {
return (
<LazySourceViewer
index={hit.raw._index}
@ -107,6 +107,7 @@ export class UnifiedDocViewerPublicPlugin
dataView={dataView}
textBasedHits={textBasedHits}
hasLineNumbers
decreaseAvailableHeightBy={decreaseAvailableHeightBy}
onRefresh={() => {}}
/>
);

View file

@ -30,7 +30,9 @@
"@kbn/react-field",
"@kbn/ui-theme",
"@kbn/discover-shared-plugin",
"@kbn/fields-metadata-plugin"
"@kbn/fields-metadata-plugin",
"@kbn/unified-data-table",
"@kbn/core-notifications-browser"
],
"exclude": [
"target/**/*",

View file

@ -134,15 +134,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('a11y test for actions on a field', async () => {
await PageObjects.discover.clickDocViewerTab('doc_view_table');
if (await testSubjects.exists('openFieldActionsButton-Cancelled')) {
await testSubjects.click('openFieldActionsButton-Cancelled'); // Open the actions
} else {
await testSubjects.existOrFail('fieldActionsGroup-Cancelled');
}
await dataGrid.expandFieldNameCellInFlyout('Cancelled');
await a11y.testAppSnapshot();
if (await testSubjects.exists('openFieldActionsButton-Cancelled')) {
await testSubjects.click('openFieldActionsButton-Cancelled'); // Close the actions
}
await browser.pressKeys(browser.keys.ESCAPE);
});
it('a11y test for data-grid table with columns', async () => {

View file

@ -22,7 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'context']);
const PageObjects = getPageObjects(['common', 'context', 'discover']);
const testSubjects = getService('testSubjects');
describe('context filters', function contextSize() {
@ -41,6 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('inclusive filter should be addable via expanded data grid rows', async function () {
await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => {
await dataGrid.clickRowToggle({ isAnchorRow: true, renderMoreRows: true });
await PageObjects.discover.findFieldByNameInDocViewer(TEST_ANCHOR_FILTER_FIELD);
await dataGrid.clickFieldActionInFlyout(
TEST_ANCHOR_FILTER_FIELD,
'addFilterForValueButton'

View file

@ -49,7 +49,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async function () {
await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: 0 });
const detailsEl = await dataGrid.getDetailsRows();
const defaultMessageEl = await detailsEl[0].findByTestSubject('docTableRowDetailsTitle');
const defaultMessageEl = await detailsEl[0].findByTestSubject('docViewerRowDetailsTitle');
expect(defaultMessageEl).to.be.ok();
await dataGrid.closeFlyout();
});

View file

@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
]);
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const find = getService('find');
const browser = getService('browser');
const fieldName = 'clientip';
const deployment = getService('deployment');
@ -82,13 +83,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.waitForWithTimeout(`${fieldName} is visible`, 30000, async () => {
return await testSubjects.isDisplayed(`tableDocViewRow-${fieldName}-value`);
});
const fieldLink = await testSubjects.find(`tableDocViewRow-${fieldName}-value`);
const fieldLink = await find.byCssSelector(
`[data-test-subj="tableDocViewRow-${fieldName}-value"] a`
);
const fieldValue = await fieldLink.getVisibleText();
await fieldLink.click();
await retry.try(async () => {
await checkUrl(fieldValue);
});
});
afterEach(async function () {
const windowHandlers = await browser.getAllWindowHandles();
if (windowHandlers.length > 1) {

View file

@ -161,7 +161,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
const detailsEl = await docTable.getDetailsRows();
const defaultMessageEl = await detailsEl[0].findByTestSubject(
'docTableRowDetailsTitle'
'docViewerRowDetailsTitle'
);
expect(defaultMessageEl).to.be.ok();
});
@ -187,14 +187,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
const detailsEl = await docTable.getDetailsRows();
const defaultMessageEl = await detailsEl[0].findByTestSubject(
'docTableRowDetailsTitle'
'docViewerRowDetailsTitle'
);
expect(defaultMessageEl).to.be.ok();
await queryBar.submitQuery();
const nrOfFetchesResubmit = await PageObjects.discover.getNrOfFetches();
expect(nrOfFetchesResubmit).to.be.above(nrOfFetches);
const defaultMessageElResubmit = await detailsEl[0].findByTestSubject(
'docTableRowDetailsTitle'
'docViewerRowDetailsTitle'
);
expect(defaultMessageElResubmit).to.be.ok();

View file

@ -65,7 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dataGrid.clickRowToggle({ rowIndex: 0 });
await testSubjects.existOrFail('docTableDetailsFlyout');
await testSubjects.existOrFail('docViewerFlyout');
await PageObjects.discover.saveSearch(savedSearchESQL);
@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dataGrid.clickRowToggle({ rowIndex: 0 });
await testSubjects.existOrFail('docTableDetailsFlyout');
await testSubjects.existOrFail('docViewerFlyout');
await dashboardPanelActions.removePanelByTitle(savedSearchESQL);

View file

@ -163,7 +163,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async function () {
await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
const detailsEl = await dataGrid.getDetailsRows();
const defaultMessageEl = await detailsEl[0].findByTestSubject('docTableRowDetailsTitle');
const defaultMessageEl = await detailsEl[0].findByTestSubject('docViewerRowDetailsTitle');
expect(defaultMessageEl).to.be.ok();
await dataGrid.closeFlyout();
});
@ -185,9 +185,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should allow paginating docs in the flyout by clicking in the doc table', async function () {
await retry.try(async function () {
await dataGrid.clickRowToggle({ rowIndex: rowToInspect - 1 });
await testSubjects.exists(`dscDocNavigationPage0`);
await testSubjects.exists(`docViewerFlyoutNavigationPage0`);
await dataGrid.clickRowToggle({ rowIndex: rowToInspect });
await testSubjects.exists(`dscDocNavigationPage1`);
await testSubjects.exists(`docViewerFlyoutNavigationPage1`);
await dataGrid.closeFlyout();
});
});

View file

@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
let fieldTokens: string[] | undefined = [];
await retry.try(async () => {
await dataGrid.clickRowToggle({ rowIndex: 0 });
fieldTokens = await findFirstFieldIcons('docTableDetailsFlyout');
fieldTokens = await findFirstFieldIcons('docViewerFlyout');
});
return fieldTokens;
}

View file

@ -41,13 +41,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
describe('search', function () {
const itemsPerPage = 25;
beforeEach(async () => {
await dataGrid.clickRowToggle();
await PageObjects.discover.isShowingDocViewer();
await retry.waitFor('rendered items', async () => {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === itemsPerPage;
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0;
});
});
@ -95,7 +93,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// expect no changes in the list
await retry.waitFor('all items', async () => {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === itemsPerPage;
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0;
});
});
});

View file

@ -105,7 +105,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// check it in the doc viewer too
await dataGrid.clickRowToggle({ rowIndex: 0 });
await testSubjects.click('fieldDescriptionPopoverButton-agent');
await dataGrid.expandFieldNameCellInFlyout('agent');
await retry.waitFor('doc viewer popover text', async () => {
return (await testSubjects.getVisibleText('fieldDescription-agent')) === customDescription2;
});

View file

@ -268,7 +268,7 @@ export class DataGridService extends FtrService {
}
public async getDetailsRows(): Promise<WebElementWrapper[]> {
return await this.testSubjects.findAll('docTableDetailsFlyout');
return await this.testSubjects.findAll('docViewerFlyout');
}
public async closeFlyout() {
@ -452,17 +452,30 @@ export class DataGridService extends FtrService {
return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`);
}
public async showFieldCellActionInFlyout(fieldName: string, actionName: string): Promise<void> {
const cellSelector = ['addFilterForValueButton', 'addFilterOutValueButton'].includes(actionName)
? `tableDocViewRow-${fieldName}-value`
: `tableDocViewRow-${fieldName}-name`;
await this.testSubjects.click(cellSelector);
await this.retry.waitFor('grid cell actions to appear', async () => {
return this.testSubjects.exists(`${actionName}-${fieldName}`);
});
}
public async clickFieldActionInFlyout(fieldName: string, actionName: string): Promise<void> {
const openPopoverButtonSelector = `openFieldActionsButton-${fieldName}`;
const inlineButtonsGroupSelector = `fieldActionsGroup-${fieldName}`;
if (await this.testSubjects.exists(openPopoverButtonSelector)) {
await this.testSubjects.click(openPopoverButtonSelector);
} else {
await this.testSubjects.existOrFail(inlineButtonsGroupSelector);
}
await this.showFieldCellActionInFlyout(fieldName, actionName);
await this.testSubjects.click(`${actionName}-${fieldName}`);
}
public async expandFieldNameCellInFlyout(fieldName: string): Promise<void> {
const buttonSelector = 'euiDataGridCellExpandButton';
await this.testSubjects.click(`tableDocViewRow-${fieldName}-name`);
await this.retry.waitFor('grid cell actions to appear', async () => {
return this.testSubjects.exists(buttonSelector);
});
await this.testSubjects.click(buttonSelector);
}
public async hasNoResults() {
return await this.find.existsByCssSelector('.euiDataGrid__noResults');
}

View file

@ -2396,12 +2396,12 @@
"discover.fieldChooser.discoverField.removeFieldTooltip": "Supprimer le champ du tableau",
"discover.globalSearch.esqlSearchTitle": "Créer des recherches ES|QL",
"discover.goToDiscoverButtonText": "Aller à Discover",
"discover.grid.flyout.documentNavigation": "Navigation dans le document",
"discover.grid.flyout.toastColumnAdded": "La colonne \"{columnName}\" a été ajoutée.",
"discover.grid.flyout.toastColumnRemoved": "La colonne \"{columnName}\" a été supprimée.",
"unifiedDocViewer.flyout.documentNavigation": "Navigation dans le document",
"unifiedDocViewer.flyout.toastColumnAdded": "La colonne \"{columnName}\" a été ajoutée.",
"unifiedDocViewer.flyout.toastColumnRemoved": "La colonne \"{columnName}\" a été supprimée.",
"discover.grid.tableRow.actionsLabel": "Actions",
"discover.grid.tableRow.docViewerDetailHeading": "Document",
"discover.grid.tableRow.docViewerEsqlDetailHeading": "Ligne",
"unifiedDocViewer.flyout.docViewerDetailHeading": "Document",
"unifiedDocViewer.flyout.docViewerEsqlDetailHeading": "Ligne",
"discover.grid.tableRow.mobileFlyoutActionsButton": "Actions",
"discover.grid.tableRow.moreFlyoutActionsButton": "Plus d'actions",
"discover.grid.tableRow.esqlDetailHeading": "Ligne développée",
@ -44235,8 +44235,6 @@
"uiActions.errors.incompatibleAction": "Action non compatible",
"uiActions.triggers.rowClickkDescription": "Un clic sur une ligne de tableau",
"uiActions.triggers.rowClickTitle": "Clic sur ligne de tableau",
"unifiedDocViewer.docView.table.actions.label": "Actions",
"unifiedDocViewer.docView.table.actions.open": "Actions ouvertes",
"unifiedDocViewer.docView.table.ignored.multiAboveTooltip": "Une ou plusieurs valeurs dans ce champ sont trop longues et ne peuvent pas être recherchées ni filtrées.",
"unifiedDocViewer.docView.table.ignored.multiMalformedTooltip": "Ce champ comporte une ou plusieurs valeurs mal formées qui ne peuvent pas être recherchées ni filtrées.",
"unifiedDocViewer.docView.table.ignored.multiUnknownTooltip": "Une ou plusieurs valeurs dans ce champ ont été ignorées par Elasticsearch et ne peuvent pas être recherchées ni filtrées.",
@ -44253,7 +44251,6 @@
"unifiedDocViewer.docViews.table.filterOutValueButtonTooltip": "Exclure la valeur",
"unifiedDocViewer.docViews.table.ignored.multiValueLabel": "Contient des valeurs ignorées",
"unifiedDocViewer.docViews.table.ignored.singleValueLabel": "Valeur ignorée",
"unifiedDocViewer.docViews.table.pinFieldAriaLabel": "Épingler le champ",
"unifiedDocViewer.docViews.table.pinFieldLabel": "Épingler le champ",
"unifiedDocViewer.docViews.table.tableTitle": "Tableau",
"unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel": "Afficher/Masquer la colonne dans le tableau",
@ -44261,7 +44258,6 @@
"unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "Impossible de filtrer sur les champs méta",
"unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "Impossible de filtrer sur les champs scriptés",
"unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "Les champs non indexés ou les valeurs ignorées ne peuvent pas être recherchés",
"unifiedDocViewer.docViews.table.unpinFieldAriaLabel": "Désépingler le champ",
"unifiedDocViewer.docViews.table.unpinFieldLabel": "Désépingler le champ",
"unifiedDocViewer.fieldChooser.discoverField.actions": "Actions",
"unifiedDocViewer.fieldChooser.discoverField.multiField": "champ multiple",

View file

@ -2393,12 +2393,12 @@
"discover.fieldChooser.discoverField.removeFieldTooltip": "フィールドを表から削除",
"discover.globalSearch.esqlSearchTitle": "ES|QLクエリを作成",
"discover.goToDiscoverButtonText": "Discoverに移動",
"discover.grid.flyout.documentNavigation": "ドキュメントナビゲーション",
"discover.grid.flyout.toastColumnAdded": "列'{columnName}'が追加されました",
"discover.grid.flyout.toastColumnRemoved": "列'{columnName}'が削除されました",
"unifiedDocViewer.flyout.documentNavigation": "ドキュメントナビゲーション",
"unifiedDocViewer.flyout.toastColumnAdded": "列'{columnName}'が追加されました",
"unifiedDocViewer.flyout.toastColumnRemoved": "列'{columnName}'が削除されました",
"discover.grid.tableRow.actionsLabel": "アクション",
"discover.grid.tableRow.docViewerDetailHeading": "ドキュメント",
"discover.grid.tableRow.docViewerEsqlDetailHeading": "行",
"unifiedDocViewer.flyout.docViewerDetailHeading": "ドキュメント",
"unifiedDocViewer.flyout.docViewerEsqlDetailHeading": "行",
"discover.grid.tableRow.mobileFlyoutActionsButton": "アクション",
"discover.grid.tableRow.moreFlyoutActionsButton": "さらにアクションを表示",
"discover.grid.tableRow.esqlDetailHeading": "展開された行",
@ -44211,8 +44211,6 @@
"uiActions.errors.incompatibleAction": "操作に互換性がありません",
"uiActions.triggers.rowClickkDescription": "テーブル行をクリック",
"uiActions.triggers.rowClickTitle": "テーブル行クリック",
"unifiedDocViewer.docView.table.actions.label": "アクション",
"unifiedDocViewer.docView.table.actions.open": "アクションを開く",
"unifiedDocViewer.docView.table.ignored.multiAboveTooltip": "このフィールドの1つ以上の値が長すぎるため、検索またはフィルタリングできません。",
"unifiedDocViewer.docView.table.ignored.multiMalformedTooltip": "このフィールドは、検索またはフィルタリングできない正しくない形式の値が1つ以上あります。",
"unifiedDocViewer.docView.table.ignored.multiUnknownTooltip": "このフィールドの1つ以上の値がElasticsearchによって無視されたため、検索またはフィルタリングできません。",
@ -44229,7 +44227,6 @@
"unifiedDocViewer.docViews.table.filterOutValueButtonTooltip": "値を除外",
"unifiedDocViewer.docViews.table.ignored.multiValueLabel": "無視された値を含む",
"unifiedDocViewer.docViews.table.ignored.singleValueLabel": "無視された値",
"unifiedDocViewer.docViews.table.pinFieldAriaLabel": "フィールドを固定",
"unifiedDocViewer.docViews.table.pinFieldLabel": "フィールドを固定",
"unifiedDocViewer.docViews.table.tableTitle": "表",
"unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel": "表の列を切り替える",
@ -44237,7 +44234,6 @@
"unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "メタフィールドの有無でフィルタリングできません",
"unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "スクリプトフィールドの有無でフィルタリングできません",
"unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "インデックスがないフィールドまたは無視された値は検索できません",
"unifiedDocViewer.docViews.table.unpinFieldAriaLabel": "フィールドを固定解除",
"unifiedDocViewer.docViews.table.unpinFieldLabel": "フィールドを固定解除",
"unifiedDocViewer.fieldChooser.discoverField.actions": "アクション",
"unifiedDocViewer.fieldChooser.discoverField.multiField": "複数フィールド",

View file

@ -2397,12 +2397,12 @@
"discover.fieldChooser.discoverField.removeFieldTooltip": "从表中移除字段",
"discover.globalSearch.esqlSearchTitle": "创建 ES|QL 查询",
"discover.goToDiscoverButtonText": "前往 Discover",
"discover.grid.flyout.documentNavigation": "文档导航",
"discover.grid.flyout.toastColumnAdded": "已添加列“{columnName}”",
"discover.grid.flyout.toastColumnRemoved": "已移除列“{columnName}”",
"unifiedDocViewer.flyout.documentNavigation": "文档导航",
"unifiedDocViewer.flyout.toastColumnAdded": "已添加列“{columnName}”",
"unifiedDocViewer.flyout.toastColumnRemoved": "已移除列“{columnName}”",
"discover.grid.tableRow.actionsLabel": "操作",
"discover.grid.tableRow.docViewerDetailHeading": "文档",
"discover.grid.tableRow.docViewerEsqlDetailHeading": "行",
"unifiedDocViewer.flyout.docViewerDetailHeading": "文档",
"unifiedDocViewer.flyout.docViewerEsqlDetailHeading": "行",
"discover.grid.tableRow.mobileFlyoutActionsButton": "操作",
"discover.grid.tableRow.moreFlyoutActionsButton": "更多操作",
"discover.grid.tableRow.esqlDetailHeading": "已展开行",
@ -44259,8 +44259,6 @@
"uiActions.errors.incompatibleAction": "操作不兼容",
"uiActions.triggers.rowClickkDescription": "表格行的单击",
"uiActions.triggers.rowClickTitle": "表格行单击",
"unifiedDocViewer.docView.table.actions.label": "操作",
"unifiedDocViewer.docView.table.actions.open": "打开操作",
"unifiedDocViewer.docView.table.ignored.multiAboveTooltip": "此字段中的一个或多个值过长,无法搜索或筛选。",
"unifiedDocViewer.docView.table.ignored.multiMalformedTooltip": "此字段包含一个或多个格式错误的值,无法搜索或筛选。",
"unifiedDocViewer.docView.table.ignored.multiUnknownTooltip": "此字段中的一个或多个值被 Elasticsearch 忽略,无法搜索或筛选。",
@ -44277,7 +44275,6 @@
"unifiedDocViewer.docViews.table.filterOutValueButtonTooltip": "筛除值",
"unifiedDocViewer.docViews.table.ignored.multiValueLabel": "包含被忽略的值",
"unifiedDocViewer.docViews.table.ignored.singleValueLabel": "被忽略的值",
"unifiedDocViewer.docViews.table.pinFieldAriaLabel": "固定字段",
"unifiedDocViewer.docViews.table.pinFieldLabel": "固定字段",
"unifiedDocViewer.docViews.table.tableTitle": "表",
"unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel": "在表中切换列",
@ -44285,7 +44282,6 @@
"unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "无法筛选元数据字段是否存在",
"unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "无法筛选脚本字段是否存在",
"unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "无法搜索未编入索引的字段或被忽略的值",
"unifiedDocViewer.docViews.table.unpinFieldAriaLabel": "取消固定字段",
"unifiedDocViewer.docViews.table.unpinFieldLabel": "取消固定字段",
"unifiedDocViewer.fieldChooser.discoverField.actions": "操作",
"unifiedDocViewer.fieldChooser.discoverField.multiField": "多字段",

View file

@ -21,7 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'context', 'svlCommonPage']);
const PageObjects = getPageObjects(['common', 'context', 'svlCommonPage', 'discover']);
const testSubjects = getService('testSubjects');
describe('context filters', function contextSize() {
@ -42,6 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('inclusive filter should be addable via expanded data grid rows', async function () {
await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => {
await dataGrid.clickRowToggle({ isAnchorRow: true, renderMoreRows: true });
await PageObjects.discover.findFieldByNameInDocViewer(TEST_ANCHOR_FILTER_FIELD);
await dataGrid.clickFieldActionInFlyout(
TEST_ANCHOR_FILTER_FIELD,
'addFilterForValueButton'

View file

@ -164,7 +164,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async function () {
await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
const detailsEl = await dataGrid.getDetailsRows();
const defaultMessageEl = await detailsEl[0].findByTestSubject('docTableRowDetailsTitle');
const defaultMessageEl = await detailsEl[0].findByTestSubject('docViewerRowDetailsTitle');
expect(defaultMessageEl).to.be.ok();
await dataGrid.closeFlyout();
});
@ -186,9 +186,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should allow paginating docs in the flyout by clicking in the doc table', async function () {
await retry.try(async function () {
await dataGrid.clickRowToggle({ rowIndex: rowToInspect - 1 });
await testSubjects.exists(`dscDocNavigationPage0`);
await testSubjects.exists(`docViewerFlyoutNavigationPage0`);
await dataGrid.clickRowToggle({ rowIndex: rowToInspect });
await testSubjects.exists(`dscDocNavigationPage1`);
await testSubjects.exists(`docViewerFlyoutNavigationPage1`);
await dataGrid.closeFlyout();
});
});