[OneDiscover][UnifiedDocViewer] Add dedicated column for Pinning/Unpinning rows (#190344)

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

## Summary

This PR adds a dedicated column for pinning/unpinning fields inside
DocViewer.

![Aug-13-2024
15-06-25](https://github.com/user-attachments/assets/93496cdd-e730-4ee6-8597-c78d7bffe07f)



### 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)
This commit is contained in:
Julia Rechkunova 2024-08-19 16:06:54 +02:00 committed by GitHub
parent e7aabcdfae
commit cf58ef9e51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 236 additions and 190 deletions

View file

@ -8,14 +8,7 @@
import React from 'react';
import './field_name.scss';
import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
EuiHighlight,
EuiIcon,
} from '@elastic/eui';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiHighlight } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { FieldIcon, FieldIconProps } from '@kbn/react-field';
@ -30,7 +23,6 @@ interface Props {
fieldIconProps?: Omit<FieldIconProps, 'type'>;
scripted?: boolean;
highlight?: string;
isPinned?: boolean;
}
export function FieldName({
@ -40,7 +32,6 @@ export function FieldName({
fieldIconProps,
scripted = false,
highlight = '',
isPinned = false,
}: Props) {
const typeName = getFieldTypeName(fieldType);
const displayName =
@ -63,17 +54,6 @@ export function FieldName({
<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>

View file

@ -43,95 +43,10 @@ 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 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 correctly for undefined functions 2`] = `Array []`;
exports[`TableActions getFieldCellActions should render the panels correctly for defined onFilter function 1`] = `
Array [
@ -217,47 +132,6 @@ 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,
},
}
}
/>,
]
`;

View file

@ -0,0 +1,63 @@
/*
* 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 { render, screen } from '@testing-library/react';
import { DataViewField } from '@kbn/data-views-plugin/common';
import { TableRow } from './table_cell_actions';
import { getPinColumnControl } from './get_pin_control';
import { EuiDataGridCellValueElementProps } from '@elastic/eui/src/components/datagrid/data_grid_types';
describe('getPinControl', () => {
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',
},
},
];
it('should render correctly', () => {
const control = getPinColumnControl({ rows });
const Cell = control.rowCellRender as React.FC<EuiDataGridCellValueElementProps>;
render(
<Cell
rowIndex={0}
columnId="test"
setCellProps={jest.fn()}
colIndex={0}
isDetails={false}
isExpanded={false}
isExpandable={false}
/>
);
screen.getByTestId('unifiedDocViewer_pinControlButton_message').click();
expect(rows[0].field.onTogglePinned).toHaveBeenCalledWith('message');
});
});

View file

@ -0,0 +1,86 @@
/*
* 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 {
EuiButtonIcon,
EuiDataGridControlColumn,
EuiScreenReaderOnly,
EuiToolTip,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import type { TableRow } from './table_cell_actions';
interface PinControlCellProps {
row: TableRow;
}
const PinControlCell: React.FC<PinControlCellProps> = React.memo(({ row }) => {
const { euiTheme } = useEuiTheme();
const fieldName = row.field.field;
const isPinned = row.field.pinned;
const label = isPinned
? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldLabel', {
defaultMessage: 'Unpin field',
})
: i18n.translate('unifiedDocViewer.docViews.table.pinFieldLabel', {
defaultMessage: 'Pin field',
});
return (
<div
data-test-subj={`unifiedDocViewer_pinControl_${fieldName}`}
className={!isPinned ? 'kbnDocViewer__fieldsGrid__pinAction' : undefined}
css={css`
margin-left: ${isPinned ? '-1px' : 0}; // to align filled/unfilled pin icons better
width: ${euiTheme.size.l};
height: ${euiTheme.size.l};
overflow: hidden;
`}
>
<EuiToolTip content={label} delay="long">
<EuiButtonIcon
data-test-subj={`unifiedDocViewer_pinControlButton_${fieldName}`}
iconSize="m"
iconType={isPinned ? 'pinFilled' : 'pin'}
color="text"
aria-label={label}
onClick={() => {
row.field.onTogglePinned(fieldName);
}}
/>
</EuiToolTip>
</div>
);
});
export const getPinColumnControl = ({ rows }: { rows: TableRow[] }): EuiDataGridControlColumn => {
return {
id: 'pin_field',
width: 32,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>
{i18n.translate('unifiedDocViewer.fieldsTable.pinControlColumnHeader', {
defaultMessage: 'Pin field column',
})}
</span>
</EuiScreenReaderOnly>
),
rowCellRender: ({ rowIndex }) => {
const row = rows[rowIndex];
if (!row) {
return null;
}
return <PinControlCell key={`control-${row.field.field}`} row={row} />;
},
};
};

View file

@ -80,8 +80,26 @@
background-color: tintOrShade($euiColorLightShade, 50%, 0);
}
& .euiDataGridRowCell--firstColumn .euiDataGridRowCell__content {
& [data-gridcell-column-id='name'] .euiDataGridRowCell__content {
padding-top: 0;
padding-bottom: 0;
}
& [data-gridcell-column-id='pin_field'] .euiDataGridRowCell__content {
padding: $euiSizeXS / 2 0 0 $euiSizeXS;
}
.kbnDocViewer__fieldsGrid__pinAction {
opacity: 0;
}
& [data-gridcell-column-id='pin_field']:focus-within {
.kbnDocViewer__fieldsGrid__pinAction {
opacity: 1;
}
}
.euiDataGridRow:hover .kbnDocViewer__fieldsGrid__pinAction {
opacity: 1;
}
}

View file

@ -53,6 +53,7 @@ import {
} from '../doc_viewer_source/get_height';
import { TableFilters, TableFiltersProps, useTableFilters } from './table_filters';
import { TableCell } from './table_cell';
import { getPinColumnControl } from './get_pin_control';
export type FieldRecord = TableRow;
@ -295,6 +296,10 @@ export const DocViewerTable = ({
const rows = useMemo(() => [...pinnedItems, ...restItems], [pinnedItems, restItems]);
const leadingControlColumns = useMemo(() => {
return [getPinColumnControl({ rows })];
}, [rows]);
const { curPageIndex, pageSize, totalPages, changePageIndex, changePageSize } = usePager({
initialPageSize: getPageSize(storage),
totalItems: rows.length,
@ -492,6 +497,7 @@ export const DocViewerTable = ({
renderCellValue={renderCellValue}
renderCellPopover={renderCellPopover}
pagination={pagination}
leadingControlColumns={leadingControlColumns}
/>
</EuiFlexItem>
</>

View file

@ -33,7 +33,7 @@ export const TableCell: React.FC<TableCellProps> = React.memo(
const {
action: { flattenedField },
field: { field, fieldMapping, fieldType, scripted, pinned },
field: { field, fieldMapping, fieldType, scripted },
value: { formattedValue, ignored },
} = row;
@ -49,7 +49,6 @@ export const TableCell: React.FC<TableCellProps> = React.memo(
fieldMapping?.displayName ?? field,
searchTerm
)}
isPinned={pinned}
/>
{isDetails && !!fieldMapping ? (

View file

@ -202,38 +202,6 @@ export const FilterExist: React.FC<TableActionsProps> = ({ Component, row }) =>
);
};
export const PinToggle: React.FC<TableActionsProps> = ({ Component, row }) => {
if (!row) {
return null;
}
const {
field: { field, pinned, onTogglePinned },
} = row;
// Pinned
const pinnedLabel = pinned
? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldLabel', {
defaultMessage: 'Unpin field',
})
: i18n.translate('unifiedDocViewer.docViews.table.pinFieldLabel', {
defaultMessage: 'Pin field',
});
const pinnedIconType = pinned ? 'pinFilled' : 'pin';
return (
<Component
data-test-subj={`togglePinFilterButton-${field}`}
iconType={pinnedIconType}
title={pinnedLabel}
flush="left"
onClick={() => onTogglePinned(field)}
>
{pinnedLabel}
</Component>
);
};
export const ToggleColumn: React.FC<TableActionsProps> = ({ Component, row }) => {
if (!row) {
return null;
@ -293,9 +261,6 @@ export function getFieldCellActions({
},
]
: []),
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
return <PinToggle row={rows[rowIndex]} Component={Component} />;
},
];
}

View file

@ -24,10 +24,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const dataGrid = getService('dataGrid');
const monacoEditor = getService('monacoEditor');
const browser = getService('browser');
describe('discover doc viewer', function describeIndexTests() {
before(async function () {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await browser.setWindowSize(1600, 1200);
});
beforeEach(async () => {
@ -174,7 +176,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(initialFieldsCount).to.above(numberFieldsCount);
const pinnedFieldsCount = 1;
await dataGrid.clickFieldActionInFlyout('agent', 'togglePinFilterButton');
await dataGrid.togglePinActionInFlyout('agent');
await PageObjects.discover.openFilterByFieldTypeInDocViewer();
expect(await find.allByCssSelector('[data-test-subj*="typeFilter"]')).to.have.length(6);
@ -229,5 +231,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
});
describe('pinning fields', function () {
it('should be able to pin and unpin fields', async function () {
await dataGrid.clickRowToggle();
await PageObjects.discover.isShowingDocViewer();
await retry.waitFor('rendered items', async () => {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0;
});
let fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName');
let fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText()));
expect(fieldNames.join(',').startsWith('_id,_ignored,_index,_score,@message')).to.be(true);
expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(false);
await dataGrid.togglePinActionInFlyout('agent');
fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName');
fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText()));
expect(fieldNames.join(',').startsWith('agent,_id,_ignored')).to.be(true);
expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(true);
await dataGrid.togglePinActionInFlyout('@message');
fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName');
fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText()));
expect(fieldNames.join(',').startsWith('@message,agent,_id,_ignored')).to.be(true);
expect(await dataGrid.isFieldPinnedInFlyout('@message')).to.be(true);
await dataGrid.togglePinActionInFlyout('@message');
fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName');
fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText()));
expect(fieldNames.join(',').startsWith('agent,_id,_ignored')).to.be(true);
expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(true);
expect(await dataGrid.isFieldPinnedInFlyout('@message')).to.be(false);
});
});
});
}

View file

@ -567,6 +567,24 @@ export class DataGridService extends FtrService {
await this.testSubjects.click(`${actionName}-${fieldName}`);
}
public async isFieldPinnedInFlyout(fieldName: string): Promise<boolean> {
return !(
await this.testSubjects.getAttribute(`unifiedDocViewer_pinControl_${fieldName}`, 'class')
)?.includes('kbnDocViewer__fieldsGrid__pinAction');
}
public async togglePinActionInFlyout(fieldName: string): Promise<void> {
await this.testSubjects.moveMouseTo(`unifiedDocViewer_pinControl_${fieldName}`);
const isPinned = await this.isFieldPinnedInFlyout(fieldName);
await this.retry.waitFor('pin action to appear', async () => {
return this.testSubjects.exists(`unifiedDocViewer_pinControlButton_${fieldName}`);
});
await this.testSubjects.click(`unifiedDocViewer_pinControlButton_${fieldName}`);
await this.retry.waitFor('pin action to toggle', async () => {
return (await this.isFieldPinnedInFlyout(fieldName)) !== isPinned;
});
}
public async expandFieldNameCellInFlyout(fieldName: string): Promise<void> {
const buttonSelector = 'euiDataGridCellExpandButton';
await this.testSubjects.click(`tableDocViewRow-${fieldName}-name`);

View file

@ -7929,7 +7929,6 @@
"unifiedDocViewer.json.codeEditorAriaLabel": "Affichage JSON en lecture seule dun document Elasticsearch",
"unifiedDocViewer.json.copyToClipboardLabel": "Copier dans le presse-papiers",
"unifiedDocViewer.loadingJSON": "Chargement de JSON",
"unifiedDocViewer.pinnedFieldTooltipContent": "Champ épinglé",
"unifiedDocViewer.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.",
"unifiedDocViewer.sourceViewer.errorMessageTitle": "Une erreur s'est produite.",
"unifiedDocViewer.sourceViewer.refresh": "Actualiser",

View file

@ -7923,7 +7923,6 @@
"unifiedDocViewer.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む",
"unifiedDocViewer.json.copyToClipboardLabel": "クリップボードにコピー",
"unifiedDocViewer.loadingJSON": "JSONを読み込んでいます",
"unifiedDocViewer.pinnedFieldTooltipContent": "固定されたフィールド",
"unifiedDocViewer.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。",
"unifiedDocViewer.sourceViewer.errorMessageTitle": "エラーが発生しました",
"unifiedDocViewer.sourceViewer.refresh": "更新",

View file

@ -7936,7 +7936,6 @@
"unifiedDocViewer.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图",
"unifiedDocViewer.json.copyToClipboardLabel": "复制到剪贴板",
"unifiedDocViewer.loadingJSON": "正在加载 JSON",
"unifiedDocViewer.pinnedFieldTooltipContent": "已固定字段",
"unifiedDocViewer.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。",
"unifiedDocViewer.sourceViewer.errorMessageTitle": "发生错误",
"unifiedDocViewer.sourceViewer.refresh": "刷新",