diff --git a/packages/kbn-unified-data-table/README.md b/packages/kbn-unified-data-table/README.md index 576a676289d7ac..7a2db17781ad1e 100644 --- a/packages/kbn-unified-data-table/README.md +++ b/packages/kbn-unified-data-table/README.md @@ -41,13 +41,12 @@ Props description: | **configRowHeight** | (optional)number | Optional value for providing configuration setting for UnifiedDataTable rows height. | | **showMultiFields** | (optional)boolean | Optional value for providing configuration setting for enabling to display the complex fields in the table. Default is true. | | **maxDocFieldsDisplayed** | (optional)number | Optional value for providing configuration setting for maximum number of document fields to display in the table. Default is 50. | -| **externalControlColumns** | (optional)EuiDataGridControlColumn[] | Optional value for providing EuiDataGridControlColumn list of the additional leading control columns. UnifiedDataTable includes two control columns: Open Details and Select. | +| **rowAdditionalLeadingControls** | (optional)RowControlColumn[] | Optional value for providing an list of the additional leading control columns. UnifiedDataTable includes two control columns: Open Details and Select. | | **totalHits** | (optional)number | Number total hits from ES. | | **onFetchMoreRecords** | (optional)() => void | To fetch more. | | **externalAdditionalControls** | (optional)React.ReactNode | Optional value for providing the additional controls available in the UnifiedDataTable toolbar to manage it's records or state. UnifiedDataTable includes Columns, Sorting and Bulk Actions. | | **rowsPerPageOptions** | (optional)number[] | Optional list of number type values to set custom UnifiedDataTable paging options to display the records per page. | | **renderCustomGridBody** | (optional)(args: EuiDataGridCustomBodyProps) => React.ReactNode; | An optional function called to completely customize and control the rendering of EuiDataGrid's body and cell placement. | -| **trailingControlColumns** | (optional)EuiDataGridControlColumn[] | An optional list of the EuiDataGridControlColumn type for setting trailing control columns standard for EuiDataGrid. | | **visibleCellActions** | (optional)number | An optional value for a custom number of the visible cell actions in the table. By default is up to 3. | | **externalCustomRenderers** | (optional)Record React.ReactNode>; | An optional settings for a specified fields rendering like links. Applied only for the listed fields rendering. | | **consumer** | (optional)string | Name of the UnifiedDataTable consumer component or application. | @@ -141,9 +140,7 @@ Usage example: [browserFields, handleOnPanelClosed, runtimeMappings, timelineId] ); } - externalControlColumns={leadingControlColumns} externalAdditionalControls={additionalControls} - trailingControlColumns={trailingControlColumns} renderCustomGridBody={renderCustomGridBody} rowsPerPageOptions={[10, 30, 40, 100]} showFullScreenButton={false} diff --git a/packages/kbn-unified-data-table/__mocks__/external_control_columns.tsx b/packages/kbn-unified-data-table/__mocks__/external_control_columns.tsx index d67afccc015599..dce5e13a3c89d6 100644 --- a/packages/kbn-unified-data-table/__mocks__/external_control_columns.tsx +++ b/packages/kbn-unified-data-table/__mocks__/external_control_columns.tsx @@ -17,6 +17,7 @@ import { EuiSpacer, EuiDataGridControlColumn, } from '@elastic/eui'; +import type { RowControlColumn } from '../src/types'; const SelectionHeaderCell = () => { return ( @@ -116,3 +117,22 @@ export const testLeadingControlColumn: EuiDataGridControlColumn = { rowCellRender: SelectionRowCell, width: 100, }; + +export const mockRowAdditionalLeadingControls = ['visBarVerticalStacked', 'heart', 'inspect'].map( + (iconType, index): RowControlColumn => ({ + id: `exampleControl_${iconType}`, + headerAriaLabel: `Example Row Control ${iconType}`, + renderControl: (Control, rowProps) => { + return ( + { + alert(`Example "${iconType}" control clicked. Row index: ${rowProps.rowIndex}`); + }} + /> + ); + }, + }) +); diff --git a/packages/kbn-unified-data-table/index.ts b/packages/kbn-unified-data-table/index.ts index 0929c33208fa09..7dace83c3774eb 100644 --- a/packages/kbn-unified-data-table/index.ts +++ b/packages/kbn-unified-data-table/index.ts @@ -25,7 +25,7 @@ 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 { OPEN_DETAILS, SELECT_ROW } from './src/components/data_table_columns'; +export { OPEN_DETAILS, SELECT_ROW } from './src/components/data_table_columns'; // TODO: deprecate? export { DataTableRowControl } from './src/components/data_table_row_control'; export type { diff --git a/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/get_additional_row_control_columns.test.tsx b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/get_additional_row_control_columns.test.tsx new file mode 100644 index 00000000000000..044b864213d828 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/get_additional_row_control_columns.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { getAdditionalRowControlColumns } from './get_additional_row_control_columns'; +import { mockRowAdditionalLeadingControls } from '../../../../__mocks__/external_control_columns'; + +describe('getAdditionalRowControlColumns', () => { + it('should work correctly for 0 controls', () => { + const columns = getAdditionalRowControlColumns([]); + + expect(columns).toHaveLength(0); + }); + + it('should work correctly for 1 control', () => { + const columns = getAdditionalRowControlColumns([mockRowAdditionalLeadingControls[0]]); + + expect(columns.map((column) => column.id)).toEqual([ + `additionalRowControl_${mockRowAdditionalLeadingControls[0].id}`, + ]); + }); + + it('should work correctly for 2 controls', () => { + const columns = getAdditionalRowControlColumns([ + mockRowAdditionalLeadingControls[0], + mockRowAdditionalLeadingControls[1], + ]); + + expect(columns.map((column) => column.id)).toEqual([ + `additionalRowControl_${mockRowAdditionalLeadingControls[0].id}`, + `additionalRowControl_${mockRowAdditionalLeadingControls[1].id}`, + ]); + }); + + it('should work correctly for 3 and more controls', () => { + const columns = getAdditionalRowControlColumns([ + mockRowAdditionalLeadingControls[0], + mockRowAdditionalLeadingControls[1], + mockRowAdditionalLeadingControls[2], + ]); + + expect(columns.map((column) => column.id)).toEqual([ + `additionalRowControl_${mockRowAdditionalLeadingControls[0].id}`, + `additionalRowControl_menuControl`, + ]); + }); +}); diff --git a/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/get_additional_row_control_columns.ts b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/get_additional_row_control_columns.ts new file mode 100644 index 00000000000000..bd297b37bb5df1 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/get_additional_row_control_columns.ts @@ -0,0 +1,25 @@ +/* + * 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 type { EuiDataGridControlColumn } from '@elastic/eui'; +import type { RowControlColumn } from '../../../types'; +import { getRowControlColumn } from './row_control_column'; +import { getRowMenuControlColumn } from './row_menu_control_column'; + +export const getAdditionalRowControlColumns = ( + rowControlColumns: RowControlColumn[] +): EuiDataGridControlColumn[] => { + if (rowControlColumns.length <= 2) { + return rowControlColumns.map(getRowControlColumn); + } + + return [ + getRowControlColumn(rowControlColumns[0]), + getRowMenuControlColumn(rowControlColumns.slice(1)), + ]; +}; diff --git a/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/index.ts b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/index.ts new file mode 100644 index 00000000000000..d3a79fc3a0d506 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { getAdditionalRowControlColumns } from './get_additional_row_control_columns'; diff --git a/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_control_column.test.tsx b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_control_column.test.tsx new file mode 100644 index 00000000000000..360fa7bc235c45 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_control_column.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 type { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; +import { getRowControlColumn } from './row_control_column'; +import { dataTableContextMock } from '../../../../__mocks__/table_context'; +import { UnifiedDataTableContext } from '../../../table_context'; + +describe('getRowControlColumn', () => { + const contextMock = { + ...dataTableContextMock, + }; + + it('should render the component', () => { + const mockClick = jest.fn(); + const props = { + id: 'test_row_control', + headerAriaLabel: 'row control', + renderControl: jest.fn((Control, rowProps) => ( + + )), + }; + const rowControlColumn = getRowControlColumn(props); + const RowControlColumn = + rowControlColumn.rowCellRender as React.FC; + render( + + + + ); + const button = screen.getByTestId('unifiedDataTable_rowControl_test_row_control'); + expect(button).toBeInTheDocument(); + + button.click(); + + expect(mockClick).toHaveBeenCalledWith({ record: contextMock.rows[1], rowIndex: 1 }); + }); +}); diff --git a/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_control_column.tsx b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_control_column.tsx new file mode 100644 index 00000000000000..f8d3ad063fb2d7 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_control_column.tsx @@ -0,0 +1,76 @@ +/* + * 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 } from 'react'; +import { + EuiButtonIcon, + EuiDataGridCellValueElementProps, + EuiDataGridControlColumn, + EuiScreenReaderOnly, + EuiToolTip, +} from '@elastic/eui'; +import { DataTableRowControl, Size } from '../../data_table_row_control'; +import type { RowControlColumn, RowControlProps } from '../../../types'; +import { DEFAULT_CONTROL_COLUMN_WIDTH } from '../../../constants'; +import { useControlColumn } from '../../../hooks/use_control_column'; + +export const RowControlCell = ({ + renderControl, + ...props +}: EuiDataGridCellValueElementProps & { + renderControl: RowControlColumn['renderControl']; +}) => { + const rowProps = useControlColumn(props); + + const Control: React.FC = useMemo( + () => + ({ 'data-test-subj': dataTestSubj, color, disabled, label, iconType, onClick }) => { + return ( + + + { + onClick?.(rowProps); + }} + /> + + + ); + }, + [props.columnId, rowProps] + ); + + return renderControl(Control, rowProps); +}; + +export const getRowControlColumn = ( + rowControlColumn: RowControlColumn +): EuiDataGridControlColumn => { + const { id, headerAriaLabel, headerCellRender, renderControl } = rowControlColumn; + + return { + id: `additionalRowControl_${id}`, + width: DEFAULT_CONTROL_COLUMN_WIDTH, + headerCellRender: + headerCellRender ?? + (() => ( + + {headerAriaLabel} + + )), + rowCellRender: (props) => { + return ; + }, + }; +}; diff --git a/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_menu_control_column.test.tsx b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_menu_control_column.test.tsx new file mode 100644 index 00000000000000..8e26e3f01d0623 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_menu_control_column.test.tsx @@ -0,0 +1,65 @@ +/* + * 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 type { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { render, screen } from '@testing-library/react'; +import { getRowMenuControlColumn } from './row_menu_control_column'; +import { dataTableContextMock } from '../../../../__mocks__/table_context'; +import { mockRowAdditionalLeadingControls } from '../../../../__mocks__/external_control_columns'; +import { UnifiedDataTableContext } from '../../../table_context'; + +describe('getRowMenuControlColumn', () => { + const contextMock = { + ...dataTableContextMock, + }; + + it('should render the component', () => { + const mockClick = jest.fn(); + const props = { + id: 'test_row_menu_control', + headerAriaLabel: 'row control', + renderControl: jest.fn((Control, rowProps) => ( + + )), + }; + const rowMenuControlColumn = getRowMenuControlColumn([ + props, + mockRowAdditionalLeadingControls[0], + mockRowAdditionalLeadingControls[1], + ]); + const RowMenuControlColumn = + rowMenuControlColumn.rowCellRender as React.FC; + render( + + + + ); + const menuButton = screen.getByTestId('unifiedDataTable_test_row_menu_control'); + expect(menuButton).toBeInTheDocument(); + + menuButton.click(); + + expect(screen.getByTestId('exampleRowControl-visBarVerticalStacked')).toBeInTheDocument(); + expect(screen.getByTestId('exampleRowControl-heart')).toBeInTheDocument(); + + const button = screen.getByTestId('unifiedDataTable_rowMenu_test_row_menu_control'); + expect(button).toBeInTheDocument(); + + button.click(); + expect(mockClick).toHaveBeenCalledWith({ record: contextMock.rows[1], rowIndex: 1 }); + }); +}); diff --git a/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_menu_control_column.tsx b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_menu_control_column.tsx new file mode 100644 index 00000000000000..917174618fa37a --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/custom_control_columns/additional_row_control/row_menu_control_column.tsx @@ -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 React, { Fragment, useCallback, useMemo, useState } from 'react'; +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiDataGridCellValueElementProps, + EuiDataGridControlColumn, + EuiPopover, + EuiScreenReaderOnly, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { DataTableRowControl, Size } from '../../data_table_row_control'; +import type { RowControlColumn, RowControlProps } from '../../../types'; +import { DEFAULT_CONTROL_COLUMN_WIDTH } from '../../../constants'; +import { useControlColumn } from '../../../hooks/use_control_column'; + +/** + * Menu button under which all other additional row controls would be placed + */ +export const RowMenuControlCell = ({ + rowControlColumns, + ...props +}: EuiDataGridCellValueElementProps & { + rowControlColumns: RowControlColumn[]; +}) => { + const rowProps = useControlColumn(props); + const [isMoreActionsPopoverOpen, setIsMoreActionsPopoverOpen] = useState(false); + + const buttonLabel = i18n.translate('unifiedDataTable.grid.additionalRowActions', { + defaultMessage: 'Additional actions', + }); + + const getControlComponent: (id: string) => React.FC = useCallback( + (id) => + ({ 'data-test-subj': dataTestSubj, color, disabled, label, iconType, onClick }) => { + return ( + { + onClick?.(rowProps); + setIsMoreActionsPopoverOpen(false); + }} + > + {label} + + ); + }, + [rowProps, setIsMoreActionsPopoverOpen] + ); + + const popoverMenuItems = useMemo( + () => + rowControlColumns.map((rowControlColumn) => { + const Control = getControlComponent(rowControlColumn.id); + return ( + + {rowControlColumn.renderControl(Control, rowProps)} + + ); + }), + [rowControlColumns, rowProps, getControlComponent] + ); + + return ( + + + { + setIsMoreActionsPopoverOpen(!isMoreActionsPopoverOpen); + }} + /> + + + } + isOpen={isMoreActionsPopoverOpen} + closePopover={() => setIsMoreActionsPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + ); +}; + +export const getRowMenuControlColumn = ( + rowControlColumns: RowControlColumn[] +): EuiDataGridControlColumn => { + return { + id: 'additionalRowControl_menuControl', + width: DEFAULT_CONTROL_COLUMN_WIDTH, + headerCellRender: () => ( + + + {i18n.translate('unifiedDataTable.additionalActionsColumnHeader', { + defaultMessage: 'Additional actions column', + })} + + + ), + rowCellRender: (props) => { + return ; + }, + }; +}; diff --git a/packages/kbn-unified-data-table/src/components/custom_control_columns/color_indicator/color_indicator_control_column.tsx b/packages/kbn-unified-data-table/src/components/custom_control_columns/color_indicator/color_indicator_control_column.tsx index dd9be4ab90d133..902667810e613c 100644 --- a/packages/kbn-unified-data-table/src/components/custom_control_columns/color_indicator/color_indicator_control_column.tsx +++ b/packages/kbn-unified-data-table/src/components/custom_control_columns/color_indicator/color_indicator_control_column.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useContext, useEffect } from 'react'; +import React from 'react'; import { css } from '@emotion/react'; import { EuiDataGridControlColumn, @@ -15,7 +15,7 @@ import { EuiDataGridCellValueElementProps, } from '@elastic/eui'; import type { DataTableRecord } from '@kbn/discover-utils'; -import { UnifiedDataTableContext } from '../../../table_context'; +import { useControlColumn } from '../../../hooks/use_control_column'; const COLOR_INDICATOR_WIDTH = 4; @@ -28,32 +28,14 @@ interface ColorIndicatorCellParams { ) => { color: string; label: string } | undefined; } -const ColorIndicatorCell: React.FC = ({ - rowIndex, - setCellProps, - getRowIndicator, -}) => { +const ColorIndicatorCell: React.FC = ({ getRowIndicator, ...props }) => { + const { record } = useControlColumn(props); const { euiTheme } = useEuiTheme(); - const { rows, expanded } = useContext(UnifiedDataTableContext); - const row = rows[rowIndex]; - const configuration = row ? getRowIndicator(row, euiTheme) : undefined; + + const configuration = record ? getRowIndicator(record, euiTheme) : undefined; const color = configuration?.color || 'transparent'; const label = configuration?.label; - useEffect(() => { - if (row.isAnchor) { - setCellProps({ - className: 'unifiedDataTable__cell--highlight', - }); - } else if (expanded && row && expanded.id === row.id) { - setCellProps({ - className: 'unifiedDataTable__cell--expanded', - }); - } else { - setCellProps({ className: '' }); - } - }, [expanded, row, setCellProps]); - return (
{ }); }); - describe('customControlColumnsConfiguration', () => { - const customControlColumnsConfiguration = jest.fn(); - it('should be able to customise the leading control column', async () => { + describe('custom control columns', () => { + it('should be able to customise the leading controls', async () => { const component = await getComponent({ ...getProps(), expandedDoc: { @@ -467,23 +467,19 @@ describe('UnifiedDataTable', () => { setExpandedDoc: jest.fn(), renderDocumentView: jest.fn(), externalControlColumns: [testLeadingControlColumn], - customControlColumnsConfiguration: customControlColumnsConfiguration.mockImplementation( - () => { - return { - leadingControlColumns: [testLeadingControlColumn, testTrailingControlColumns[0]], - trailingControlColumns: [], - }; - } - ), + rowAdditionalLeadingControls: mockRowAdditionalLeadingControls, }); expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy(); expect( - findTestSubject(component, 'test-trailing-column-popover-button').exists() + findTestSubject(component, 'exampleRowControl-visBarVerticalStacked').exists() + ).toBeTruthy(); + expect( + findTestSubject(component, 'unifiedDataTable_additionalRowControl_menuControl').exists() ).toBeTruthy(); }); - it('should be able to customise the trailing control column', async () => { + it('should be able to customise the trailing controls', async () => { const component = await getComponent({ ...getProps(), expandedDoc: { @@ -497,14 +493,7 @@ describe('UnifiedDataTable', () => { setExpandedDoc: jest.fn(), renderDocumentView: jest.fn(), externalControlColumns: [testLeadingControlColumn], - customControlColumnsConfiguration: customControlColumnsConfiguration.mockImplementation( - () => { - return { - leadingControlColumns: [], - trailingControlColumns: [testLeadingControlColumn, testTrailingControlColumns[0]], - }; - } - ), + trailingControlColumns: testTrailingControlColumns, }); expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy(); diff --git a/packages/kbn-unified-data-table/src/components/data_table.tsx b/packages/kbn-unified-data-table/src/components/data_table.tsx index 1a12151c7c18a5..243b86b5408653 100644 --- a/packages/kbn-unified-data-table/src/components/data_table.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.tsx @@ -51,18 +51,19 @@ import { DataTableColumnsMeta, CustomCellRenderer, CustomGridColumnsConfiguration, - CustomControlColumnConfiguration, + RowControlColumn, } from '../types'; import { getDisplayedColumns } from '../utils/columns'; import { convertValueToString } from '../utils/convert_value_to_string'; import { getRowsPerPageOptions } from '../utils/rows_per_page'; import { getRenderCellValueFn } from '../utils/get_render_cell_value'; import { - getAllControlColumns, getEuiGridColumns, getLeadControlColumns, getVisibleColumns, canPrependTimeFieldColumn, + SELECT_ROW, + OPEN_DETAILS, } from './data_table_columns'; import { UnifiedDataTableContext } from '../table_context'; import { getSchemaDetectors } from './data_table_schema'; @@ -85,8 +86,11 @@ import { useSelectedDocs } from '../hooks/use_selected_docs'; import { getColorIndicatorControlColumn, type ColorIndicatorControlColumnParams, + getAdditionalRowControlColumns, } from './custom_control_columns'; +const CONTROL_COLUMN_IDS_DEFAULT = [SELECT_ROW, OPEN_DETAILS]; + export type SortOrder = [string, string]; export enum DataLoadingState { @@ -290,9 +294,20 @@ export interface UnifiedDataTableProps { */ maxDocFieldsDisplayed?: number; /** + * @deprecated Use only `rowAdditionalLeadingControls` instead * Optional value for providing EuiDataGridControlColumn list of the additional leading control columns. UnifiedDataTable includes two control columns: Open Details and Select. */ externalControlColumns?: EuiDataGridControlColumn[]; + /** + * An optional list of the EuiDataGridControlColumn type for setting trailing control columns standard for EuiDataGrid. + * We recommend to rather position all controls in the beginning of rows and use `rowAdditionalLeadingControls` for that + * as number of columns can be dynamically changed and we don't want the controls to become hidden due to horizontal scroll. + */ + trailingControlColumns?: EuiDataGridControlColumn[]; + /** + * Optional value to extend the list of default row actions + */ + rowAdditionalLeadingControls?: RowControlColumn[]; /** * Number total hits from ES */ @@ -327,10 +342,6 @@ export interface UnifiedDataTableProps { * @param gridProps */ renderCustomToolbar?: UnifiedDataTableRenderCustomToolbar; - /** - * An optional list of the EuiDataGridControlColumn type for setting trailing control columns standard for EuiDataGrid. - */ - trailingControlColumns?: EuiDataGridControlColumn[]; /** * An optional value for a custom number of the visible cell actions in the table. By default is up to 3. **/ @@ -347,10 +358,6 @@ export interface UnifiedDataTableProps { * An optional settings for customising the column */ customGridColumnsConfiguration?: CustomGridColumnsConfiguration; - /** - * An optional settings to control which columns to render as trailing and leading control columns - */ - customControlColumnsConfiguration?: CustomControlColumnConfiguration; /** * Name of the UnifiedDataTable consumer component or application */ @@ -396,8 +403,6 @@ export interface UnifiedDataTableProps { export const EuiDataGridMemoized = React.memo(EuiDataGrid); -const CONTROL_COLUMN_IDS_DEFAULT = ['openDetails', 'select']; - export const UnifiedDataTable = ({ ariaLabelledBy, columns, @@ -407,6 +412,7 @@ export const UnifiedDataTable = ({ headerRowHeightState, onUpdateHeaderRowHeight, controlColumnIds = CONTROL_COLUMN_IDS_DEFAULT, + rowAdditionalLeadingControls, dataView, loadingState, onFilter, @@ -437,7 +443,8 @@ export const UnifiedDataTable = ({ services, renderCustomGridBody, renderCustomToolbar, - trailingControlColumns, + externalControlColumns, // TODO: deprecate in favor of rowAdditionalLeadingControls + trailingControlColumns, // TODO: deprecate in favor of rowAdditionalLeadingControls totalHits, onFetchMoreRecords, renderDocumentView, @@ -446,7 +453,6 @@ export const UnifiedDataTable = ({ configRowHeight, showMultiFields = true, maxDocFieldsDisplayed = 50, - externalControlColumns, externalAdditionalControls, rowsPerPageOptions, visibleCellActions, @@ -458,7 +464,6 @@ export const UnifiedDataTable = ({ rowLineHeightOverride, cellActionsMetadata, customGridColumnsConfiguration, - customControlColumnsConfiguration, enableComparisonMode, cellContext, renderCellPopover, @@ -847,10 +852,19 @@ export const UnifiedDataTable = ({ const canSetExpandedDoc = Boolean(setExpandedDoc && !!renderDocumentView); const leadingControlColumns: EuiDataGridControlColumn[] = useMemo(() => { - const internalControlColumns = getLeadControlColumns(canSetExpandedDoc).filter(({ id }) => - controlColumnIds.includes(id) - ); - const leadingColumns = externalControlColumns + const defaultControlColumns = getLeadControlColumns(canSetExpandedDoc); + const internalControlColumns = controlColumnIds + ? // reorder the default controls as per controlColumnIds + controlColumnIds.reduce((acc, id) => { + const controlColumn = defaultControlColumns.find((col) => col.id === id); + if (controlColumn) { + acc.push(controlColumn); + } + return acc; + }, [] as EuiDataGridControlColumn[]) + : defaultControlColumns; + + const leadingColumns: EuiDataGridControlColumn[] = externalControlColumns ? [...internalControlColumns, ...externalControlColumns] : internalControlColumns; @@ -861,17 +875,18 @@ export const UnifiedDataTable = ({ leadingColumns.unshift(colorIndicatorControlColumn); } - return leadingColumns; - }, [canSetExpandedDoc, controlColumnIds, externalControlColumns, getRowIndicator]); - - const controlColumnsConfig = customControlColumnsConfiguration?.({ - controlColumns: getAllControlColumns(), - }); + if (rowAdditionalLeadingControls?.length) { + leadingColumns.push(...getAdditionalRowControlColumns(rowAdditionalLeadingControls)); + } - const customLeadingControlColumn = - controlColumnsConfig?.leadingControlColumns ?? leadingControlColumns; - const customTrailingControlColumn = - controlColumnsConfig?.trailingControlColumns ?? trailingControlColumns; + return leadingColumns; + }, [ + canSetExpandedDoc, + controlColumnIds, + externalControlColumns, + getRowIndicator, + rowAdditionalLeadingControls, + ]); const additionalControls = useMemo(() => { if (!externalAdditionalControls && !selectedDocIds.length) { @@ -1082,7 +1097,7 @@ export const UnifiedDataTable = ({ columns={euiGridColumns} columnVisibility={columnsVisibility} data-test-subj="docTable" - leadingControlColumns={customLeadingControlColumn} + leadingControlColumns={leadingControlColumns} onColumnResize={onResize} pagination={paginationObj} renderCellValue={renderCellValue} @@ -1096,7 +1111,7 @@ export const UnifiedDataTable = ({ gridStyle={gridStyleOverride ?? GRID_STYLE} renderCustomGridBody={renderCustomGridBody} renderCustomToolbar={renderCustomToolbarFn} - trailingControlColumns={customTrailingControlColumn} + trailingControlColumns={trailingControlColumns} cellContext={cellContext} renderCellPopover={renderCustomPopover} /> diff --git a/packages/kbn-unified-data-table/src/components/data_table_columns.tsx b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx index 202a5930187928..4528e323c2047d 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_columns.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx @@ -17,12 +17,16 @@ import { type DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { ToastsStart, IUiSettingsClient } from '@kbn/core/public'; import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { ExpandButton } from './data_table_expand_button'; -import { ControlColumns, CustomGridColumnsConfiguration, UnifiedDataTableSettings } from '../types'; +import { CustomGridColumnsConfiguration, UnifiedDataTableSettings } from '../types'; import type { ValueToStringConverter, DataTableColumnsMeta } from '../types'; import { buildCellActions } from './default_cell_actions'; import { getSchemaByKbnType } from './data_table_schema'; import { SelectButton, SelectAllButton } from './data_table_document_selection'; -import { defaultTimeColumnWidth, ROWS_HEIGHT_OPTIONS } from '../constants'; +import { + defaultTimeColumnWidth, + ROWS_HEIGHT_OPTIONS, + DEFAULT_CONTROL_COLUMN_WIDTH, +} from '../constants'; import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button'; import { buildEditFieldButton } from './build_edit_field_button'; import { DataTableColumnHeader, DataTableTimeColumnHeader } from './data_table_column_header'; @@ -53,7 +57,7 @@ export const SELECT_ROW = 'select'; const openDetails = { id: OPEN_DETAILS, - width: 26, + width: DEFAULT_CONTROL_COLUMN_WIDTH, headerCellRender: () => ( @@ -68,18 +72,11 @@ const openDetails = { const select = { id: SELECT_ROW, - width: 24, + width: DEFAULT_CONTROL_COLUMN_WIDTH, rowCellRender: SelectButton, headerCellRender: SelectAllButton, }; -export function getAllControlColumns(): ControlColumns { - return { - [SELECT_ROW]: select, - [OPEN_DETAILS]: openDetails, - }; -} - export function getLeadControlColumns(canSetExpandedDoc: boolean) { if (!canSetExpandedDoc) { return [select]; diff --git a/packages/kbn-unified-data-table/src/components/data_table_document_selection.tsx b/packages/kbn-unified-data-table/src/components/data_table_document_selection.tsx index b844a4bb537ca8..2a34c4866cb867 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_document_selection.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_document_selection.tsx @@ -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, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { @@ -28,28 +28,19 @@ import { css } from '@emotion/react'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import type { UseSelectedDocsState } from '../hooks/use_selected_docs'; import { UnifiedDataTableContext } from '../table_context'; +import { useControlColumn } from '../hooks/use_control_column'; -export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { +export const SelectButton = (props: EuiDataGridCellValueElementProps) => { + const { record, rowIndex } = useControlColumn(props); const { euiTheme } = useEuiTheme(); - const { selectedDocsState, expanded, rows, isDarkMode } = useContext(UnifiedDataTableContext); + const { selectedDocsState } = useContext(UnifiedDataTableContext); const { isDocSelected, toggleDocSelection } = selectedDocsState; - const doc = useMemo(() => rows[rowIndex], [rows, rowIndex]); const toggleDocumentSelectionLabel = i18n.translate('unifiedDataTable.grid.selectDoc', { defaultMessage: `Select document ''{rowNumber}''`, values: { rowNumber: rowIndex + 1 }, }); - useEffect(() => { - if (expanded && doc && expanded.id === doc.id) { - setCellProps({ - className: 'unifiedDataTable__cell--selected', - }); - } else { - setCellProps({ className: '' }); - } - }, [expanded, doc, setCellProps, isDarkMode]); - return ( { - toggleDocSelection(doc.id); + toggleDocSelection(record.id); }} /> diff --git a/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx b/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx index da3c8a5026ea0f..04ae49abec1414 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx @@ -10,39 +10,27 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UnifiedDataTableContext } from '../table_context'; -import { DataTableRowControl } from './data_table_row_control'; +import { DataTableRowControl, Size } from './data_table_row_control'; +import { useControlColumn } from '../hooks/use_control_column'; /** * Button to expand a given row */ -export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { +export const ExpandButton = (props: EuiDataGridCellValueElementProps) => { + const { record, rowIndex } = useControlColumn(props); + const toolTipRef = useRef(null); const [pressed, setPressed] = useState(false); - const { expanded, setExpanded, rows, isDarkMode, componentsTourSteps } = - useContext(UnifiedDataTableContext); - const current = rows[rowIndex]; + const { expanded, setExpanded, componentsTourSteps } = useContext(UnifiedDataTableContext); const tourStep = componentsTourSteps ? componentsTourSteps.expandButton : undefined; - useEffect(() => { - if (current.isAnchor) { - setCellProps({ - className: 'unifiedDataTable__cell--highlight', - }); - } else if (expanded && current && expanded.id === current.id) { - setCellProps({ - className: 'unifiedDataTable__cell--expanded', - }); - } else { - setCellProps({ className: '' }); - } - }, [expanded, current, setCellProps, isDarkMode]); - const isCurrentRowExpanded = current === expanded; + const isCurrentRowExpanded = record === expanded; const buttonLabel = i18n.translate('unifiedDataTable.grid.viewDoc', { defaultMessage: 'Toggle dialog with details', }); - const testSubj = current.isAnchor + const testSubj = record.isAnchor ? 'docTableExpandToggleColumnAnchor' : 'docTableExpandToggleColumn'; @@ -60,7 +48,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle } return ( - + { - const nextHit = isCurrentRowExpanded ? undefined : current; + const nextHit = isCurrentRowExpanded ? undefined : record; toolTipRef.current?.hideToolTip(); setPressed(Boolean(nextHit)); setExpanded?.(nextHit); diff --git a/packages/kbn-unified-data-table/src/components/data_table_row_control.tsx b/packages/kbn-unified-data-table/src/components/data_table_row_control.tsx index 4ceadea549dce5..0ac0bbd4cb2f68 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_row_control.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_row_control.tsx @@ -7,7 +7,16 @@ */ import React from 'react'; +import classnames from 'classnames'; -export const DataTableRowControl = ({ children }: { children: React.ReactNode }) => { - return {children}; +export enum Size { + normal = 'normal', +} + +export const DataTableRowControl: React.FC<{ size?: Size }> = ({ size, children }) => { + const classes = classnames('unifiedDataTable__rowControl', { + // normalize the size of the control + [`unifiedDataTable__rowControl--size-${size}`]: size, + }); + return {children}; }; diff --git a/packages/kbn-unified-data-table/src/constants.ts b/packages/kbn-unified-data-table/src/constants.ts index c2d5654c602c25..c7cf1793039a5a 100644 --- a/packages/kbn-unified-data-table/src/constants.ts +++ b/packages/kbn-unified-data-table/src/constants.ts @@ -7,6 +7,8 @@ */ import { EuiDataGridStyle } from '@elastic/eui'; +export const DEFAULT_CONTROL_COLUMN_WIDTH = 24; + export const DEFAULT_ROWS_PER_PAGE = 100; export const MAX_LOADED_GRID_ROWS = 10000; diff --git a/packages/kbn-unified-data-table/src/hooks/use_control_column.ts b/packages/kbn-unified-data-table/src/hooks/use_control_column.ts new file mode 100644 index 00000000000000..e2bc05f668508c --- /dev/null +++ b/packages/kbn-unified-data-table/src/hooks/use_control_column.ts @@ -0,0 +1,41 @@ +/* + * 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 { useContext, useEffect, useMemo } from 'react'; +import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import type { DataTableRecord } from '@kbn/discover-utils'; +import { UnifiedDataTableContext } from '../table_context'; + +export const useControlColumn = ({ + rowIndex, + setCellProps, +}: Pick): { + record: DataTableRecord; + rowIndex: number; +} => { + const { expanded, rows } = useContext(UnifiedDataTableContext); + const record = useMemo(() => rows[rowIndex], [rows, rowIndex]); + + useEffect(() => { + if (record.isAnchor) { + setCellProps({ + className: 'unifiedDataTable__cell--highlight', + }); + } else if (expanded && record && expanded.id === record.id) { + setCellProps({ + className: 'unifiedDataTable__cell--expanded', + }); + } else { + setCellProps({ + className: '', + }); + } + }, [expanded, record, setCellProps]); + + return useMemo(() => ({ record, rowIndex }), [record, rowIndex]); +}; diff --git a/packages/kbn-unified-data-table/src/types.ts b/packages/kbn-unified-data-table/src/types.ts index 5914fa03f88276..b08818e1a861a2 100644 --- a/packages/kbn-unified-data-table/src/types.ts +++ b/packages/kbn-unified-data-table/src/types.ts @@ -6,8 +6,13 @@ * Side Public License, v 1. */ -import type { ReactElement } from 'react'; -import type { EuiDataGridCellValueElementProps, EuiDataGridColumn } from '@elastic/eui'; +import type { ReactElement, FC } from 'react'; +import type { + EuiDataGridCellValueElementProps, + EuiDataGridColumn, + IconType, + EuiButtonIconProps, +} from '@elastic/eui'; import type { DataTableRecord } from '@kbn/discover-utils/src/types'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; @@ -70,16 +75,25 @@ export type CustomGridColumnsConfiguration = Record< (props: CustomGridColumnProps) => EuiDataGridColumn >; -export interface ControlColumns { - select: EuiDataGridControlColumn; - openDetails: EuiDataGridControlColumn; +export interface RowControlRowProps { + rowIndex: number; + record: DataTableRecord; } -export interface ControlColumnsProps { - controlColumns: ControlColumns; +export interface RowControlProps { + 'data-test-subj'?: string; + color?: EuiButtonIconProps['color']; + disabled?: boolean; + label: string; + iconType: IconType; + onClick: ((props: RowControlRowProps) => void) | undefined; } -export type CustomControlColumnConfiguration = (props: ControlColumnsProps) => { - leadingControlColumns: EuiDataGridControlColumn[]; - trailingControlColumns?: EuiDataGridControlColumn[]; -}; +export type RowControlComponent = FC; + +export interface RowControlColumn { + id: string; + headerAriaLabel: string; + headerCellRender?: EuiDataGridControlColumn['headerCellRender']; + renderControl: (Control: RowControlComponent, props: RowControlRowProps) => ReactElement; +} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 85c2dd581eecbb..458d3dcb6318cc 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -114,15 +114,10 @@ describe('Discover documents layout', () => { }); test('should render customisations', async () => { - const customControlColumnsConfiguration = () => ({ - leadingControlColumns: [], - trailingControlColumns: [], - }); - const customization: DiscoverCustomization = { id: 'data_table', logsEnabled: true, - customControlColumnsConfiguration, + rowAdditionalLeadingControls: [], }; customisationService.set(customization); @@ -130,8 +125,8 @@ describe('Discover documents layout', () => { const discoverGridComponent = component.find(DiscoverGrid); expect(discoverGridComponent.exists()).toBeTruthy(); - expect(discoverGridComponent.prop('customControlColumnsConfiguration')).toEqual( - customControlColumnsConfiguration + expect(discoverGridComponent.prop('rowAdditionalLeadingControls')).toBe( + customization.rowAdditionalLeadingControls ); expect(discoverGridComponent.prop('externalCustomRenderers')).toBeDefined(); expect(discoverGridComponent.prop('customGridColumnsConfiguration')).toBeDefined(); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index e6fb6472397f3f..e1b5636d010b11 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -259,7 +259,7 @@ function DiscoverDocumentsComponent({ [dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc] ); - const { customControlColumnsConfiguration } = useDiscoverCustomization('data_table') || {}; + const { rowAdditionalLeadingControls } = useDiscoverCustomization('data_table') || {}; const { customCellRenderer, customGridColumnsConfiguration } = useContextualGridCustomisations() || {}; const additionalFieldGroups = useAdditionalFieldGroups(); @@ -435,7 +435,7 @@ function DiscoverDocumentsComponent({ componentsTourSteps={TOUR_STEPS} externalCustomRenderers={cellRenderers} customGridColumnsConfiguration={customGridColumnsConfiguration} - customControlColumnsConfiguration={customControlColumnsConfiguration} + rowAdditionalLeadingControls={rowAdditionalLeadingControls} additionalFieldGroups={additionalFieldGroups} /> diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx index 79ca8afd005e34..986f66e9680c4b 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx @@ -16,21 +16,33 @@ import { useProfileAccessor } from '../../context_awareness'; /** * Customized version of the UnifiedDataTable - * @param props * @constructor */ -export const DiscoverGrid: React.FC = (props) => { +export const DiscoverGrid: React.FC = ({ + rowAdditionalLeadingControls: customRowAdditionalLeadingControls, + ...props +}) => { const getRowIndicatorProvider = useProfileAccessor('getRowIndicatorProvider'); const getRowIndicator = useMemo(() => { return getRowIndicatorProvider(() => undefined)({ dataView: props.dataView }); }, [getRowIndicatorProvider, props.dataView]); + const getRowAdditionalLeadingControlsAccessor = useProfileAccessor( + 'getRowAdditionalLeadingControls' + ); + const rowAdditionalLeadingControls = useMemo(() => { + return getRowAdditionalLeadingControlsAccessor(() => customRowAdditionalLeadingControls)({ + dataView: props.dataView, + }); + }, [getRowAdditionalLeadingControlsAccessor, props.dataView, customRowAdditionalLeadingControls]); + return ( ); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx index 911e69e6d0d333..747bada5b02844 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx @@ -8,6 +8,7 @@ import { EuiBadge } from '@elastic/eui'; import type { DataTableRecord } from '@kbn/discover-utils'; +import type { RowControlColumn } from '@kbn/unified-data-table'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -71,6 +72,31 @@ export const exampleDataSourceProfileProvider: DataSourceProfileProvider = { }, }; }, + getRowAdditionalLeadingControls: (prev) => (params) => { + const additionalControls = prev(params) || []; + + return [ + ...additionalControls, + ...['visBarVerticalStacked', 'heart', 'inspect'].map( + (iconType, index): RowControlColumn => ({ + id: `exampleControl_${iconType}`, + headerAriaLabel: `Example Row Control ${iconType}`, + renderControl: (Control, rowProps) => { + return ( + { + alert(`Example "${iconType}" control clicked. Row index: ${rowProps.rowIndex}`); + }} + /> + ); + }, + }) + ), + ]; + }, getDefaultAppState: () => () => ({ columns: [ { diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index 38c7116a765b1b..b6e4d4558162d9 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -38,11 +38,20 @@ export interface DefaultAppStateExtension { rowHeight?: number; } +export interface RowControlsExtensionParams { + dataView: DataView; +} + export interface Profile { - getCellRenderers: () => CustomCellRenderer; - getDocViewer: (params: DocViewerExtensionParams) => DocViewerExtension; getDefaultAppState: (params: DefaultAppStateExtensionParams) => DefaultAppStateExtension; + // Data grid + getCellRenderers: () => CustomCellRenderer; getRowIndicatorProvider: ( params: RowIndicatorExtensionParams ) => UnifiedDataTableProps['getRowIndicator'] | undefined; + getRowAdditionalLeadingControls: ( + params: RowControlsExtensionParams + ) => UnifiedDataTableProps['rowAdditionalLeadingControls'] | undefined; + // Doc viewer + getDocViewer: (params: DocViewerExtensionParams) => DocViewerExtension; } diff --git a/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts b/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts index 3e6e510488fbd3..d53485911d12c3 100644 --- a/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts +++ b/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { CustomControlColumnConfiguration } from '@kbn/unified-data-table'; +import type { UnifiedDataTableProps } from '@kbn/unified-data-table'; export interface DataTableCustomization { id: 'data_table'; logsEnabled: boolean; // TODO / NOTE: Just temporary until Discover's data type contextual awareness lands. - customControlColumnsConfiguration?: CustomControlColumnConfiguration; + rowAdditionalLeadingControls?: UnifiedDataTableProps['rowAdditionalLeadingControls']; } diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_row_additional_leading_controls.ts b/test/functional/apps/discover/context_awareness/extensions/_get_row_additional_leading_controls.ts new file mode 100644 index 00000000000000..be536fd6cdbe93 --- /dev/null +++ b/test/functional/apps/discover/context_awareness/extensions/_get_row_additional_leading_controls.ts @@ -0,0 +1,64 @@ +/* + * 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 kbnRison from '@kbn/rison'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'discover']); + const testSubjects = getService('testSubjects'); + const dataViews = getService('dataViews'); + + describe('extension getRowAdditionalLeadingControls', () => { + describe('ES|QL mode', () => { + it('should render logs controls for logs data source', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-logs | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.existOrFail('exampleLogsControl_visBarVerticalStacked'); + await testSubjects.existOrFail('unifiedDataTable_additionalRowControl_menuControl'); + }); + + it('should not render logs controls for non-logs data source', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-metrics | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.missingOrFail('exampleLogsControl_visBarVerticalStacked'); + await testSubjects.missingOrFail('unifiedDataTable_additionalRowControl_menuControl'); + }); + }); + + describe('data view mode', () => { + it('should render logs controls for logs data source', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.existOrFail('exampleLogsControl_visBarVerticalStacked'); + await testSubjects.existOrFail('unifiedDataTable_additionalRowControl_menuControl'); + }); + + it('should not render logs controls for non-logs data source', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-metrics'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.missingOrFail('exampleLogsControl_visBarVerticalStacked'); + await testSubjects.missingOrFail('unifiedDataTable_additionalRowControl_menuControl'); + }); + }); + }); +} diff --git a/test/functional/apps/discover/context_awareness/index.ts b/test/functional/apps/discover/context_awareness/index.ts index 82f03e7f54bbc8..0bba18a3392633 100644 --- a/test/functional/apps/discover/context_awareness/index.ts +++ b/test/functional/apps/discover/context_awareness/index.ts @@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./_root_profile')); loadTestFile(require.resolve('./_data_source_profile')); loadTestFile(require.resolve('./extensions/_get_row_indicator_provider')); + loadTestFile(require.resolve('./extensions/_get_row_additional_leading_controls')); loadTestFile(require.resolve('./extensions/_get_doc_viewer')); loadTestFile(require.resolve('./extensions/_get_cell_renderers')); loadTestFile(require.resolve('./extensions/_get_default_app_state')); diff --git a/test/functional/apps/discover/esql/_esql_view.ts b/test/functional/apps/discover/esql/_esql_view.ts index a66471b921528e..0ab0e30a45eab0 100644 --- a/test/functional/apps/discover/esql/_esql_view.ts +++ b/test/functional/apps/discover/esql/_esql_view.ts @@ -194,8 +194,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const cell = await dataGrid.getCellElementExcludingControlColumns(0, 1); expect(await cell.getVisibleText()).to.be(' - '); expect(await dataGrid.getHeaders()).to.eql([ - 'Control column', 'Select column', + 'Control column', 'Numberbytes', 'machine.ram_range', ]); diff --git a/test/functional/screenshots/baseline/dashboard_embed_mode_scrolling.png b/test/functional/screenshots/baseline/dashboard_embed_mode_scrolling.png index 3fd3025ebb9a18..94de1a1c3cc4f7 100644 Binary files a/test/functional/screenshots/baseline/dashboard_embed_mode_scrolling.png and b/test/functional/screenshots/baseline/dashboard_embed_mode_scrolling.png differ diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 5014846a674074..70a67d33ffd002 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -139,6 +139,22 @@ export class DataGridService extends FtrService { 'euiDataGridCellExpandButton' ); await actionButton.click(); + await this.retry.waitFor('popover to be opened', async () => { + return await this.testSubjects.exists('euiDataGridExpansionPopover'); + }); + } + + /** + * Clicks grid cell 'expand' action button + * @param rowIndex data row index starting from 0 (0 means 1st row) + * @param columnIndex column index starting from 0 (0 means 1st column) + */ + public async clickCellExpandButtonExcludingControlColumns( + rowIndex: number = 0, + columnIndex: number = 0 + ) { + const controlsCount = await this.getControlColumnsCount(); + await this.clickCellExpandButton(rowIndex, controlsCount + columnIndex); } /** diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.tsx index 577e068483427c..826fcdab659156 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiCode } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; export const contentLabel = i18n.translate('xpack.logsExplorer.dataTable.header.popover.content', { @@ -21,17 +20,6 @@ export const resourceLabel = i18n.translate( } ); -export const actionsLabel = i18n.translate('xpack.logsExplorer.dataTable.header.popover.actions', { - defaultMessage: 'Actions', -}); - -export const actionsLabelLowerCase = i18n.translate( - 'xpack.logsExplorer.dataTable.header.popover.actions.lowercase', - { - defaultMessage: 'actions', - } -); - export const actionFilterForText = (text: string) => i18n.translate('xpack.logsExplorer.flyoutDetail.value.hover.filterFor', { defaultMessage: 'Filter for this {value}', @@ -109,35 +97,18 @@ export const resourceHeaderTooltipParagraph = i18n.translate( } ); -export const actionsHeaderTooltipParagraph = i18n.translate( - 'xpack.logsExplorer.dataTable.header.actions.tooltip.paragraph', +export const actionsHeaderAriaLabelDegradedAction = i18n.translate( + 'xpack.logsExplorer.dataTable.controlColumnHeader.degradedDocArialLabel', { - defaultMessage: 'Fields that provide actionable information, such as:', + defaultMessage: 'Access to degraded docs', } ); -export const actionsHeaderTooltipExpandAction = i18n.translate( - 'xpack.logsExplorer.dataTable.header.actions.tooltip.expand', - { defaultMessage: 'Expand log details' } -); - -export const actionsHeaderTooltipDegradedAction = ( - - _ignored - - ), - }} - /> -); - -export const actionsHeaderTooltipStacktraceAction = i18n.translate( - 'xpack.logsExplorer.dataTable.header.actions.tooltip.stacktrace', - { defaultMessage: 'Access to available stacktraces based on:' } +export const actionsHeaderAriaLabelStacktraceAction = i18n.translate( + 'xpack.logsExplorer.dataTable.controlColumnHeader.stacktraceArialLabel', + { + defaultMessage: 'Access to available stacktraces', + } ); export const degradedDocButtonLabelWhenPresent = i18n.translate( diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/actions_column_tooltip.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/actions_column_tooltip.tsx deleted file mode 100644 index c33c514bf37892..00000000000000 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/actions_column_tooltip.tsx +++ /dev/null @@ -1,95 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { css } from '@emotion/react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { - actionsHeaderTooltipExpandAction, - actionsHeaderTooltipDegradedAction, - actionsHeaderTooltipParagraph, - actionsHeaderTooltipStacktraceAction, - actionsLabel, - actionsLabelLowerCase, -} from '../../common/translations'; -import { TooltipButton } from './tooltip_button'; -import * as constants from '../../../../common/constants'; -import { FieldWithToken } from './field_with_token'; - -const spacingCSS = css` - margin-bottom: ${euiThemeVars.euiSizeS}; -`; - -export const ActionsColumnTooltip = () => { - return ( - -
- -

{actionsHeaderTooltipParagraph}

-
- - - - - - -

{actionsHeaderTooltipExpandAction}

-
-
-
- - - - - - -

{actionsHeaderTooltipDegradedAction}

-
-
-
- - - - - - -

{actionsHeaderTooltipStacktraceAction}

-
-
-
-
- {[ - constants.ERROR_STACK_TRACE, - constants.ERROR_EXCEPTION_STACKTRACE, - constants.ERROR_LOG_STACKTRACE, - ].map((field) => ( - - ))} -
-
-
- ); -}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_control_column.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_control_column.tsx index 43edee4cf73af8..89bc38482c803c 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_control_column.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_control_column.tsx @@ -5,19 +5,16 @@ * 2.0. */ -import React, { ComponentClass } from 'react'; -import { - OPEN_DETAILS, - SELECT_ROW, - type ControlColumnsProps, - DataTableRowControl, -} from '@kbn/unified-data-table'; -import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; -import type { DataTableRecord } from '@kbn/discover-utils/src/types'; -import { useActor } from '@xstate/react'; +import React from 'react'; import { LogDocument } from '@kbn/discover-utils/src'; -import { LogsExplorerControllerStateService } from '../state_machines/logs_explorer_controller'; +import type { + UnifiedDataTableProps, + RowControlComponent, + RowControlRowProps, +} from '@kbn/unified-data-table'; import { + actionsHeaderAriaLabelDegradedAction, + actionsHeaderAriaLabelStacktraceAction, degradedDocButtonLabelWhenNotPresent, degradedDocButtonLabelWhenPresent, stacktraceAvailableControlButton, @@ -25,122 +22,79 @@ import { } from '../components/common/translations'; import * as constants from '../../common/constants'; import { getStacktraceFields } from '../utils/get_stack_trace'; -import { ActionsColumnTooltip } from '../components/virtual_columns/column_tooltips/actions_column_tooltip'; - -const ConnectedDegradedDocs = ({ - rowIndex, - service, -}: { - rowIndex: number; - service: LogsExplorerControllerStateService; -}) => { - const [state] = useActor(service); - if (state.matches('initialized') && state.context.rows[rowIndex]) { - return ; - } - - return null; -}; -const ConnectedStacktraceDocs = ({ - rowIndex, - service, +const DegradedDocs = ({ + Control, + rowProps: { record }, }: { - rowIndex: number; - service: LogsExplorerControllerStateService; + Control: RowControlComponent; + rowProps: RowControlRowProps; }) => { - const [state] = useActor(service); - if (state.matches('initialized') && state.context.rows[rowIndex]) { - return ; - } - - return null; -}; - -const DegradedDocs = ({ row, rowIndex }: { row: DataTableRecord; rowIndex: number }) => { - const isDegradedDocumentExists = constants.DEGRADED_DOCS_FIELD in row.raw; + const isDegradedDocumentExists = constants.DEGRADED_DOCS_FIELD in record.raw; return isDegradedDocumentExists ? ( - - - - - + ) : ( - - - - - + ); }; -const Stacktrace = ({ row, rowIndex }: { row: DataTableRecord; rowIndex: number }) => { - const stacktrace = getStacktraceFields(row as LogDocument); +const Stacktrace = ({ + Control, + rowProps: { record }, +}: { + Control: RowControlComponent; + rowProps: RowControlRowProps; +}) => { + const stacktrace = getStacktraceFields(record as LogDocument); const hasValue = Object.values(stacktrace).some((value) => value); - return ( - - - - - + return hasValue ? ( + + ) : ( + ); }; -export const createCustomControlColumnsConfiguration = - (service: LogsExplorerControllerStateService) => - ({ controlColumns }: ControlColumnsProps) => { - const checkBoxColumn = controlColumns[SELECT_ROW]; - const openDetails = controlColumns[OPEN_DETAILS]; - const ExpandButton = - openDetails.rowCellRender as ComponentClass; - const actionsColumn = { - id: 'actionsColumn', - width: constants.ACTIONS_COLUMN_WIDTH, - headerCellRender: ActionsColumnTooltip, - rowCellRender: ({ rowIndex, setCellProps, ...rest }: EuiDataGridCellValueElementProps) => { - return ( - - - - - - ); +export const getRowAdditionalControlColumns = + (): UnifiedDataTableProps['rowAdditionalLeadingControls'] => { + return [ + { + id: 'connectedDegradedDocs', + headerAriaLabel: actionsHeaderAriaLabelDegradedAction, + renderControl: (Control, rowProps) => { + return ; + }, }, - }; - - return { - leadingControlColumns: [checkBoxColumn, actionsColumn], - }; + { + id: 'connectedStacktraceDocs', + headerAriaLabel: actionsHeaderAriaLabelStacktraceAction, + renderControl: (Control, rowProps) => { + return ; + }, + }, + ]; }; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx index 7b193f4aafff88..557cbe4dd2728f 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx @@ -82,8 +82,8 @@ export const createLogsExplorerProfileCustomizations = customizations.set({ id: 'data_table', logsEnabled: true, - customControlColumnsConfiguration: await import('./custom_control_column').then((module) => - module.createCustomControlColumnsConfiguration(service) + rowAdditionalLeadingControls: await import('./custom_control_column').then((module) => + module.getRowAdditionalControlColumns() ), }); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9abcbd4c40cde0..d81b810c983b8b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -24325,13 +24325,8 @@ "xpack.lists.services.items.fileUploadFromFileSystem": "Fichier chargé depuis le système de fichiers de {fileName}", "xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available": "Traces d'appel disponibles", "xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "Traces d'appel indisponibles", - "xpack.logsExplorer.dataTable.header.actions.tooltip.expand": "Développer les détails du log", - "xpack.logsExplorer.dataTable.header.actions.tooltip.paragraph": "Les champs fournissant des informations exploitables, comme :", - "xpack.logsExplorer.dataTable.header.actions.tooltip.stacktrace": "L'accès aux traces d'appel disponibles est basé sur :", "xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1": "Affiche le {logLevel} du document et les champs {message}.", "xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2": "Lorsque le champ de message est vide, l'une des informations suivantes s'affiche :", - "xpack.logsExplorer.dataTable.header.popover.actions": "Actions", - "xpack.logsExplorer.dataTable.header.popover.actions.lowercase": "actions", "xpack.logsExplorer.dataTable.header.popover.content": "Contenu", "xpack.logsExplorer.dataTable.header.popover.resource": "Ressource", "xpack.logsExplorer.dataTable.header.resource.tooltip.paragraph": "Les champs fournissant des informations sur la source du document, comme :", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c5be020320a045..267f2bb3c8f2ee 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -24250,13 +24250,8 @@ "xpack.lists.services.items.fileUploadFromFileSystem": "ファイルは{fileName}のファイルシステムからアップロードされました", "xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available": "スタックトレースがあります", "xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "スタックトレースがありません", - "xpack.logsExplorer.dataTable.header.actions.tooltip.expand": "ログの詳細を展開", - "xpack.logsExplorer.dataTable.header.actions.tooltip.paragraph": "次のようなアクショナブルな情報を提供するフィールド:", - "xpack.logsExplorer.dataTable.header.actions.tooltip.stacktrace": "次に基づいて使用可能なスタックトレースにアクセス:", "xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1": "ドキュメントの{logLevel}と{message}フィールドを表示します。", "xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2": "メッセージフィールドが空のときには、次のいずれかが表示されます。", - "xpack.logsExplorer.dataTable.header.popover.actions": "アクション", - "xpack.logsExplorer.dataTable.header.popover.actions.lowercase": "アクション", "xpack.logsExplorer.dataTable.header.popover.content": "コンテンツ", "xpack.logsExplorer.dataTable.header.popover.resource": "リソース", "xpack.logsExplorer.dataTable.header.resource.tooltip.paragraph": "次のようなドキュメントのソースに関する情報を提供するフィールド:", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0497f0f96fe316..287dde9aaaaf7e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -24360,13 +24360,8 @@ "xpack.lists.services.items.fileUploadFromFileSystem": "从 {fileName} 的文件系统上传的文件", "xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available": "堆栈跟踪可用", "xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "堆栈跟踪不可用", - "xpack.logsExplorer.dataTable.header.actions.tooltip.expand": "展开日志详情", - "xpack.logsExplorer.dataTable.header.actions.tooltip.paragraph": "提供可操作信息的字段,例如:", - "xpack.logsExplorer.dataTable.header.actions.tooltip.stacktrace": "基于以下项访问可用堆栈跟踪:", "xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1": "显示该文档的 {logLevel} 和 {message} 字段。", "xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2": "消息字段为空时,将显示以下项之一:", - "xpack.logsExplorer.dataTable.header.popover.actions": "操作", - "xpack.logsExplorer.dataTable.header.popover.actions.lowercase": "操作", "xpack.logsExplorer.dataTable.header.popover.content": "内容", "xpack.logsExplorer.dataTable.header.popover.resource": "资源", "xpack.logsExplorer.dataTable.header.resource.tooltip.paragraph": "提供有关文档来源信息的字段,例如:", diff --git a/x-pack/test/functional/apps/observability_logs_explorer/columns_selection.ts b/x-pack/test/functional/apps/observability_logs_explorer/columns_selection.ts index b944ebd9b8306c..3c87d53031aa42 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/columns_selection.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/columns_selection.ts @@ -85,7 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('render content virtual column properly', async () => { it('should render log level and log message when present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('A sample log')).to.be(true); @@ -94,7 +94,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render log message when present and skip log level when missing', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(1, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(1, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(false); expect(cellValue.includes('A sample log')).to.be(true); @@ -103,7 +103,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render message from error object when top level message not present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(2, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(2, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('error.message')).to.be(true); @@ -113,7 +113,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render message from event.original when top level message and error.message not present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(3, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(3, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('event.original')).to.be(true); @@ -123,7 +123,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the whole JSON when neither message, error.message and event.original are present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(4, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(4, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); @@ -137,7 +137,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('on cell expansion with no message field should open JSON Viewer', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - await dataGrid.clickCellExpandButton(4, 4); + await dataGrid.clickCellExpandButtonExcludingControlColumns(4, 2); await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover'); }); }); @@ -145,7 +145,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('on cell expansion with message field should open regular popover', async () => { await navigateToLogsExplorer(); await retry.tryForTime(TEST_TIMEOUT, async () => { - await dataGrid.clickCellExpandButton(3, 4); + await dataGrid.clickCellExpandButtonExcludingControlColumns(3, 2); await testSubjects.existOrFail('euiDataGridExpansionPopover'); }); }); @@ -154,7 +154,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('render resource virtual column properly', async () => { it('should render service name and host name when present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 3); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 1); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('synth-service')).to.be(true); expect(cellValue.includes('synth-host')).to.be(true); @@ -168,7 +168,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should render a popover with cell actions when a chip on content column is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 2); const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-'); await logLevelChip.click(); // Check Filter In button is present @@ -182,7 +182,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where log.level value is info when filter in action is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 2); const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-'); const actionSelector = 'dataTableCellAction_addToFilterAction_log.level'; @@ -203,7 +203,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where log.level value is not info when filter out action is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 2); const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-'); const actionSelector = 'dataTableCellAction_removeFromFilterAction_log.level'; @@ -222,7 +222,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where service.name value is selected', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 3); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 1); const serviceNameChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_service.name' ); diff --git a/x-pack/test/functional/apps/observability_logs_explorer/custom_control_columns.ts b/x-pack/test/functional/apps/observability_logs_explorer/custom_control_columns.ts index 21dd8a6772bb7e..58b123d08cdaf4 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/custom_control_columns.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/custom_control_columns.ts @@ -47,7 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render control column with proper header', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { // First control column has no title, so empty string, leading control column has title - expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', 'actions']); + expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', '', '', '']); }); }); @@ -61,7 +61,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the degraded icon in the leading control column if degraded doc exists', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(1, 1); + const cellElement = await dataGrid.getCellElement(1, 2); const degradedButton = await cellElement.findByTestSubject('docTableDegradedDocExist'); expect(degradedButton).to.not.be.empty(); }); @@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the disabled degraded icon in the leading control column when degraded doc does not exists', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 1); + const cellElement = await dataGrid.getCellElement(0, 2); const degradedDisableButton = await cellElement.findByTestSubject( 'docTableDegradedDocDoesNotExist' ); @@ -79,7 +79,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the stacktrace icon in the leading control column when stacktrace exists', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(4, 1); + const cellElement = await dataGrid.getCellElement(4, 3); const stacktraceButton = await cellElement.findByTestSubject('docTableStacktraceExist'); expect(stacktraceButton).to.not.be.empty(); }); @@ -87,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the stacktrace icon disabled in the leading control column when stacktrace does not exists', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(1, 1); + const cellElement = await dataGrid.getCellElement(1, 3); const stacktraceButton = await cellElement.findByTestSubject( 'docTableStacktraceDoesNotExist' ); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_row_additional_leading_controls.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_row_additional_leading_controls.ts new file mode 100644 index 00000000000000..c91dae10bc4eab --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_row_additional_leading_controls.ts @@ -0,0 +1,66 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import kbnRison from '@kbn/rison'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'discover', 'svlCommonPage']); + const testSubjects = getService('testSubjects'); + const dataViews = getService('dataViews'); + + describe('extension getRowAdditionalLeadingControls', () => { + before(async () => { + await PageObjects.svlCommonPage.loginAsAdmin(); + }); + describe('ES|QL mode', () => { + it('should render logs controls for logs data source', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-logs | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.existOrFail('exampleLogsControl_visBarVerticalStacked'); + await testSubjects.existOrFail('unifiedDataTable_additionalRowControl_menuControl'); + }); + + it('should not render logs controls for non-logs data source', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-metrics | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.missingOrFail('exampleLogsControl_visBarVerticalStacked'); + await testSubjects.missingOrFail('unifiedDataTable_additionalRowControl_menuControl'); + }); + }); + + describe('data view mode', () => { + it('should render logs controls for logs data source', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.existOrFail('exampleLogsControl_visBarVerticalStacked'); + await testSubjects.existOrFail('unifiedDataTable_additionalRowControl_menuControl'); + }); + + it('should not render logs controls for non-logs data source', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-metrics'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await testSubjects.missingOrFail('exampleLogsControl_visBarVerticalStacked'); + await testSubjects.missingOrFail('unifiedDataTable_additionalRowControl_menuControl'); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts index e8c8f1234aab5e..d0e23c825870b6 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts @@ -38,6 +38,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./_root_profile')); loadTestFile(require.resolve('./_data_source_profile')); loadTestFile(require.resolve('./extensions/_get_row_indicator_provider')); + loadTestFile(require.resolve('./extensions/_get_row_additional_leading_controls')); loadTestFile(require.resolve('./extensions/_get_doc_viewer')); loadTestFile(require.resolve('./extensions/_get_cell_renderers')); loadTestFile(require.resolve('./extensions/_get_default_app_state')); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts index 80fff5ef760147..0d1e2b3de6a022 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts @@ -198,8 +198,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const cell = await dataGrid.getCellElementExcludingControlColumns(0, 1); expect(await cell.getVisibleText()).to.be(' - '); expect(await dataGrid.getHeaders()).to.eql([ - 'Control column', 'Select column', + 'Control column', 'Numberbytes', 'machine.ram_range', ]); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/columns_selection.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/columns_selection.ts index 8e7294d35f1b8b..6fcfea151db123 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/columns_selection.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/columns_selection.ts @@ -86,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('render content virtual column properly', async () => { it('should render log level and log message when present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('A sample log')).to.be(true); @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render log message when present and skip log level when missing', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(1, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(1, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(false); expect(cellValue.includes('A sample log')).to.be(true); @@ -104,7 +104,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render message from error object when top level message not present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(2, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(2, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('error.message')).to.be(true); @@ -114,7 +114,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render message from event.original when top level message and error.message not present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(3, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(3, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('event.original')).to.be(true); @@ -124,7 +124,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the whole JSON when neither message, error.message and event.original are present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(4, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(4, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); @@ -138,7 +138,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('on cell expansion with no message field should open JSON Viewer', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - await dataGrid.clickCellExpandButton(4, 4); + await dataGrid.clickCellExpandButtonExcludingControlColumns(4, 2); await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover'); }); }); @@ -146,7 +146,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('on cell expansion with message field should open regular popover', async () => { await navigateToLogsExplorer(); await retry.tryForTime(TEST_TIMEOUT, async () => { - await dataGrid.clickCellExpandButton(3, 4); + await dataGrid.clickCellExpandButtonExcludingControlColumns(3, 2); await testSubjects.existOrFail('euiDataGridExpansionPopover'); }); }); @@ -155,7 +155,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('render resource virtual column properly', async () => { it('should render service name and host name when present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 3); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 1); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('synth-service')).to.be(true); expect(cellValue.includes('synth-host')).to.be(true); @@ -169,7 +169,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should render a popover with cell actions when a chip on content column is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 2); const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-'); await logLevelChip.click(); // Check Filter In button is present @@ -183,7 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where log.level value is info when filter in action is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 2); const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-'); const actionSelector = 'dataTableCellAction_addToFilterAction_log.level'; @@ -204,7 +204,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where log.level value is not info when filter out action is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 2); const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-'); const actionSelector = 'dataTableCellAction_removeFromFilterAction_log.level'; @@ -223,7 +223,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where service.name value is selected', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 3); + const cellElement = await dataGrid.getCellElementExcludingControlColumns(0, 1); const serviceNameChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_service.name' ); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/custom_control_columns.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/custom_control_columns.ts index fea974fd0096ef..c1f4692f19fdf5 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/custom_control_columns.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/custom_control_columns.ts @@ -48,7 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render control column with proper header', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { // First control column has no title, so empty string, leading control column has title - expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', 'actions']); + expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', '', '', '']); }); }); @@ -60,27 +60,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('should render the malformed icon in the leading control column if malformed doc exists', async () => { + it('should render the degraded icon in the leading control column if degraded doc exists', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(1, 1); - const malformedButton = await cellElement.findByTestSubject('docTableDegradedDocExist'); - expect(malformedButton).to.not.be.empty(); + const cellElement = await dataGrid.getCellElement(1, 2); + const degradedButton = await cellElement.findByTestSubject('docTableDegradedDocExist'); + expect(degradedButton).to.not.be.empty(); }); }); - it('should render the disabled malformed icon in the leading control column when malformed doc does not exists', async () => { + it('should render the disabled degraded icon in the leading control column when degraded doc does not exists', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 1); - const malformedDisableButton = await cellElement.findByTestSubject( + const cellElement = await dataGrid.getCellElement(0, 2); + const degradedDisableButton = await cellElement.findByTestSubject( 'docTableDegradedDocDoesNotExist' ); - expect(malformedDisableButton).to.not.be.empty(); + expect(degradedDisableButton).to.not.be.empty(); }); }); it('should render the stacktrace icon in the leading control column when stacktrace exists', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(4, 1); + const cellElement = await dataGrid.getCellElement(4, 3); const stacktraceButton = await cellElement.findByTestSubject('docTableStacktraceExist'); expect(stacktraceButton).to.not.be.empty(); }); @@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the stacktrace icon disabled in the leading control column when stacktrace does not exists', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(1, 1); + const cellElement = await dataGrid.getCellElement(1, 3); const stacktraceButton = await cellElement.findByTestSubject( 'docTableStacktraceDoesNotExist' ); @@ -116,10 +116,7 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) { }) ); - const malformedDocs = timerange( - moment(to).subtract(2, 'second'), - moment(to).subtract(1, 'second') - ) + const degradedDocs = timerange(moment(to).subtract(2, 'second'), moment(to).subtract(1, 'second')) .interval('1m') .rate(1) .generator((timestamp) => @@ -128,7 +125,7 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) { .map(() => { return log .create() - .message('A malformed doc') + .message('A degraded doc') .logLevel(MORE_THAN_1024_CHARS) .timestamp(timestamp) .defaults({ 'service.name': 'synth-service' }); @@ -186,5 +183,5 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) { }) ); - return [logs, malformedDocs, logsWithErrorMessage, logsWithErrorException, logsWithErrorInLog]; + return [logs, degradedDocs, logsWithErrorMessage, logsWithErrorException, logsWithErrorInLog]; }