Skip to content

Commit cd5d0bc

Browse files
kertaljughosta
andauthored
[Discover] Add close button to field popover using Document explorer (#131899)
* Add close button to field popover * Redesign `Copy to clipboard` button when showing JSON Co-authored-by: Julia Rechkunova <[email protected]>
1 parent afe71c7 commit cd5d0bc

12 files changed

+483
-196
lines changed

src/plugins/discover/public/components/discover_grid/discover_grid.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66
* Side Public License, v 1.
77
*/
88

9-
import React, { useCallback, useMemo, useState } from 'react';
9+
import React, { useCallback, useMemo, useState, useRef } from 'react';
1010
import { FormattedMessage } from '@kbn/i18n-react';
1111
import './discover_grid.scss';
1212
import {
1313
EuiDataGridSorting,
14-
EuiDataGridProps,
1514
EuiDataGrid,
1615
EuiScreenReaderOnly,
1716
EuiSpacer,
1817
EuiText,
1918
htmlIdGenerator,
2019
EuiLoadingSpinner,
2120
EuiIcon,
21+
EuiDataGridRefProps,
2222
} from '@elastic/eui';
2323
import type { DataView } from '@kbn/data-views-plugin/public';
2424
import { flattenHit } from '@kbn/data-plugin/public';
@@ -165,9 +165,7 @@ export interface DiscoverGridProps {
165165
onUpdateRowHeight?: (rowHeight: number) => void;
166166
}
167167

168-
export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => {
169-
return <EuiDataGrid {...props} />;
170-
});
168+
export const EuiDataGridMemoized = React.memo(EuiDataGrid);
171169

172170
const CONTROL_COLUMN_IDS_DEFAULT = ['openDetails', 'select'];
173171

@@ -199,6 +197,7 @@ export const DiscoverGrid = ({
199197
rowHeightState,
200198
onUpdateRowHeight,
201199
}: DiscoverGridProps) => {
200+
const dataGridRef = useRef<EuiDataGridRefProps>(null);
202201
const services = useDiscoverServices();
203202
const [selectedDocs, setSelectedDocs] = useState<string[]>([]);
204203
const [isFilterActive, setIsFilterActive] = useState(false);
@@ -232,6 +231,12 @@ export const DiscoverGrid = ({
232231
return rowsFiltered;
233232
}, [rows, usedSelectedDocs, isFilterActive]);
234233

234+
const displayedRowsFlattened = useMemo(() => {
235+
return displayedRows.map((hit) => {
236+
return flattenHit(hit, indexPattern, { includeIgnoredValues: true });
237+
});
238+
}, [displayedRows, indexPattern]);
239+
235240
/**
236241
* Pagination
237242
*/
@@ -290,16 +295,20 @@ export const DiscoverGrid = ({
290295
getRenderCellValueFn(
291296
indexPattern,
292297
displayedRows,
293-
displayedRows
294-
? displayedRows.map((hit) =>
295-
flattenHit(hit, indexPattern, { includeIgnoredValues: true })
296-
)
297-
: [],
298+
displayedRowsFlattened,
298299
useNewFieldsApi,
299300
fieldsToShow,
300-
services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)
301+
services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED),
302+
() => dataGridRef.current?.closeCellPopover()
301303
),
302-
[indexPattern, displayedRows, useNewFieldsApi, fieldsToShow, services.uiSettings]
304+
[
305+
indexPattern,
306+
displayedRowsFlattened,
307+
displayedRows,
308+
useNewFieldsApi,
309+
fieldsToShow,
310+
services.uiSettings,
311+
]
303312
);
304313

305314
/**
@@ -432,6 +441,7 @@ export const DiscoverGrid = ({
432441
expanded: expandedDoc,
433442
setExpanded: setExpandedDoc,
434443
rows: displayedRows,
444+
rowsFlattened: displayedRowsFlattened,
435445
onFilter,
436446
indexPattern,
437447
isDarkMode: services.uiSettings.get('theme:darkMode'),
@@ -463,6 +473,7 @@ export const DiscoverGrid = ({
463473
onColumnResize={onResize}
464474
pagination={paginationObj}
465475
renderCellValue={renderCellValue}
476+
ref={dataGridRef}
466477
rowCount={rowCount}
467478
schemaDetectors={schemaDetectors}
468479
sorting={sorting as EuiDataGridSorting}

src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,57 @@
55
* in compliance with, at your election, the Elastic License 2.0 or the Server
66
* Side Public License, v 1.
77
*/
8+
const mockCopyToClipboard = jest.fn();
9+
jest.mock('@elastic/eui', () => {
10+
const original = jest.requireActual('@elastic/eui');
11+
return {
12+
...original,
13+
copyToClipboard: (value: string) => mockCopyToClipboard(value),
14+
};
15+
});
16+
17+
jest.mock('../../utils/use_discover_services', () => {
18+
const services = {
19+
toastNotifications: {
20+
addInfo: jest.fn(),
21+
},
22+
};
23+
const originalModule = jest.requireActual('../../utils/use_discover_services');
24+
return {
25+
...originalModule,
26+
useDiscoverServices: () => services,
27+
};
28+
});
829

930
import React from 'react';
1031
import { mountWithIntl } from '@kbn/test-jest-helpers';
1132
import { findTestSubject } from '@elastic/eui/lib/test';
12-
import { FilterInBtn, FilterOutBtn, buildCellActions } from './discover_grid_cell_actions';
33+
import { FilterInBtn, FilterOutBtn, buildCellActions, CopyBtn } from './discover_grid_cell_actions';
1334
import { DiscoverGridContext } from './discover_grid_context';
14-
35+
import { EuiButton } from '@elastic/eui';
1536
import { indexPatternMock } from '../../__mocks__/index_pattern';
1637
import { esHits } from '../../__mocks__/es_hits';
17-
import { EuiButton } from '@elastic/eui';
1838
import { DataViewField } from '@kbn/data-views-plugin/public';
39+
import { flattenHit } from '@kbn/data-plugin/common';
40+
41+
const contextMock = {
42+
expanded: undefined,
43+
setExpanded: jest.fn(),
44+
rows: esHits,
45+
rowsFlattened: esHits.map((hit) => flattenHit(hit, indexPatternMock)),
46+
onFilter: jest.fn(),
47+
indexPattern: indexPatternMock,
48+
isDarkMode: false,
49+
selectedDocs: [],
50+
setSelectedDocs: jest.fn(),
51+
};
1952

2053
describe('Discover cell actions ', function () {
2154
it('should not show cell actions for unfilterable fields', async () => {
2255
expect(buildCellActions({ name: 'foo', filterable: false } as DataViewField)).toBeUndefined();
2356
});
2457

2558
it('triggers filter function when FilterInBtn is clicked', async () => {
26-
const contextMock = {
27-
expanded: undefined,
28-
setExpanded: jest.fn(),
29-
rows: esHits,
30-
onFilter: jest.fn(),
31-
indexPattern: indexPatternMock,
32-
isDarkMode: false,
33-
selectedDocs: [],
34-
setSelectedDocs: jest.fn(),
35-
};
36-
3759
const component = mountWithIntl(
3860
<DiscoverGridContext.Provider value={contextMock}>
3961
<FilterInBtn
@@ -55,17 +77,6 @@ describe('Discover cell actions ', function () {
5577
);
5678
});
5779
it('triggers filter function when FilterOutBtn is clicked', async () => {
58-
const contextMock = {
59-
expanded: undefined,
60-
setExpanded: jest.fn(),
61-
rows: esHits,
62-
onFilter: jest.fn(),
63-
indexPattern: indexPatternMock,
64-
isDarkMode: false,
65-
selectedDocs: [],
66-
setSelectedDocs: jest.fn(),
67-
};
68-
6980
const component = mountWithIntl(
7081
<DiscoverGridContext.Provider value={contextMock}>
7182
<FilterOutBtn
@@ -86,4 +97,21 @@ describe('Discover cell actions ', function () {
8697
'-'
8798
);
8899
});
100+
it('triggers clipboard copy when CopyBtn is clicked', async () => {
101+
const component = mountWithIntl(
102+
<DiscoverGridContext.Provider value={contextMock}>
103+
<CopyBtn
104+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105+
Component={(props: any) => <EuiButton {...props} />}
106+
rowIndex={1}
107+
colIndex={1}
108+
columnId="extension"
109+
isExpanded={false}
110+
/>
111+
</DiscoverGridContext.Provider>
112+
);
113+
const button = findTestSubject(component, 'copyClipboardButton');
114+
await button.simulate('click');
115+
expect(mockCopyToClipboard).toHaveBeenCalledWith('jpg');
116+
});
89117
});

src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,25 @@
77
*/
88

99
import React, { useContext } from 'react';
10-
import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
10+
import { copyToClipboard, EuiDataGridColumnCellActionProps } from '@elastic/eui';
1111
import { i18n } from '@kbn/i18n';
1212
import { DataViewField } from '@kbn/data-views-plugin/public';
13-
import { flattenHit } from '@kbn/data-plugin/public';
1413
import { DiscoverGridContext, GridContext } from './discover_grid_context';
14+
import { useDiscoverServices } from '../../utils/use_discover_services';
15+
import { formatFieldValue } from '../../utils/format_value';
1516

1617
function onFilterCell(
1718
context: GridContext,
1819
rowIndex: EuiDataGridColumnCellActionProps['rowIndex'],
1920
columnId: EuiDataGridColumnCellActionProps['columnId'],
2021
mode: '+' | '-'
2122
) {
22-
const row = context.rows[rowIndex];
23-
const flattened = flattenHit(row, context.indexPattern);
23+
const row = context.rowsFlattened[rowIndex];
24+
const value = String(row[columnId]);
2425
const field = context.indexPattern.fields.getByName(columnId);
2526

26-
if (flattened && field) {
27-
context.onFilter(field, flattened[columnId], mode);
27+
if (value && field) {
28+
context.onFilter(field, value, mode);
2829
}
2930
}
3031

@@ -84,8 +85,52 @@ export const FilterOutBtn = ({
8485
);
8586
};
8687

88+
export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) => {
89+
const { indexPattern: dataView, rowsFlattened, rows } = useContext(DiscoverGridContext);
90+
const { fieldFormats, toastNotifications } = useDiscoverServices();
91+
92+
const buttonTitle = i18n.translate('discover.grid.copyClipboardButtonTitle', {
93+
defaultMessage: 'Copy value of {column}',
94+
values: { column: columnId },
95+
});
96+
97+
return (
98+
<Component
99+
onClick={() => {
100+
const rowFlattened = rowsFlattened[rowIndex];
101+
const field = dataView.fields.getByName(columnId);
102+
const value = rowFlattened[columnId];
103+
104+
const valueFormatted =
105+
field?.type === '_source'
106+
? JSON.stringify(rowFlattened, null, 2)
107+
: formatFieldValue(value, rows[rowIndex], fieldFormats, dataView, field, 'text');
108+
copyToClipboard(valueFormatted);
109+
const infoTitle = i18n.translate('discover.grid.copyClipboardToastTitle', {
110+
defaultMessage: 'Copied value of {column} to clipboard.',
111+
values: { column: columnId },
112+
});
113+
114+
toastNotifications.addInfo({
115+
title: infoTitle,
116+
});
117+
}}
118+
iconType="copyClipboard"
119+
aria-label={buttonTitle}
120+
title={buttonTitle}
121+
data-test-subj="copyClipboardButton"
122+
>
123+
{i18n.translate('discover.grid.copyClipboardButton', {
124+
defaultMessage: 'Copy to clipboard',
125+
})}
126+
</Component>
127+
);
128+
};
129+
87130
export function buildCellActions(field: DataViewField) {
88-
if (!field.filterable) {
131+
if (field?.type === '_source') {
132+
return [CopyBtn];
133+
} else if (!field.filterable) {
89134
return undefined;
90135
}
91136

src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface GridContext {
1515
expanded?: ElasticSearchHit;
1616
setExpanded: (hit?: ElasticSearchHit) => void;
1717
rows: ElasticSearchHit[];
18+
rowsFlattened: Array<Record<string, unknown>>;
1819
onFilter: DocViewFilterFn;
1920
indexPattern: DataView;
2021
isDarkMode: boolean;

src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const baseContextMock = {
2121
expanded: undefined,
2222
setExpanded: jest.fn(),
2323
rows: esHits,
24+
rowsFlattened: esHits,
2425
onFilter: jest.fn(),
2526
indexPattern: indexPatternMock,
2627
isDarkMode: false,

src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const baseContextMock = {
1818
expanded: undefined,
1919
setExpanded: jest.fn(),
2020
rows: esHits,
21+
rowsFlattened: esHits,
2122
onFilter: jest.fn(),
2223
indexPattern: indexPatternMock,
2324
isDarkMode: false,

0 commit comments

Comments
 (0)