[Discover] [Unified Data Table] Improve absolute column width handling (#190288)

## Summary

This PR improves the handling of columns with absolute widths in
Discover, including the following enhancements:
- If there are no auto width columns in the profile default app state,
set the last column to auto width so the default columns always fill the
grid.
- If there are no auto width columns remaining when removing a column
from the grid, set the last column to auto width so the remaining
columns fill the grid.
- Add a "Reset width" button to the column header popovers to allow
resetting absolute width columns back to auto width.


https://github.com/user-attachments/assets/0c588969-5720-40e3-91e2-07a83a93b797

Resolves #189817.
Related #188550.

### Checklist

- [ ] 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] 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))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Julia Rechkunova <julia.rechkunova@elastic.co>
This commit is contained in:
Davis McPhee 2024-08-20 17:27:13 -03:00 committed by GitHub
parent 5f19307d26
commit 349fdac456
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 708 additions and 153 deletions

View file

@ -13,7 +13,7 @@ Props description:
| **dataView** | DataView | The used data view. |
| **loadingState** | DataLoadingState | Determines if data is currently loaded. |
| **onFilter** | DocViewFilterFn | Function to add a filter in the grid cell or document flyout. |
| **onResize** | (optional)(colSettings: { columnId: string; width: number }) => void; | Function triggered when a column is resized by the user. |
| **onResize** | (optional)(colSettings: { columnId: string; width: number | undefind }) => void; | Function triggered when a column is resized by the user, passes `undefined` for auto-width. |
| **onSetColumns** | (columns: string[], hideTimeColumn: boolean) => void; | Function to set all columns. |
| **onSort** | (optional)(sort: string[][]) => void; | Function to change sorting of the documents, skipped when isSortEnabled is set to false. |
| **rows** | (optional)DataTableRecord[] | Array of documents provided by Elasticsearch. |
@ -81,7 +81,7 @@ Usage example:
onFilter={() => {
// Add logic to refetch the data when the filter by field was added/removed. Refetch data.
}}
onResize={(colSettings: { columnId: string; width: number }) => {
onResize={(colSettings: { columnId: string; width: number | undefined }) => {
// Update the table state with the new width for the column
}}
onSetColumns={(columns: string[], hideTimeColumn: boolean) => {

View file

@ -24,7 +24,7 @@ export * as columnActions from './src/components/actions/columns';
export { getRowsPerPageOptions } from './src/utils/rows_per_page';
export { popularizeField } from './src/utils/popularize_field';
export { useColumns } from './src/hooks/use_data_grid_columns';
export { useColumns, type UseColumnsProps } from './src/hooks/use_data_grid_columns';
export { OPEN_DETAILS, SELECT_ROW } from './src/components/data_table_columns'; // TODO: deprecate?
export { DataTableRowControl } from './src/components/data_table_row_control';

View file

@ -10,9 +10,10 @@ import { getStateColumnActions } from './columns';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { Capabilities } from '@kbn/core/types';
import { dataViewsMock } from '../../../__mocks__/data_views';
import { UnifiedDataTableSettings } from '../../types';
function getStateColumnAction(
state: { columns?: string[]; sort?: string[][] },
state: { columns?: string[]; sort?: string[][]; settings?: UnifiedDataTableSettings },
setAppState: (state: { columns: string[]; sort?: string[][] }) => void
) {
return getStateColumnActions({
@ -28,6 +29,7 @@ function getStateColumnAction(
columns: state.columns,
sort: state.sort,
defaultOrder: 'desc',
settings: state.settings,
});
}
@ -41,6 +43,7 @@ describe('Test column actions', () => {
actions.onAddColumn('test');
expect(setAppState).toHaveBeenCalledWith({ columns: ['test'] });
});
test('getStateColumnActions with columns and sort in state', () => {
const setAppState = jest.fn();
const actions = getStateColumnAction(
@ -77,4 +80,95 @@ describe('Test column actions', () => {
columns: ['second', 'first'],
});
});
it('should pass settings to setAppState', () => {
const setAppState = jest.fn();
const settings: UnifiedDataTableSettings = { columns: { first: { width: 100 } } };
const actions = getStateColumnAction({ columns: ['first'], settings }, setAppState);
actions.onAddColumn('second');
expect(setAppState).toHaveBeenCalledWith({ columns: ['first', 'second'], settings });
setAppState.mockClear();
actions.onRemoveColumn('second');
expect(setAppState).toHaveBeenCalledWith({ columns: ['first'], settings, sort: [] });
setAppState.mockClear();
actions.onMoveColumn('first', 0);
expect(setAppState).toHaveBeenCalledWith({ columns: ['first'], settings });
setAppState.mockClear();
actions.onSetColumns(['first', 'second'], true);
expect(setAppState).toHaveBeenCalledWith({ columns: ['first', 'second'], settings });
setAppState.mockClear();
});
it('should clean up settings to remove non-existing columns', () => {
const setAppState = jest.fn();
const actions = getStateColumnAction(
{
columns: ['first', 'second', 'third'],
settings: { columns: { first: { width: 100 }, second: { width: 200 } } },
},
setAppState
);
actions.onRemoveColumn('second');
expect(setAppState).toHaveBeenCalledWith({
columns: ['first', 'third'],
settings: { columns: { first: { width: 100 } } },
sort: [],
});
setAppState.mockClear();
actions.onSetColumns(['first', 'third'], true);
expect(setAppState).toHaveBeenCalledWith({
columns: ['first', 'third'],
settings: { columns: { first: { width: 100 } } },
});
});
it('should reset the last column to auto width if only absolute width columns remain', () => {
const setAppState = jest.fn();
let actions = getStateColumnAction(
{
columns: ['first', 'second', 'third'],
settings: { columns: { second: { width: 100 }, third: { width: 100, display: 'test' } } },
},
setAppState
);
actions.onRemoveColumn('first');
expect(setAppState).toHaveBeenCalledWith({
columns: ['second', 'third'],
settings: { columns: { second: { width: 100 }, third: { display: 'test' } } },
sort: [],
});
setAppState.mockClear();
actions = getStateColumnAction(
{
columns: ['first', 'second', 'third'],
settings: { columns: { second: { width: 100 }, third: { width: 100 } } },
},
setAppState
);
actions.onSetColumns(['second', 'third'], true);
expect(setAppState).toHaveBeenCalledWith({
columns: ['second', 'third'],
settings: { columns: { second: { width: 100 } } },
});
});
it('should not reset the last column to auto width if there are remaining auto width columns', () => {
const setAppState = jest.fn();
const actions = getStateColumnAction(
{ columns: ['first', 'second', 'third'], settings: { columns: { third: { width: 100 } } } },
setAppState
);
actions.onRemoveColumn('first');
expect(setAppState).toHaveBeenCalledWith({
columns: ['second', 'third'],
settings: { columns: { third: { width: 100 } } },
sort: [],
});
setAppState.mockClear();
actions.onSetColumns(['second', 'third'], true);
expect(setAppState).toHaveBeenCalledWith({
columns: ['second', 'third'],
settings: { columns: { third: { width: 100 } } },
});
});
});

View file

@ -5,10 +5,93 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Capabilities } from '@kbn/core/public';
import type { Capabilities } from '@kbn/core/public';
import type { DataViewsContract } from '@kbn/data-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { omit } from 'lodash';
import { popularizeField } from '../../utils/popularize_field';
import type { UnifiedDataTableSettings } from '../../types';
export function getStateColumnActions({
capabilities,
dataView,
dataViews,
useNewFieldsApi,
setAppState,
columns,
sort,
defaultOrder,
settings,
}: {
capabilities: Capabilities;
dataView: DataView;
dataViews: DataViewsContract;
useNewFieldsApi: boolean;
setAppState: (state: {
columns: string[];
sort?: string[][];
settings?: UnifiedDataTableSettings;
}) => void;
columns?: string[];
sort: string[][] | undefined;
defaultOrder: string;
settings?: UnifiedDataTableSettings;
}) {
function onAddColumn(columnName: string) {
popularizeField(dataView, columnName, dataViews, capabilities);
const nextColumns = addColumn(columns || [], columnName, useNewFieldsApi);
const nextSort = columnName === '_score' && !sort?.length ? [['_score', defaultOrder]] : sort;
setAppState({ columns: nextColumns, sort: nextSort, settings });
}
function onRemoveColumn(columnName: string) {
popularizeField(dataView, columnName, dataViews, capabilities);
const nextColumns = removeColumn(columns || [], columnName, useNewFieldsApi);
// The state's sort property is an array of [sortByColumn,sortDirection]
const nextSort = sort && sort.length ? sort.filter((subArr) => subArr[0] !== columnName) : [];
let nextSettings = cleanColumnSettings(nextColumns, settings);
// When columns are removed, reset the last column to auto width if only absolute
// width columns remain, to ensure the columns fill the available grid space
if (nextColumns.length < (columns?.length ?? 0)) {
nextSettings = adjustLastColumnWidth(nextColumns, nextSettings);
}
setAppState({ columns: nextColumns, sort: nextSort, settings: nextSettings });
}
function onMoveColumn(columnName: string, newIndex: number) {
const nextColumns = moveColumn(columns || [], columnName, newIndex);
setAppState({ columns: nextColumns, settings });
}
function onSetColumns(nextColumns: string[], hideTimeColumn: boolean) {
// The next line should be gone when classic table will be removed
const actualColumns =
!hideTimeColumn && dataView.timeFieldName && dataView.timeFieldName === nextColumns[0]
? (nextColumns || []).slice(1)
: nextColumns;
let nextSettings = cleanColumnSettings(nextColumns, settings);
// When columns are removed, reset the last column to auto width if only absolute
// width columns remain, to ensure the columns fill the available grid space
if (actualColumns.length < (columns?.length ?? 0)) {
nextSettings = adjustLastColumnWidth(actualColumns, nextSettings);
}
setAppState({ columns: actualColumns, settings: nextSettings });
}
return {
onAddColumn,
onRemoveColumn,
onMoveColumn,
onSetColumns,
};
}
/**
* Helper function to provide a fallback to a single _source column if the given array of columns
@ -25,14 +108,14 @@ function buildColumns(columns: string[], useNewFieldsApi = false) {
return useNewFieldsApi ? [] : ['_source'];
}
export function addColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) {
function addColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) {
if (columns.includes(columnName)) {
return columns;
}
return buildColumns([...columns, columnName], useNewFieldsApi);
}
export function removeColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) {
function removeColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) {
if (!columns.includes(columnName)) {
return columns;
}
@ -42,7 +125,7 @@ export function removeColumn(columns: string[], columnName: string, useNewFields
);
}
export function moveColumn(columns: string[], columnName: string, newIndex: number) {
function moveColumn(columns: string[], columnName: string, newIndex: number) {
if (newIndex < 0 || newIndex >= columns.length || !columns.includes(columnName)) {
return columns;
}
@ -52,58 +135,51 @@ export function moveColumn(columns: string[], columnName: string, newIndex: numb
return modifiedColumns;
}
export function getStateColumnActions({
capabilities,
dataView,
dataViews,
useNewFieldsApi,
setAppState,
columns,
sort,
defaultOrder,
}: {
capabilities: Capabilities;
dataView: DataView;
dataViews: DataViewsContract;
useNewFieldsApi: boolean;
setAppState: (state: { columns: string[]; sort?: string[][] }) => void;
columns?: string[];
sort: string[][] | undefined;
defaultOrder: string;
}) {
function onAddColumn(columnName: string) {
popularizeField(dataView, columnName, dataViews, capabilities);
const nextColumns = addColumn(columns || [], columnName, useNewFieldsApi);
const nextSort = columnName === '_score' && !sort?.length ? [['_score', defaultOrder]] : sort;
setAppState({ columns: nextColumns, sort: nextSort });
function cleanColumnSettings(
columns: string[],
settings?: UnifiedDataTableSettings
): UnifiedDataTableSettings | undefined {
const columnSettings = settings?.columns;
if (!columnSettings) {
return settings;
}
function onRemoveColumn(columnName: string) {
popularizeField(dataView, columnName, dataViews, capabilities);
const nextColumns = removeColumn(columns || [], columnName, useNewFieldsApi);
// The state's sort property is an array of [sortByColumn,sortDirection]
const nextSort = sort && sort.length ? sort.filter((subArr) => subArr[0] !== columnName) : [];
setAppState({ columns: nextColumns, sort: nextSort });
const nextColumnSettings = columns.reduce<NonNullable<UnifiedDataTableSettings['columns']>>(
(acc, column) => (columnSettings[column] ? { ...acc, [column]: columnSettings[column] } : acc),
{}
);
return { ...settings, columns: nextColumnSettings };
}
function adjustLastColumnWidth(
columns: string[],
settings?: UnifiedDataTableSettings
): UnifiedDataTableSettings | undefined {
const columnSettings = settings?.columns;
if (!columns.length || !columnSettings) {
return settings;
}
function onMoveColumn(columnName: string, newIndex: number) {
const nextColumns = moveColumn(columns || [], columnName, newIndex);
setAppState({ columns: nextColumns });
const hasAutoWidthColumn = columns.some((colId) => columnSettings[colId]?.width == null);
if (hasAutoWidthColumn) {
return settings;
}
function onSetColumns(nextColumns: string[], hideTimeColumn: boolean) {
// The next line should be gone when classic table will be removed
const actualColumns =
!hideTimeColumn && dataView.timeFieldName && dataView.timeFieldName === nextColumns[0]
? (nextColumns || []).slice(1)
: nextColumns;
const lastColumn = columns[columns.length - 1];
const lastColumnSettings = omit(columnSettings[lastColumn] ?? {}, 'width');
const lastColumnSettingsOptional = Object.keys(lastColumnSettings).length
? { [lastColumn]: lastColumnSettings }
: undefined;
setAppState({ columns: actualColumns });
}
return {
onAddColumn,
onRemoveColumn,
onMoveColumn,
onSetColumns,
...settings,
columns: {
...omit(columnSettings, lastColumn),
...lastColumnSettingsOptional,
},
};
}

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import React, { useCallback, useState } from 'react';
import { ReactWrapper } from 'enzyme';
import {
EuiButton,
@ -33,6 +33,10 @@ import { DatatableColumnType } from '@kbn/expressions-plugin/common';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CELL_CLASS } from '../utils/get_render_cell_value';
import { defaultTimeColumnWidth } from '../constants';
import { useColumns } from '../hooks/use_data_grid_columns';
import { capabilitiesServiceMock } from '@kbn/core-capabilities-browser-mocks';
import { dataViewsMock } from '../../__mocks__/data_views';
const mockUseDataGridColumnsCellActions = jest.fn((prop: unknown) => []);
jest.mock('@kbn/cell-actions', () => ({
@ -90,6 +94,56 @@ const DataTable = (props: Partial<UnifiedDataTableProps>) => (
</KibanaContextProvider>
);
const capabilities = capabilitiesServiceMock.createStartContract().capabilities;
const renderDataTable = (props: Partial<UnifiedDataTableProps>) => {
const DataTableWrapped = () => {
const [columns, setColumns] = useState(props.columns ?? []);
const [settings, setSettings] = useState(props.settings);
const { onSetColumns } = useColumns({
capabilities,
dataView: dataViewMock,
dataViews: dataViewsMock,
setAppState: useCallback((state) => {
if (state.columns) {
setColumns(state.columns);
}
if (state.settings) {
setSettings(state.settings);
}
}, []),
useNewFieldsApi: true,
columns,
settings,
});
return (
<IntlProvider locale="en">
<DataTable
{...props}
columns={columns}
onSetColumns={onSetColumns}
settings={settings}
onResize={({ columnId, width }) => {
setSettings({
...settings,
columns: {
...settings?.columns,
[columnId]: {
width,
},
},
});
}}
/>
</IntlProvider>
);
};
render(<DataTableWrapped />);
};
async function getComponent(props: UnifiedDataTableProps = getProps()) {
const component = mountWithIntl(<DataTable {...props} />);
await act(async () => {
@ -265,34 +319,19 @@ describe('UnifiedDataTable', () => {
describe('edit field button', () => {
it('should render the edit field button if onFieldEdited is provided', async () => {
const component = await getComponent({
...getProps(),
columns: ['message'],
onFieldEdited: jest.fn(),
});
expect(findTestSubject(component, 'dataGridHeaderCellActionGroup-message').exists()).toBe(
false
);
findTestSubject(component, 'dataGridHeaderCell-message').find('button').simulate('click');
expect(findTestSubject(component, 'dataGridHeaderCellActionGroup-message').exists()).toBe(
true
);
expect(findTestSubject(component, 'gridEditFieldButton').exists()).toBe(true);
renderDataTable({ columns: ['message'], onFieldEdited: jest.fn() });
expect(screen.queryByTestId('dataGridHeaderCellActionGroup-message')).not.toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'message' }));
expect(screen.getByTestId('dataGridHeaderCellActionGroup-message')).toBeInTheDocument();
expect(screen.getByTestId('gridEditFieldButton')).toBeInTheDocument();
});
it('should not render the edit field button if onFieldEdited is not provided', async () => {
const component = await getComponent({
...getProps(),
columns: ['message'],
});
expect(findTestSubject(component, 'dataGridHeaderCellActionGroup-message').exists()).toBe(
false
);
findTestSubject(component, 'dataGridHeaderCell-message').find('button').simulate('click');
expect(findTestSubject(component, 'dataGridHeaderCellActionGroup-message').exists()).toBe(
true
);
expect(findTestSubject(component, 'gridEditFieldButton').exists()).toBe(false);
renderDataTable({ columns: ['message'] });
expect(screen.queryByTestId('dataGridHeaderCellActionGroup-message')).not.toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: 'message' }));
expect(screen.getByTestId('dataGridHeaderCellActionGroup-message')).toBeInTheDocument();
expect(screen.queryByTestId('gridEditFieldButton')).not.toBeInTheDocument();
});
});
@ -793,14 +832,6 @@ describe('UnifiedDataTable', () => {
});
describe('document comparison', () => {
const renderDataTable = (props: Partial<UnifiedDataTableProps>) => {
render(
<IntlProvider locale="en">
<DataTable {...props} />
</IntlProvider>
);
};
const getSelectedDocumentsButton = () => screen.queryByTestId('unifiedDataTableSelectionBtn');
const selectDocument = (document: EsHitRecord) =>
@ -920,4 +951,75 @@ describe('UnifiedDataTable', () => {
expect(findTestSubject(component, 'dataGridHeaderCell-colorIndicator').exists()).toBeFalsy();
});
});
describe('columns', () => {
// Default column width in EUI is hardcoded to 100px for Jest envs
const EUI_DEFAULT_COLUMN_WIDTH = '100px';
const getColumnHeader = (name: string) => screen.getByRole('columnheader', { name });
const queryColumnHeader = (name: string) => screen.queryByRole('columnheader', { name });
const getButton = (name: string) => screen.getByRole('button', { name });
const queryButton = (name: string) => screen.queryByRole('button', { name });
it('should reset the last column to auto width if only absolute width columns remain', async () => {
renderDataTable({
columns: ['message', 'extension', 'bytes'],
settings: {
columns: {
extension: { width: 50 },
bytes: { width: 50 },
},
},
});
expect(getColumnHeader('message')).toHaveStyle({ width: EUI_DEFAULT_COLUMN_WIDTH });
expect(getColumnHeader('extension')).toHaveStyle({ width: '50px' });
expect(getColumnHeader('bytes')).toHaveStyle({ width: '50px' });
userEvent.click(getButton('message'));
userEvent.click(getButton('Remove column'), undefined, { skipPointerEventsCheck: true });
expect(queryColumnHeader('message')).not.toBeInTheDocument();
expect(getColumnHeader('extension')).toHaveStyle({ width: '50px' });
expect(getColumnHeader('bytes')).toHaveStyle({ width: EUI_DEFAULT_COLUMN_WIDTH });
});
it('should not reset the last column to auto width when there are remaining auto width columns', async () => {
renderDataTable({
columns: ['message', 'extension', 'bytes'],
settings: {
columns: {
bytes: { width: 50 },
},
},
});
expect(getColumnHeader('message')).toHaveStyle({ width: EUI_DEFAULT_COLUMN_WIDTH });
expect(getColumnHeader('extension')).toHaveStyle({ width: EUI_DEFAULT_COLUMN_WIDTH });
expect(getColumnHeader('bytes')).toHaveStyle({ width: '50px' });
userEvent.click(getButton('message'));
userEvent.click(getButton('Remove column'), undefined, { skipPointerEventsCheck: true });
expect(queryColumnHeader('message')).not.toBeInTheDocument();
expect(getColumnHeader('extension')).toHaveStyle({ width: EUI_DEFAULT_COLUMN_WIDTH });
expect(getColumnHeader('bytes')).toHaveStyle({ width: '50px' });
});
it('should show the reset width button only for absolute width columns, and allow resetting to default width', async () => {
renderDataTable({
columns: ['message', 'extension'],
settings: {
columns: {
'@timestamp': { width: 50 },
extension: { width: 50 },
},
},
});
expect(getColumnHeader('@timestamp')).toHaveStyle({ width: '50px' });
userEvent.click(getButton('@timestamp'));
userEvent.click(getButton('Reset width'), undefined, { skipPointerEventsCheck: true });
expect(getColumnHeader('@timestamp')).toHaveStyle({ width: `${defaultTimeColumnWidth}px` });
expect(getColumnHeader('message')).toHaveStyle({ width: EUI_DEFAULT_COLUMN_WIDTH });
userEvent.click(getButton('message'));
expect(queryButton('Reset width')).not.toBeInTheDocument();
expect(getColumnHeader('extension')).toHaveStyle({ width: '50px' });
userEvent.click(getButton('extension'));
userEvent.click(getButton('Reset width'), undefined, { skipPointerEventsCheck: true });
expect(getColumnHeader('extension')).toHaveStyle({ width: EUI_DEFAULT_COLUMN_WIDTH });
});
});
});

View file

@ -164,9 +164,9 @@ export interface UnifiedDataTableProps {
*/
onFilter?: DocViewFilterFn;
/**
* Function triggered when a column is resized by the user
* Function triggered when a column is resized by the user, passes `undefined` for auto-width
*/
onResize?: (colSettings: { columnId: string; width: number }) => void;
onResize?: (colSettings: { columnId: string; width: number | undefined }) => void;
/**
* Function to set all columns
*/
@ -810,6 +810,7 @@ export const UnifiedDataTable = ({
showColumnTokens,
headerRowHeightLines,
customGridColumnsConfiguration,
onResize,
}),
[
columnsMeta,
@ -824,6 +825,7 @@ export const UnifiedDataTable = ({
isPlainRecord,
isSortEnabled,
onFilter,
onResize,
settings,
showColumnTokens,
toastNotifications,

View file

@ -50,6 +50,7 @@ describe('Data table columns', function () {
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
onResize: () => {},
});
expect(actual).toMatchSnapshot();
});
@ -72,6 +73,7 @@ describe('Data table columns', function () {
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
onResize: () => {},
});
expect(actual).toMatchSnapshot();
});
@ -99,6 +101,7 @@ describe('Data table columns', function () {
message: { type: 'string', esType: 'keyword' },
timestamp: { type: 'date', esType: 'dateTime' },
},
onResize: () => {},
});
expect(actual).toMatchSnapshot();
});
@ -297,6 +300,7 @@ describe('Data table columns', function () {
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
onResize: () => {},
});
expect(actual).toMatchSnapshot();
});
@ -324,6 +328,7 @@ describe('Data table columns', function () {
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
onResize: () => {},
});
expect(actual).toMatchSnapshot();
});
@ -356,6 +361,7 @@ describe('Data table columns', function () {
columnsMeta: {
extension: { type: 'string' },
},
onResize: () => {},
});
expect(gridColumns[1].schema).toBe('string');
});
@ -386,6 +392,7 @@ describe('Data table columns', function () {
columnsMeta: {
var_test: { type: 'number' },
},
onResize: () => {},
});
expect(gridColumns[1].schema).toBe('numeric');
});
@ -412,6 +419,7 @@ describe('Data table columns', function () {
extension: { type: 'string' },
message: { type: 'string', esType: 'keyword' },
},
onResize: () => {},
});
const extensionGridColumn = gridColumns[0];
@ -442,6 +450,7 @@ describe('Data table columns', function () {
extension: { type: 'string' },
message: { type: 'string', esType: 'keyword' },
},
onResize: () => {},
});
expect(customizedGridColumns).toMatchSnapshot();
@ -484,6 +493,7 @@ describe('Data table columns', function () {
},
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onResize: () => {},
});
const columnDisplayNames = customizedGridColumns.map((column) => column.displayAsText);
expect(columnDisplayNames.includes('test_column_one')).toBeTruthy();

View file

@ -12,6 +12,7 @@ import {
type EuiDataGridColumn,
type EuiDataGridColumnCellAction,
EuiScreenReaderOnly,
EuiListGroupItemProps,
} from '@elastic/eui';
import { type DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { ToastsStart, IUiSettingsClient } from '@kbn/core/public';
@ -30,6 +31,7 @@ import {
import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button';
import { buildEditFieldButton } from './build_edit_field_button';
import { DataTableColumnHeader, DataTableTimeColumnHeader } from './data_table_column_header';
import { UnifiedDataTableProps } from './data_table';
export const getColumnDisplayName = (
columnName: string,
@ -105,6 +107,7 @@ function buildEuiGridColumn({
headerRowHeight,
customGridColumnsConfiguration,
columnDisplay,
onResize,
}: {
numberOfColumns: number;
columnName: string;
@ -126,6 +129,7 @@ function buildEuiGridColumn({
headerRowHeight?: number;
customGridColumnsConfiguration?: CustomGridColumnsConfiguration;
columnDisplay?: string;
onResize: UnifiedDataTableProps['onResize'];
}) {
const dataViewField = !isPlainRecord
? dataView.getFieldByName(columnName)
@ -142,6 +146,26 @@ function buildEuiGridColumn({
editField &&
dataViewField &&
buildEditFieldButton({ hasEditDataViewPermission, dataView, field: dataViewField, editField });
const resetWidthButton: EuiListGroupItemProps | undefined =
onResize && columnWidth > 0
? {
// @ts-expect-error
// We need to force a key here because EuiListGroup uses the array index as a key by default,
// which causes re-render issues with conditional items like this one, and can result in
// incorrect attributes (e.g. title) on the HTML element as well as test failures
key: 'reset-width',
label: i18n.translate('unifiedDataTable.grid.resetColumnWidthButton', {
defaultMessage: 'Reset width',
}),
iconType: 'refresh',
size: 'xs',
iconProps: { size: 'm' },
onClick: () => {
onResize({ columnId: columnName, width: undefined });
},
'data-test-subj': 'unifiedDataTableResetColumnWidth',
}
: undefined;
const columnDisplayName = getColumnDisplayName(
columnName,
@ -193,6 +217,7 @@ function buildEuiGridColumn({
showMoveLeft: !defaultColumns,
showMoveRight: !defaultColumns,
additional: [
...(resetWidthButton ? [resetWidthButton] : []),
...(columnName === '__source'
? []
: [
@ -268,6 +293,7 @@ export function getEuiGridColumns({
showColumnTokens,
headerRowHeightLines,
customGridColumnsConfiguration,
onResize,
}: {
columns: string[];
columnsCellActions?: EuiDataGridColumnCellAction[][];
@ -290,6 +316,7 @@ export function getEuiGridColumns({
showColumnTokens?: boolean;
headerRowHeightLines: number;
customGridColumnsConfiguration?: CustomGridColumnsConfiguration;
onResize: UnifiedDataTableProps['onResize'];
}) {
const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0;
const headerRowHeight = deserializeHeaderRowHeight(headerRowHeightLines);
@ -317,6 +344,7 @@ export function getEuiGridColumns({
headerRowHeight,
customGridColumnsConfiguration,
columnDisplay: settings?.columns?.[column]?.display,
onResize,
})
);
}

View file

@ -12,16 +12,22 @@ import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'
import { Capabilities } from '@kbn/core/public';
import { isEqual } from 'lodash';
import { getStateColumnActions } from '../components/actions/columns';
import { UnifiedDataTableSettings } from '../types';
interface UseColumnsProps {
export interface UseColumnsProps {
capabilities: Capabilities;
dataView: DataView;
dataViews: DataViewsContract;
useNewFieldsApi: boolean;
setAppState: (state: { columns: string[]; sort?: string[][] }) => void;
setAppState: (state: {
columns: string[];
sort?: string[][];
settings?: UnifiedDataTableSettings;
}) => void;
columns?: string[];
sort?: string[][];
defaultOrder?: string;
settings?: UnifiedDataTableSettings;
}
export const useColumns = ({
@ -33,6 +39,7 @@ export const useColumns = ({
columns,
sort,
defaultOrder = 'desc',
settings,
}: UseColumnsProps) => {
const [usedColumns, setUsedColumns] = useState(getColumns(columns, useNewFieldsApi));
useEffect(() => {
@ -53,6 +60,7 @@ export const useColumns = ({
columns: usedColumns,
sort,
defaultOrder,
settings,
}),
[
capabilities,
@ -60,6 +68,7 @@ export const useColumns = ({
dataViews,
defaultOrder,
setAppState,
settings,
sort,
useNewFieldsApi,
usedColumns,

View file

@ -38,5 +38,6 @@
"@kbn/shared-ux-utility",
"@kbn/unified-field-list",
"@kbn/core-notifications-browser",
"@kbn/core-capabilities-browser-mocks",
]
}

View file

@ -23,8 +23,9 @@ import {
SEARCH_FIELDS_FROM_SOURCE,
SORT_DEFAULT_ORDER_SETTING,
} from '@kbn/discover-utils';
import { popularizeField, useColumns } from '@kbn/unified-data-table';
import { UseColumnsProps, popularizeField, useColumns } from '@kbn/unified-data-table';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { ContextErrorMessage } from './components/context_error_message';
import { LoadingStatus } from './services/context_query_state';
import { AppState, GlobalState, isEqualFilters } from './services/context_state';
@ -69,15 +70,23 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
const prevAppState = useRef<AppState>();
const prevGlobalState = useRef<GlobalState>({ filters: [] });
const setAppState = useCallback<UseColumnsProps['setAppState']>(
({ settings, ...rest }) => {
stateContainer.setAppState({ ...rest, grid: settings as DiscoverGridSettings });
},
[stateContainer]
);
const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useColumns({
capabilities,
defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
dataView,
dataViews,
useNewFieldsApi,
setAppState: stateContainer.setAppState,
setAppState,
columns: appState.columns,
sort: appState.sort,
settings: appState.grid,
});
useEffect(() => {
@ -260,6 +269,7 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
useNewFieldsApi={useNewFieldsApi}
isLegacy={isLegacy}
columns={columns}
grid={appState.grid}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
onSetColumns={onSetColumns}

View file

@ -27,7 +27,7 @@ import {
ROW_HEIGHT_OPTION,
SHOW_MULTIFIELDS,
} from '@kbn/discover-utils';
import { DataLoadingState } from '@kbn/unified-data-table';
import { DataLoadingState, UnifiedDataTableProps } from '@kbn/unified-data-table';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { DiscoverGrid } from '../../components/discover_grid';
import { getDefaultRowsPerPage } from '../../../common/constants';
@ -43,6 +43,7 @@ import { onResizeGridColumn } from '../../utils/on_resize_grid_column';
export interface ContextAppContentProps {
columns: string[];
grid?: DiscoverGridSettings;
onAddColumn: (columnsName: string) => void;
onRemoveColumn: (columnsName: string) => void;
onSetColumns: (columnsNames: string[], hideTimeColumn: boolean) => void;
@ -74,6 +75,7 @@ const ActionBarMemoized = React.memo(ActionBar);
export function ContextAppContent({
columns,
grid,
onAddColumn,
onRemoveColumn,
onSetColumns,
@ -94,7 +96,6 @@ export function ContextAppContent({
}: ContextAppContentProps) {
const { uiSettings: config, uiActions } = useDiscoverServices();
const services = useDiscoverServices();
const [gridSettings, setGridSettings] = useState<DiscoverGridSettings>();
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>();
const isAnchorLoading =
@ -151,13 +152,11 @@ export function ContextAppContent({
[addFilter, dataView, onAddColumn, onRemoveColumn]
);
const onResize = useCallback(
const onResize = useCallback<NonNullable<UnifiedDataTableProps['onResize']>>(
(colSettings) => {
setGridSettings((currentGridSettings) =>
onResizeGridColumn(colSettings, currentGridSettings)
);
setAppState({ grid: onResizeGridColumn(colSettings, grid) });
},
[setGridSettings]
[grid, setAppState]
);
return (
@ -221,7 +220,7 @@ export function ContextAppContent({
renderDocumentView={renderDocumentView}
services={services}
configHeaderRowHeight={3}
settings={gridSettings}
settings={grid}
onResize={onResize}
/>
</CellActionsProvider>

View file

@ -20,6 +20,7 @@ import {
import { connectToQueryState, DataPublicPluginStart, FilterManager } from '@kbn/data-plugin/public';
import { DataView } from '@kbn/data-views-plugin/common';
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { getValidFilters } from '../../../utils/get_valid_filters';
import { handleSourceColumnState } from '../../../utils/state_helpers';
@ -32,6 +33,10 @@ export interface AppState {
* Array of filters
*/
filters: Filter[];
/**
* Data Grid related state
*/
grid?: DiscoverGridSettings;
/**
* Number of records to be fetched before anchor records (newer records)
*/

View file

@ -28,6 +28,8 @@ import {
getTextBasedColumnsMeta,
getRenderCustomToolbarWithElements,
type DataGridDensity,
UnifiedDataTableProps,
UseColumnsProps,
} from '@kbn/unified-data-table';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
@ -41,6 +43,7 @@ import {
} from '@kbn/discover-utils';
import useObservable from 'react-use/lib/useObservable';
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { DiscoverGrid } from '../../../../components/discover_grid';
import { getDefaultRowsPerPage } from '../../../../../common/constants';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
@ -86,7 +89,7 @@ const DiscoverGridMemoized = React.memo(DiscoverGrid);
// export needs for testing
export const onResize = (
colSettings: { columnId: string; width: number },
colSettings: { columnId: string; width: number | undefined },
stateContainer: DiscoverStateContainer
) => {
const state = stateContainer.appState.getState();
@ -166,6 +169,13 @@ function DiscoverDocumentsComponent({
stateContainer,
});
const setAppState = useCallback<UseColumnsProps['setAppState']>(
({ settings, ...rest }) => {
stateContainer.appState.update({ ...rest, grid: settings as DiscoverGridSettings });
},
[stateContainer]
);
const {
columns: currentColumns,
onAddColumn,
@ -177,10 +187,11 @@ function DiscoverDocumentsComponent({
defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
dataView,
dataViews,
setAppState: stateContainer.appState.update,
setAppState,
useNewFieldsApi,
columns,
sort,
settings: grid,
});
const setExpandedDoc = useCallback(
@ -190,7 +201,7 @@ function DiscoverDocumentsComponent({
[stateContainer]
);
const onResizeDataGrid = useCallback(
const onResizeDataGrid = useCallback<NonNullable<UnifiedDataTableProps['onResize']>>(
(colSettings) => onResize(colSettings, stateContainer),
[stateContainer]
);

View file

@ -30,9 +30,10 @@ import {
SHOW_FIELD_STATISTICS,
SORT_DEFAULT_ORDER_SETTING,
} from '@kbn/discover-utils';
import { popularizeField, useColumns } from '@kbn/unified-data-table';
import { UseColumnsProps, popularizeField, useColumns } from '@kbn/unified-data-table';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { BehaviorSubject } from 'rxjs';
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { useSavedSearchInitial } from '../../state_management/discover_state_provider';
import { DiscoverStateContainer } from '../../state_management/discover_state';
import { VIEW_MODE } from '../../../../../common/constants';
@ -80,11 +81,12 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
const pageBackgroundColor = useEuiBackgroundColor('plain');
const globalQueryState = data.query.getState();
const { main$ } = stateContainer.dataState.data$;
const [query, savedQuery, columns, sort] = useAppStateSelector((state) => [
const [query, savedQuery, columns, sort, grid] = useAppStateSelector((state) => [
state.query,
state.savedQuery,
state.columns,
state.sort,
state.grid,
]);
const isEsqlMode = useIsEsqlMode();
@ -126,6 +128,13 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
[dataState.fetchStatus, dataState.foundDocuments]
);
const setAppState = useCallback<UseColumnsProps['setAppState']>(
({ settings, ...rest }) => {
stateContainer.appState.update({ ...rest, grid: settings as DiscoverGridSettings });
},
[stateContainer]
);
const {
columns: currentColumns,
onAddColumn,
@ -135,10 +144,11 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
dataView,
dataViews,
setAppState: stateContainer.appState.update,
setAppState,
useNewFieldsApi,
columns,
sort,
settings: grid,
});
// The assistant is getting the state from the url correctly

View file

@ -53,10 +53,13 @@ describe('getDefaultProfileState', () => {
},
defaultColumns: ['messsage', 'bytes'],
dataView: emptyDataView,
esqlQueryColumns: [{ id: '1', name: 'foo', meta: { type: 'string' } }],
esqlQueryColumns: [
{ id: '1', name: 'foo', meta: { type: 'string' } },
{ id: '2', name: 'bar', meta: { type: 'string' } },
],
});
expect(appState).toEqual({
columns: ['foo'],
columns: ['foo', 'bar'],
grid: {
columns: {
foo: {

View file

@ -43,8 +43,13 @@ export const getDefaultProfileState = ({
);
if (validColumns?.length) {
const hasAutoWidthColumn = validColumns.some(({ width }) => !width);
const columns = validColumns.reduce<DiscoverGridSettings['columns']>(
(acc, { name, width }) => (width ? { ...acc, [name]: { width } } : acc),
(acc, { name, width }, index) => {
// Ensure there's at least one auto width column so the columns fill the grid
const skipColumnWidth = !hasAutoWidthColumn && index === validColumns.length - 1;
return width && !skipColumnWidth ? { ...acc, [name]: { width } } : acc;
},
undefined
);

View file

@ -63,6 +63,10 @@ export const createContextAwarenessMocks = ({
name: 'foo',
width: 300,
},
{
name: 'bar',
width: 400,
},
],
rowHeight: 3,
})),

View file

@ -13,6 +13,7 @@ import type { DataView } from '@kbn/data-views-plugin/common';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SORT_DEFAULT_ORDER_SETTING,
isLegacyTableEnabled,
} from '@kbn/discover-utils';
import { Filter } from '@kbn/es-query';
@ -22,9 +23,10 @@ import {
} from '@kbn/presentation-publishing';
import { SortOrder } from '@kbn/saved-search-plugin/public';
import { SearchResponseIncompleteWarning } from '@kbn/search-response-warnings/src/types';
import { columnActions, DataGridDensity, DataLoadingState } from '@kbn/unified-data-table';
import { DataGridDensity, DataLoadingState, useColumns } from '@kbn/unified-data-table';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import { DiscoverDocTableEmbeddable } from '../../components/doc_table/create_doc_table_embeddable';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { getSortForEmbeddable } from '../../utils';
@ -34,6 +36,7 @@ import { isEsqlMode } from '../initialize_fetch';
import type { SearchEmbeddableApi, SearchEmbeddableStateManager } from '../types';
import { DiscoverGridEmbeddable } from './saved_search_grid';
import { getSearchEmbeddableDefaults } from '../get_search_embeddable_defaults';
import { onResizeGridColumn } from '../../utils/on_resize_grid_column';
interface SavedSearchEmbeddableComponentProps {
api: SearchEmbeddableApi & { fetchWarnings$: BehaviorSubject<SearchResponseIncompleteWarning[]> };
@ -60,6 +63,7 @@ export function SearchEmbeddableGridComponent({
rows,
totalHitCount,
columnsMeta,
grid,
] = useBatchedPublishingSubjects(
api.dataLoading,
api.savedSearch$,
@ -67,7 +71,8 @@ export function SearchEmbeddableGridComponent({
api.fetchWarnings$,
stateManager.rows,
stateManager.totalHitCount,
stateManager.columnsMeta
stateManager.columnsMeta,
stateManager.grid
);
const [panelTitle, panelDescription, savedSearchTitle, savedSearchDescription] =
@ -92,32 +97,37 @@ export function SearchEmbeddableGridComponent({
return getSortForEmbeddable(savedSearch.sort, dataView, discoverServices.uiSettings, isEsql);
}, [savedSearch.sort, dataView, isEsql, discoverServices.uiSettings]);
const originalColumns = useMemo(() => savedSearch.columns ?? [], [savedSearch.columns]);
const useNewFieldsApi = !discoverServices.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false);
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useColumns({
capabilities: discoverServices.capabilities,
defaultOrder: discoverServices.uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
dataView,
dataViews: discoverServices.dataViews,
setAppState: (params) => {
if (params.columns) {
stateManager.columns.next(params.columns);
}
if (params.sort) {
stateManager.sort.next(params.sort as SortOrder[]);
}
if (params.settings) {
stateManager.grid.next(params.settings as DiscoverGridSettings);
}
},
useNewFieldsApi,
columns: originalColumns,
sort,
settings: grid,
});
const onStateEditedProps = useMemo(
() => ({
onAddColumn: (columnName: string) => {
if (!savedSearch.columns) {
return;
}
const updatedColumns = columnActions.addColumn(savedSearch.columns, columnName, true);
stateManager.columns.next(updatedColumns);
},
onSetColumns: (updatedColumns: string[]) => {
stateManager.columns.next(updatedColumns);
},
onMoveColumn: (columnName: string, newIndex: number) => {
if (!savedSearch.columns) {
return;
}
const updatedColumns = columnActions.moveColumn(savedSearch.columns, columnName, newIndex);
stateManager.columns.next(updatedColumns);
},
onRemoveColumn: (columnName: string) => {
if (!savedSearch.columns) {
return;
}
const updatedColumns = columnActions.removeColumn(savedSearch.columns, columnName, true);
stateManager.columns.next(updatedColumns);
},
onAddColumn,
onSetColumns,
onMoveColumn,
onRemoveColumn,
onUpdateRowsPerPage: (newRowsPerPage: number | undefined) => {
stateManager.rowsPerPage.next(newRowsPerPage);
},
@ -140,8 +150,24 @@ export function SearchEmbeddableGridComponent({
onUpdateDataGridDensity: (newDensity: DataGridDensity | undefined) => {
stateManager.density.next(newDensity);
},
onResize: (newGridSettings: { columnId: string; width: number | undefined }) => {
stateManager.grid.next(onResizeGridColumn(newGridSettings, grid));
},
}),
[stateManager, savedSearch.columns]
[
onAddColumn,
onSetColumns,
onMoveColumn,
onRemoveColumn,
stateManager.rowsPerPage,
stateManager.rowHeight,
stateManager.headerRowHeight,
stateManager.sort,
stateManager.sampleSize,
stateManager.density,
stateManager.grid,
grid,
]
);
const fetchedSampleSize = useMemo(() => {
@ -151,7 +177,7 @@ export function SearchEmbeddableGridComponent({
const defaults = getSearchEmbeddableDefaults(discoverServices.uiSettings);
const sharedProps = {
columns: savedSearch.columns ?? [],
columns,
dataView,
interceptedWarnings,
onFilter: onAddFilter,
@ -161,7 +187,7 @@ export function SearchEmbeddableGridComponent({
searchDescription: panelDescription || savedSearchDescription,
sort,
totalHitCount,
useNewFieldsApi: !discoverServices.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false),
useNewFieldsApi,
};
if (useLegacyTable) {

View file

@ -32,6 +32,7 @@ export const EDITABLE_SAVED_SEARCH_KEYS: Readonly<Array<keyof SavedSearchAttribu
'rowsPerPage',
'headerRowHeight',
'density',
'grid',
] as const;
/** This constant refers to the dashboard panel specific state */

View file

@ -171,6 +171,7 @@ export const initializeSearchEmbeddableApi = async (
comparators: {
sort: [sort$, (value) => sort$.next(value), (a, b) => deepEqual(a, b)],
columns: [columns$, (value) => columns$.next(value), (a, b) => deepEqual(a, b)],
grid: [grid$, (value) => grid$.next(value), (a, b) => deepEqual(a, b)],
sampleSize: [
sampleSize$,
(value) => sampleSize$.next(value),
@ -198,7 +199,6 @@ export const initializeSearchEmbeddableApi = async (
(value) => serializedSearchSource$.next(value),
],
viewMode: [savedSearchViewMode$, (value) => savedSearchViewMode$.next(value)],
grid: [grid$, (value) => grid$.next(value)],
density: [density$, (value) => density$.next(value)],
},
};

View file

@ -9,13 +9,13 @@
import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
export const onResizeGridColumn = (
colSettings: { columnId: string; width: number },
colSettings: { columnId: string; width: number | undefined },
gridState: DiscoverGridSettings | undefined
): DiscoverGridSettings => {
const grid = { ...(gridState || {}) };
const newColumns = { ...(grid.columns || {}) };
newColumns[colSettings.columnId] = {
width: Math.round(colSettings.width),
};
newColumns[colSettings.columnId] = colSettings.width
? { width: Math.round(colSettings.width) }
: {};
return { ...grid, columns: newColumns };
};

View file

@ -0,0 +1,130 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const dataGrid = getService('dataGrid');
const dashboardAddPanel = getService('dashboardAddPanel');
const PageObjects = getPageObjects([
'common',
'discover',
'header',
'timePicker',
'dashboard',
'unifiedFieldList',
]);
const security = getService('security');
const testResizeColumn = async (field: string) => {
const { originalWidth, newWidth } = await dataGrid.resizeColumn(field, -100);
expect(newWidth).to.be(originalWidth - 100);
await dataGrid.clickResetColumnWidth(field);
const resetWidth = (await (await dataGrid.getHeaderElement(field)).getSize()).width;
expect(resetWidth).to.be(originalWidth);
};
describe('discover data grid column widths', function describeIndexTests() {
before(async () => {
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
});
after(async () => {
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
await kibanaServer.uiSettings.replace({});
await kibanaServer.savedObjects.cleanStandardList();
});
beforeEach(async function () {
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
});
it('should not show reset width button for auto width column', async () => {
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@message');
expect(await dataGrid.resetColumnWidthExists('@message')).to.be(false);
});
it('should show reset width button for absolute width column, and allow resetting to auto width', async () => {
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@message');
await testResizeColumn('@message');
});
it('should reset the last column to auto width if only absolute width columns remain', async () => {
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@message');
const { originalWidth: messageOriginalWidth, newWidth: messageNewWidth } =
await dataGrid.resizeColumn('@message', -300);
expect(messageNewWidth).to.be(messageOriginalWidth - 300);
await PageObjects.unifiedFieldList.clickFieldListItemAdd('bytes');
const { originalWidth: bytesOriginalWidth, newWidth: bytesNewWidth } =
await dataGrid.resizeColumn('bytes', -100);
expect(bytesNewWidth).to.be(bytesOriginalWidth - 100);
let messageWidth = (await (await dataGrid.getHeaderElement('@message')).getSize()).width;
expect(messageWidth).to.be(messageNewWidth);
await dataGrid.clickRemoveColumn('bytes');
messageWidth = (await (await dataGrid.getHeaderElement('@message')).getSize()).width;
expect(messageWidth).to.be(messageOriginalWidth);
});
it('should not reset the last column to auto width when there are remaining auto width columns', async () => {
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@message');
await PageObjects.unifiedFieldList.clickFieldListItemAdd('bytes');
const { originalWidth: bytesOriginalWidth, newWidth: bytesNewWidth } =
await dataGrid.resizeColumn('bytes', -200);
expect(bytesNewWidth).to.be(bytesOriginalWidth - 200);
await PageObjects.unifiedFieldList.clickFieldListItemAdd('agent');
const { originalWidth: agentOriginalWidth, newWidth: agentNewWidth } =
await dataGrid.resizeColumn('agent', -100);
expect(agentNewWidth).to.be(agentOriginalWidth - 100);
await dataGrid.clickRemoveColumn('bytes');
const agentWidth = (await (await dataGrid.getHeaderElement('agent')).getSize()).width;
expect(agentWidth).to.be(agentNewWidth);
});
it('should allow resetting column width in surrounding docs view', async () => {
await PageObjects.unifiedFieldList.clickFieldListItemAdd('@message');
await dataGrid.clickRowToggle({ rowIndex: 0 });
const [, surroundingActionEl] = await dataGrid.getRowActions({ rowIndex: 0 });
await surroundingActionEl.click();
await PageObjects.header.waitUntilLoadingHasFinished();
await testResizeColumn('@message');
});
it('should allow resetting column width in Dashboard panel', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
await dashboardAddPanel.clickOpenAddPanel();
await dashboardAddPanel.addSavedSearch('A Saved Search');
await PageObjects.header.waitUntilLoadingHasFinished();
await testResizeColumn('_source');
});
it('should use custom column width on Dashboard when specified', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
await dashboardAddPanel.clickOpenAddPanel();
await dashboardAddPanel.addSavedSearch('A Saved Search');
await PageObjects.header.waitUntilLoadingHasFinished();
const { originalWidth, newWidth } = await dataGrid.resizeColumn('_source', -100);
expect(newWidth).to.be(originalWidth - 100);
await PageObjects.dashboard.saveDashboard('test');
await browser.refresh();
await PageObjects.header.waitUntilLoadingHasFinished();
const initialWidth = (await (await dataGrid.getHeaderElement('_source')).getSize()).width;
expect(initialWidth).to.be(newWidth);
});
});
}

View file

@ -26,5 +26,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_data_grid_sample_size'));
loadTestFile(require.resolve('./_data_grid_pagination'));
loadTestFile(require.resolve('./_data_grid_density'));
loadTestFile(require.resolve('./_data_grid_column_widths'));
});
}

View file

@ -27,6 +27,7 @@ export class DataGridService extends FtrService {
private readonly find = this.ctx.getService('find');
private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly retry = this.ctx.getService('retry');
private readonly browser = this.ctx.getService('browser');
async getDataGridTableData(): Promise<TabbedGridData> {
const table = await this.find.byCssSelector('.euiDataGrid');
@ -82,6 +83,20 @@ export class DataGridService extends FtrService {
.map((cell) => $(cell).text());
}
public getHeaderElement(field: string) {
return this.testSubjects.find(`dataGridHeaderCell-${field}`);
}
public async resizeColumn(field: string, delta: number) {
const header = await this.getHeaderElement(field);
const originalWidth = (await header.getSize()).width;
const resizer = await header.findByCssSelector(
this.testSubjects.getCssSelector('dataGridColumnResizer')
);
await this.browser.dragAndDrop({ location: resizer }, { location: { x: delta, y: 0 } });
return { originalWidth, newWidth: (await header.getSize()).width };
}
private getCellElementSelector(rowIndex: number = 0, columnIndex: number = 0) {
return `[data-test-subj="euiDataGridBody"] [data-test-subj="dataGridRowCell"][data-gridcell-column-index="${columnIndex}"][data-gridcell-visible-row-index="${rowIndex}"]`;
}
@ -465,6 +480,16 @@ export class DataGridService extends FtrService {
await this.testSubjects.click('gridEditFieldButton');
}
public async resetColumnWidthExists(field: string) {
await this.openColMenuByField(field);
return await this.testSubjects.exists('unifiedDataTableResetColumnWidth');
}
public async clickResetColumnWidth(field: string) {
await this.openColMenuByField(field);
await this.testSubjects.click('unifiedDataTableResetColumnWidth');
}
public async clickGridSettings() {
await this.testSubjects.click('dataGridDisplaySelectorButton');
}

View file

@ -258,12 +258,12 @@ export const CloudSecurityDataTable = ({
[dataView, filterManager, setUrlQuery]
);
const onResize = (colSettings: { columnId: string; width: number }) => {
const onResize = (colSettings: { columnId: string; width: number | undefined }) => {
const grid = persistedSettings || {};
const newColumns = { ...(grid.columns || {}) };
newColumns[colSettings.columnId] = {
width: Math.round(colSettings.width),
};
newColumns[colSettings.columnId] = colSettings.width
? { width: Math.round(colSettings.width) }
: {};
const newGrid = { ...grid, columns: newColumns };
setPersistedSettings(newGrid);
};

View file

@ -186,7 +186,7 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
);
const onColumnResize = useCallback(
({ columnId, width }: { columnId: string; width: number }) => {
({ columnId, width }: { columnId: string; width?: number }) => {
dispatch(
timelineActions.updateColumnWidth({
columnId,
@ -198,9 +198,12 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
[dispatch, timelineId]
);
const onResizeDataGrid = useCallback(
const onResizeDataGrid = useCallback<NonNullable<UnifiedDataTableProps['onResize']>>(
(colSettings) => {
onColumnResize({ columnId: colSettings.columnId, width: Math.round(colSettings.width) });
onColumnResize({
columnId: colSettings.columnId,
...(colSettings.width ? { width: Math.round(colSettings.width) } : {}),
});
},
[onColumnResize]
);

View file

@ -291,7 +291,7 @@ export const setChanged = actionCreator<{ id: string; changed: boolean }>('SET_C
export const updateColumnWidth = actionCreator<{
columnId: string;
id: string;
width: number;
width?: number;
}>('UPDATE_COLUMN_WIDTH');
export const updateRowHeight = actionCreator<{

View file

@ -1531,7 +1531,7 @@ export const updateTimelineColumnWidth = ({
columnId: string;
id: string;
timelineById: TimelineById;
width: number;
width?: number;
}): TimelineById => {
const timeline = timelineById[id];