mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
c0b65d605c
commit
05723d4775
54 changed files with 1885 additions and 1336 deletions
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
},
|
||||
{},
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ export type {
|
|||
DiscoverCustomization,
|
||||
DiscoverCustomizationService,
|
||||
FlyoutCustomization,
|
||||
FlyoutContentProps,
|
||||
SearchBarCustomization,
|
||||
UnifiedHistogramCustomization,
|
||||
TopNavCustomization,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>,
|
||||
]
|
||||
`;
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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} />;
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -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,
|
||||
<></>
|
||||
);
|
|
@ -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();
|
||||
|
|
|
@ -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={() => {}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "複数フィールド",
|
||||
|
|
|
@ -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": "多字段",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue