[OneDiscover][Extension] DataTable Row Actions (#188762)

- Closes https://github.com/elastic/kibana/issues/186637
- Closes https://github.com/elastic/kibana/issues/186808

## Summary

- [x] Extend UnifiedDataTable with a new `rowAdditionalLeadingControls`
prop to render additional leading controls
- [x] In case of many actions, collapse the rest of them under "More"
button
- [x] New OneDiscover extension
- [x] Convert from `customControlColumnsConfiguration` to the new prop.
Refactor actions format in Log Explorer.
- [x] Swap the default "select" and "expand" control columns ~if custom
row actions are specified~
- [x] Add to example OneDiscover profile
- [x] Add functional and units tests

<img width="858" alt="Screenshot 2024-07-26 at 16 00 17"
src="https://github.com/user-attachments/assets/68832abc-a498-4ec6-8333-79ad5f83855b">
<img width="1139" alt="Screenshot 2024-07-26 at 16 00 47"
src="https://github.com/user-attachments/assets/8ff99ac1-21b0-4687-b548-fbf3c5517808">

### Testing 

For testing the example profile:
- add `discover.experimental.enabledProfiles: ['example-root-profile',
'example-data-source-profile', 'example-document-profile']` to
kibana.dev.yml
- start kibana
- make sure to have an index with `my-example-logs` name or create an
alias to an existing index:
```
POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "kibana_sample_data_logs",
        "alias": "my-example-logs"
      }
    }
  ]
}
```
- create a data view for `my-example-logs` index


### Follow up for Security solution

The following items would require deprecation/refactoring in components
on Security Solution pages to have consistent UX (can result in 500+
lines of code changes):
- Convert from `externalControlColumns` to the new prop
`rowAdditionalLeadingControls`
- Convert from `trailingControlColumns` to a normal column.
`trailingControlColumns` is deprecated.
  - https://github.com/elastic/kibana/issues/189294
- Use `getRowIndicator` prop on UnifiedDataTable instead of
`border-left` style
  - https://github.com/elastic/kibana/issues/189295


### Checklist


- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Rechkunova 2024-08-07 15:51:52 +02:00 committed by GitHub
parent f985bd4a14
commit 1c4b5c7489
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 942 additions and 473 deletions

View file

@ -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<string,(props: EuiDataGridCellValueElementProps) => 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}

View file

@ -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 (
<Control
data-test-subj={`exampleRowControl-${iconType}`}
label={`Example ${iconType}`}
iconType={iconType}
onClick={() => {
alert(`Example "${iconType}" control clicked. Row index: ${rowProps.rowIndex}`);
}}
/>
);
},
})
);

View file

@ -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 {

View file

@ -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`,
]);
});
});

View file

@ -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)),
];
};

View file

@ -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';

View file

@ -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) => (
<Control label={`test-${rowProps.rowIndex}`} iconType="heart" onClick={mockClick} />
)),
};
const rowControlColumn = getRowControlColumn(props);
const RowControlColumn =
rowControlColumn.rowCellRender as React.FC<EuiDataGridCellValueElementProps>;
render(
<UnifiedDataTableContext.Provider value={contextMock}>
<RowControlColumn
rowIndex={1}
setCellProps={jest.fn()}
columnId={props.id}
colIndex={0}
isDetails={false}
isExpandable={false}
isExpanded={false}
/>
</UnifiedDataTableContext.Provider>
);
const button = screen.getByTestId('unifiedDataTable_rowControl_test_row_control');
expect(button).toBeInTheDocument();
button.click();
expect(mockClick).toHaveBeenCalledWith({ record: contextMock.rows[1], rowIndex: 1 });
});
});

View file

@ -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<RowControlProps> = useMemo(
() =>
({ 'data-test-subj': dataTestSubj, color, disabled, label, iconType, onClick }) => {
return (
<DataTableRowControl size={Size.normal}>
<EuiToolTip content={label} delay="long">
<EuiButtonIcon
data-test-subj={dataTestSubj ?? `unifiedDataTable_rowControl_${props.columnId}`}
disabled={disabled}
iconSize="s"
iconType={iconType}
color={color ?? 'text'}
aria-label={label}
onClick={() => {
onClick?.(rowProps);
}}
/>
</EuiToolTip>
</DataTableRowControl>
);
},
[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 ??
(() => (
<EuiScreenReaderOnly>
<span>{headerAriaLabel}</span>
</EuiScreenReaderOnly>
)),
rowCellRender: (props) => {
return <RowControlCell {...props} renderControl={renderControl} />;
},
};
};

View file

@ -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) => (
<Control label={`test-${rowProps.rowIndex}`} iconType="heart" onClick={mockClick} />
)),
};
const rowMenuControlColumn = getRowMenuControlColumn([
props,
mockRowAdditionalLeadingControls[0],
mockRowAdditionalLeadingControls[1],
]);
const RowMenuControlColumn =
rowMenuControlColumn.rowCellRender as React.FC<EuiDataGridCellValueElementProps>;
render(
<UnifiedDataTableContext.Provider value={contextMock}>
<RowMenuControlColumn
rowIndex={1}
setCellProps={jest.fn()}
columnId={props.id}
colIndex={0}
isDetails={false}
isExpandable={false}
isExpanded={false}
/>
</UnifiedDataTableContext.Provider>
);
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 });
});
});

View file

@ -0,0 +1,130 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import 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<boolean>(false);
const buttonLabel = i18n.translate('unifiedDataTable.grid.additionalRowActions', {
defaultMessage: 'Additional actions',
});
const getControlComponent: (id: string) => React.FC<RowControlProps> = useCallback(
(id) =>
({ 'data-test-subj': dataTestSubj, color, disabled, label, iconType, onClick }) => {
return (
<EuiContextMenuItem
data-test-subj={dataTestSubj ?? `unifiedDataTable_rowMenu_${id}`}
disabled={disabled}
icon={iconType}
color={color}
onClick={() => {
onClick?.(rowProps);
setIsMoreActionsPopoverOpen(false);
}}
>
{label}
</EuiContextMenuItem>
);
},
[rowProps, setIsMoreActionsPopoverOpen]
);
const popoverMenuItems = useMemo(
() =>
rowControlColumns.map((rowControlColumn) => {
const Control = getControlComponent(rowControlColumn.id);
return (
<Fragment key={rowControlColumn.id}>
{rowControlColumn.renderControl(Control, rowProps)}
</Fragment>
);
}),
[rowControlColumns, rowProps, getControlComponent]
);
return (
<EuiPopover
id={`rowMenuActionsPopover_${props.rowIndex}`}
button={
<DataTableRowControl size={Size.normal}>
<EuiToolTip content={buttonLabel} delay="long">
<EuiButtonIcon
data-test-subj={`unifiedDataTable_${props.columnId}`}
iconSize="s"
iconType="boxesVertical"
color="text"
aria-label={buttonLabel}
css={css`
.euiDataGridRowCell__content--defaultHeight & {
margin-top: 2px; // to align with other controls
}
`}
onClick={() => {
setIsMoreActionsPopoverOpen(!isMoreActionsPopoverOpen);
}}
/>
</EuiToolTip>
</DataTableRowControl>
}
isOpen={isMoreActionsPopoverOpen}
closePopover={() => setIsMoreActionsPopoverOpen(false)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel size="s" items={popoverMenuItems} />
</EuiPopover>
);
};
export const getRowMenuControlColumn = (
rowControlColumns: RowControlColumn[]
): EuiDataGridControlColumn => {
return {
id: 'additionalRowControl_menuControl',
width: DEFAULT_CONTROL_COLUMN_WIDTH,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>
{i18n.translate('unifiedDataTable.additionalActionsColumnHeader', {
defaultMessage: 'Additional actions column',
})}
</span>
</EuiScreenReaderOnly>
),
rowCellRender: (props) => {
return <RowMenuControlCell {...props} rowControlColumns={rowControlColumns} />;
},
};
};

View file

@ -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<ColorIndicatorCellParams> = ({
rowIndex,
setCellProps,
getRowIndicator,
}) => {
const ColorIndicatorCell: React.FC<ColorIndicatorCellParams> = ({ 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 (
<div
data-test-subj="unifiedDataTableRowColorIndicatorCell"

View file

@ -10,3 +10,5 @@ export {
getColorIndicatorControlColumn,
type ColorIndicatorControlColumnParams,
} from './color_indicator';
export { getAdditionalRowControlColumns } from './additional_row_control';

View file

@ -55,9 +55,13 @@
.euiDataGridRowCell--controlColumn .euiDataGridRowCell__content,
.euiDataGridRowCell.euiDataGridRowCell--controlColumn[data-gridcell-column-id='openDetails'],
.euiDataGridRowCell.euiDataGridRowCell--controlColumn[data-gridcell-column-id='select'] {
.euiDataGridRowCell.euiDataGridRowCell--controlColumn[data-gridcell-column-id='select'],
.euiDataGridRowCell.euiDataGridRowCell--controlColumn[data-gridcell-column-id^='additionalRowControl_'],
.euiDataGridHeaderCell.euiDataGridHeaderCell--controlColumn[data-gridcell-column-id^='additionalRowControl_'] {
padding-left: 0;
padding-right: 0;
border-left: 0;
border-right: 0;
}
.euiDataGridHeaderCell.euiDataGridHeaderCell--controlColumn[data-gridcell-column-id='select'] {
@ -137,6 +141,13 @@
.euiDataGridRowCell__content--defaultHeight & { // "Single line" row height setting
margin-top: 0;
}
&--size-normal {
display: inline-block;
width: $euiSizeL;
height: $euiSizeL;
overflow: hidden;
}
}
.unifiedDataTable__descriptionList {

View file

@ -26,6 +26,7 @@ import { buildDataTableRecord, getDocId } from '@kbn/discover-utils';
import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import {
mockRowAdditionalLeadingControls,
testLeadingControlColumn,
testTrailingControlColumns,
} from '../../__mocks__/external_control_columns';
@ -451,9 +452,8 @@ describe('UnifiedDataTable', () => {
});
});
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();

View file

@ -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);
}
if (rowAdditionalLeadingControls?.length) {
leadingColumns.push(...getAdditionalRowControlColumns(rowAdditionalLeadingControls));
}
return leadingColumns;
}, [canSetExpandedDoc, controlColumnIds, externalControlColumns, getRowIndicator]);
const controlColumnsConfig = customControlColumnsConfiguration?.({
controlColumns: getAllControlColumns(),
});
const customLeadingControlColumn =
controlColumnsConfig?.leadingControlColumns ?? leadingControlColumns;
const customTrailingControlColumn =
controlColumnsConfig?.trailingControlColumns ?? trailingControlColumns;
}, [
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}
/>

View file

@ -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: () => (
<EuiScreenReaderOnly>
<span>
@ -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];

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { 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 (
<EuiFlexGroup
responsive={false}
@ -63,12 +54,12 @@ export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
>
<EuiFlexItem grow={false}>
<EuiCheckbox
id={doc.id}
id={record.id}
aria-label={toggleDocumentSelectionLabel}
checked={isDocSelected(doc.id)}
data-test-subj={`dscGridSelectDoc-${doc.id}`}
checked={isDocSelected(record.id)}
data-test-subj={`dscGridSelectDoc-${record.id}`}
onChange={() => {
toggleDocSelection(doc.id);
toggleDocSelection(record.id);
}}
/>
</EuiFlexItem>

View file

@ -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<EuiToolTip>(null);
const [pressed, setPressed] = useState<boolean>(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 (
<DataTableRowControl>
<DataTableRowControl size={Size.normal}>
<EuiToolTip content={buttonLabel} delay="long" ref={toolTipRef}>
<EuiButtonIcon
id={rowIndex === 0 ? tourStep : undefined}
@ -69,7 +57,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
aria-label={buttonLabel}
data-test-subj={testSubj}
onClick={() => {
const nextHit = isCurrentRowExpanded ? undefined : current;
const nextHit = isCurrentRowExpanded ? undefined : record;
toolTipRef.current?.hideToolTip();
setPressed(Boolean(nextHit));
setExpanded?.(nextHit);

View file

@ -7,7 +7,16 @@
*/
import React from 'react';
import classnames from 'classnames';
export const DataTableRowControl = ({ children }: { children: React.ReactNode }) => {
return <span className="unifiedDataTable__rowControl">{children}</span>;
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 <span className={classes}>{children}</span>;
};

View file

@ -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;

View file

@ -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<EuiDataGridCellValueElementProps, 'rowIndex' | 'setCellProps'>): {
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]);
};

View file

@ -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<RowControlProps>;
export interface RowControlColumn {
id: string;
headerAriaLabel: string;
headerCellRender?: EuiDataGridControlColumn['headerCellRender'];
renderControl: (Control: RowControlComponent, props: RowControlRowProps) => ReactElement;
}

View file

@ -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();

View file

@ -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}
/>
</CellActionsProvider>

View file

@ -16,21 +16,33 @@ import { useProfileAccessor } from '../../context_awareness';
/**
* Customized version of the UnifiedDataTable
* @param props
* @constructor
*/
export const DiscoverGrid: React.FC<UnifiedDataTableProps> = (props) => {
export const DiscoverGrid: React.FC<UnifiedDataTableProps> = ({
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 (
<UnifiedDataTable
showColumnTokens
enableComparisonMode
renderCustomToolbar={renderCustomToolbar}
getRowIndicator={getRowIndicator}
rowAdditionalLeadingControls={rowAdditionalLeadingControls}
{...props}
/>
);

View file

@ -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 (
<Control
data-test-subj={`exampleLogsControl_${iconType}`}
label={`Example ${iconType}`}
iconType={iconType}
onClick={() => {
alert(`Example "${iconType}" control clicked. Row index: ${rowProps.rowIndex}`);
}}
/>
);
},
})
),
];
},
getDefaultAppState: () => () => ({
columns: [
{

View file

@ -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;
}

View file

@ -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'];
}

View file

@ -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');
});
});
});
}

View file

@ -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'));

View file

@ -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',
]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Before After
Before After

View file

@ -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);
}
/**

View file

@ -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 = (
<FormattedMessage
id="xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDoc"
defaultMessage="Access to degraded doc with {ignoredProperty} field"
values={{
ignoredProperty: (
<EuiCode language="json" transparentBackground>
_ignored
</EuiCode>
),
}}
/>
);
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(

View file

@ -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 (
<TooltipButton displayText={actionsLabelLowerCase} popoverTitle={actionsLabel}>
<div style={{ width: '230px' }}>
<EuiText size="s" css={spacingCSS}>
<p>{actionsHeaderTooltipParagraph}</p>
</EuiText>
<EuiFlexGroup
responsive={false}
alignItems="baseline"
justifyContent="flexStart"
gutterSize="s"
css={spacingCSS}
>
<EuiFlexItem grow={false}>
<EuiIcon type="expand" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<p>{actionsHeaderTooltipExpandAction}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
responsive={false}
alignItems="baseline"
justifyContent="flexStart"
gutterSize="s"
css={spacingCSS}
>
<EuiFlexItem grow={false}>
<EuiIcon type="indexClose" size="s" color="danger" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<p>{actionsHeaderTooltipDegradedAction}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
responsive={false}
alignItems="baseline"
justifyContent="flexStart"
gutterSize="s"
css={spacingCSS}
>
<EuiFlexItem grow={false}>
<EuiIcon type="apmTrace" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<p>{actionsHeaderTooltipStacktraceAction}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<div style={{ marginLeft: '15px' }}>
{[
constants.ERROR_STACK_TRACE,
constants.ERROR_EXCEPTION_STACKTRACE,
constants.ERROR_LOG_STACKTRACE,
].map((field) => (
<FieldWithToken field={field} key={field} />
))}
</div>
</div>
</TooltipButton>
);
};

View file

@ -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,
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 <DegradedDocs row={state.context.rows[rowIndex]} rowIndex={rowIndex} />;
}
return null;
};
const ConnectedStacktraceDocs = ({
rowIndex,
service,
}: {
rowIndex: number;
service: LogsExplorerControllerStateService;
}) => {
const [state] = useActor(service);
if (state.matches('initialized') && state.context.rows[rowIndex]) {
return <Stacktrace row={state.context.rows[rowIndex]} rowIndex={rowIndex} />;
}
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 ? (
<DataTableRowControl>
<EuiToolTip content={degradedDocButtonLabelWhenPresent} delay="long">
<EuiButtonIcon
id={`degradedDocExists_${rowIndex}`}
size="xs"
iconSize="s"
data-test-subj={'docTableDegradedDocExist'}
color={'danger'}
aria-label={degradedDocButtonLabelWhenPresent}
iconType={'indexClose'}
/>
</EuiToolTip>
</DataTableRowControl>
<Control
data-test-subj="docTableDegradedDocExist"
color="danger"
label={degradedDocButtonLabelWhenPresent}
iconType="indexClose"
onClick={undefined}
/>
) : (
<DataTableRowControl>
<EuiToolTip content={degradedDocButtonLabelWhenNotPresent} delay="long">
<EuiButtonIcon
id={`degradedDocExists_${rowIndex}`}
size="xs"
iconSize="s"
data-test-subj={'docTableDegradedDocDoesNotExist'}
color={'text'}
iconType={'pagesSelect'}
aria-label={degradedDocButtonLabelWhenNotPresent}
/>
</EuiToolTip>
</DataTableRowControl>
<Control
data-test-subj="docTableDegradedDocDoesNotExist"
color="text"
label={degradedDocButtonLabelWhenNotPresent}
iconType="indexClose"
onClick={undefined}
/>
);
};
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 (
<DataTableRowControl>
<EuiToolTip
content={hasValue ? stacktraceAvailableControlButton : stacktraceNotAvailableControlButton}
delay="long"
>
<EuiButtonIcon
id={`stacktrace_${rowIndex}`}
size="xs"
iconSize="s"
data-test-subj={hasValue ? 'docTableStacktraceExist' : 'docTableStacktraceDoesNotExist'}
color={'text'}
iconType={'apmTrace'}
aria-label={
hasValue ? stacktraceAvailableControlButton : stacktraceNotAvailableControlButton
}
disabled={!hasValue}
/>
</EuiToolTip>
</DataTableRowControl>
return hasValue ? (
<Control
data-test-subj="docTableStacktraceExist"
label={stacktraceAvailableControlButton}
iconType="apmTrace"
onClick={undefined}
/>
) : (
<Control
disabled
data-test-subj="docTableStacktraceDoesNotExist"
label={stacktraceNotAvailableControlButton}
iconType="apmTrace"
onClick={undefined}
/>
);
};
export const createCustomControlColumnsConfiguration =
(service: LogsExplorerControllerStateService) =>
({ controlColumns }: ControlColumnsProps) => {
const checkBoxColumn = controlColumns[SELECT_ROW];
const openDetails = controlColumns[OPEN_DETAILS];
const ExpandButton =
openDetails.rowCellRender as ComponentClass<EuiDataGridCellValueElementProps>;
const actionsColumn = {
id: 'actionsColumn',
width: constants.ACTIONS_COLUMN_WIDTH,
headerCellRender: ActionsColumnTooltip,
rowCellRender: ({ rowIndex, setCellProps, ...rest }: EuiDataGridCellValueElementProps) => {
return (
<span>
<ExpandButton rowIndex={rowIndex} setCellProps={setCellProps} {...rest} />
<ConnectedDegradedDocs rowIndex={rowIndex} service={service} />
<ConnectedStacktraceDocs rowIndex={rowIndex} service={service} />
</span>
);
export const getRowAdditionalControlColumns =
(): UnifiedDataTableProps['rowAdditionalLeadingControls'] => {
return [
{
id: 'connectedDegradedDocs',
headerAriaLabel: actionsHeaderAriaLabelDegradedAction,
renderControl: (Control, rowProps) => {
return <DegradedDocs Control={Control} rowProps={rowProps} />;
},
},
};
return {
leadingControlColumns: [checkBoxColumn, actionsColumn],
};
{
id: 'connectedStacktraceDocs',
headerAriaLabel: actionsHeaderAriaLabelStacktraceAction,
renderControl: (Control, rowProps) => {
return <Stacktrace Control={Control} rowProps={rowProps} />;
},
},
];
};

View file

@ -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()
),
});

View file

@ -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 :",

View file

@ -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": "次のようなドキュメントのソースに関する情報を提供するフィールド:",

View file

@ -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": "提供有关文档来源信息的字段,例如:",

View file

@ -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'
);

View file

@ -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'
);

View file

@ -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');
});
});
});
}

View file

@ -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'));

View file

@ -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',
]);

View file

@ -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'
);

View file

@ -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];
}