mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
f985bd4a14
commit
1c4b5c7489
47 changed files with 942 additions and 473 deletions
|
@ -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}
|
||||
|
|
|
@ -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}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`,
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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)),
|
||||
];
|
||||
};
|
|
@ -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';
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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} />;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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} />;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -10,3 +10,5 @@ export {
|
|||
getColorIndicatorControlColumn,
|
||||
type ColorIndicatorControlColumnParams,
|
||||
} from './color_indicator';
|
||||
|
||||
export { getAdditionalRowControlColumns } from './additional_row_control';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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]);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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 |
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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()
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
@ -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 :",
|
||||
|
|
|
@ -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": "次のようなドキュメントのソースに関する情報を提供するフィールド:",
|
||||
|
|
|
@ -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": "提供有关文档来源信息的字段,例如:",
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue