[Logs Explorer] Add actions column (#175872)

## Summary

Closes - https://github.com/elastic/kibana/issues/171728

## What this PR does ?

1. Add customisation point for handling Leading and Trailing columns via
a single customisation point.
Currently the `Data Grid` exposes 2 separate customisation point for
handling this -
  - `externalControlColumns` to add extension to `LeadingControlColumn`
- `trailingControlColumns` to add extension to `TrailingControlColumns`
  
But both of these extension point creates certain problems(Discover team
can help in here) which this new extension point solves.

1. There exists a React Bug (may be because the way we bundle Kibana,
not 100% sure) due to which when customControlColumns when passed to the
above props like `externalControlColumns` or `trailingControlColumns`,
even though they render inside the `<UnifiedDataTableContext.Provider
value={unifiedDataTableContextValue}>` provider, they actually cannot
access the context. For example when i tried to import the
`ExpandButton` from the table directly and pass as it is to
`trailingControlColumn` somehow 2 different context were getting created
and the ExpandButton present in the LeadingColumn was getting access to
context with data in it, where as the ExpandButton present in the
Trailing Column which was passed via prop was ending up in different
context and hence not receiving data.

2. Access to the `rows` data is very important. If context cannot be
accessed, then accessing the row data becomes trickier.

Hence this new Customisation Point solves the above problem by passing
in both the control columns with reference and data and the consumer can
then modify it accordingly.

2. Using the Customisation point described in point 1, 3 additional
columns were added to the newly added Marker Control column
  - Expand Action. (Moved from 1st to last)
  - Malformed Document (In future this will be linked to a Fix IT flow)
- Stacktraces available (In future this will be linked to the Flyout
Tab, 4th Tab which will be Stacktrace)
  
  
  ## Demo

<img width="2178" alt="image"
src="781eaaa3-d354-43ff-a570-aeee4dc6e80c">


## What's pending

- [x] Add E2E tests for the Control Column new icons
- [x] Check for Memoisation probability in customControlColumns

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Achyut Jhunjhunwala 2024-02-09 17:46:48 +01:00 committed by GitHub
parent 5d833360f6
commit fd8a9f07fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1119 additions and 95 deletions

View file

@ -36,6 +36,9 @@ export type LogDocument = Fields &
'cloud.availability_zone'?: string;
'cloud.project.id'?: string;
'cloud.instance.id'?: string;
'error.stack_trace'?: string;
'error.exception.stacktrace'?: string;
'error.log.stacktrace'?: string;
}>;
class Log extends Serializable<LogDocument> {

View file

@ -9,6 +9,9 @@ import { LogDocument, log, generateShortId, generateLongId } from '@kbn/apm-synt
import { Scenario } from '../cli/scenario';
import { withClient } from '../lib/utils/with_client';
const MORE_THAN_1024_CHARS =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?';
const scenario: Scenario<LogDocument> = async (runOptions) => {
return {
generate: ({ range, clients: { logsEsClient } }) => {
@ -111,6 +114,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.defaults({
'trace.id': generateShortId(),
'error.message': MESSAGE_LOG_LEVELS[index].message,
'error.exception.stacktrace': 'Error message in error.exception.stacktrace',
'agent.name': 'nodejs',
'orchestrator.cluster.name': CLUSTER[index].clusterName,
'orchestrator.cluster.id': CLUSTER[index].clusterId,
@ -143,6 +147,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.defaults({
'trace.id': generateShortId(),
'event.original': MESSAGE_LOG_LEVELS[index].message,
'error.log.stacktrace': 'Error message in error.log.stacktrace',
'agent.name': 'nodejs',
'orchestrator.cluster.name': CLUSTER[index].clusterName,
'orchestrator.cluster.id': CLUSTER[index].clusterId,
@ -186,6 +191,39 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
'cloud.project.id': generateShortId(),
'cloud.instance.id': generateShortId(),
'log.file.path': `/logs/${generateLongId()}/error.txt`,
'error.stack_trace': 'Error message in error.stack_trace',
})
.timestamp(timestamp);
});
});
const malformedDocs = range
.interval('1m')
.rate(1)
.generator((timestamp) => {
return Array(3)
.fill(0)
.map(() => {
const index = Math.floor(Math.random() * 3);
return log
.create()
.message(MESSAGE_LOG_LEVELS[index].message)
.logLevel(MORE_THAN_1024_CHARS)
.service(SERVICE_NAMES[index])
.defaults({
'trace.id': generateShortId(),
'agent.name': 'nodejs',
'orchestrator.cluster.name': CLUSTER[index].clusterName,
'orchestrator.cluster.id': CLUSTER[index].clusterId,
'orchestrator.namespace': CLUSTER[index].namespace,
'container.name': `${SERVICE_NAMES[index]}-${generateShortId()}`,
'orchestrator.resource.id': generateShortId(),
'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)],
'cloud.region': CLOUD_REGION[index],
'cloud.availability_zone': MORE_THAN_1024_CHARS,
'cloud.project.id': generateShortId(),
'cloud.instance.id': generateShortId(),
'log.file.path': `/logs/${generateLongId()}/error.txt`,
})
.timestamp(timestamp);
});
@ -199,6 +237,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
logsWithErrorMessage,
logsWithEventMessage,
logsWithNoMessage,
malformedDocs,
])
);
},

View file

@ -90,7 +90,7 @@ pageLoadAssetSize:
licensing: 29004
links: 44490
lists: 22900
logsExplorer: 50000
logsExplorer: 55000
logsShared: 281060
logstash: 53548
management: 46112

View file

@ -25,3 +25,5 @@ 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 { DataTableRowControl } from './src/components/data_table_row_control';

View file

@ -24,7 +24,10 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { servicesMock } from '../../__mocks__/services';
import { buildDataTableRecord, getDocId } from '@kbn/discover-utils';
import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types';
import { testLeadingControlColumn } from '../../__mocks__/external_control_columns';
import {
testLeadingControlColumn,
testTrailingControlColumns,
} from '../../__mocks__/external_control_columns';
const mockUseDataGridColumnsCellActions = jest.fn((prop: unknown) => []);
jest.mock('@kbn/cell-actions', () => ({
@ -418,6 +421,69 @@ describe('UnifiedDataTable', () => {
});
});
describe('customControlColumnsConfiguration', () => {
const customControlColumnsConfiguration = jest.fn();
it('should be able to customise the leading control column', async () => {
const component = await getComponent({
...getProps(),
expandedDoc: {
id: 'test',
raw: {
_index: 'test_i',
_id: 'test',
},
flattened: { test: jest.fn() },
},
setExpandedDoc: jest.fn(),
renderDocumentView: jest.fn(),
externalControlColumns: [testLeadingControlColumn],
customControlColumnsConfiguration: customControlColumnsConfiguration.mockImplementation(
() => {
return {
leadingControlColumns: [testLeadingControlColumn, testTrailingControlColumns[0]],
trailingControlColumns: [],
};
}
),
});
expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy();
expect(
findTestSubject(component, 'test-trailing-column-popover-button').exists()
).toBeTruthy();
});
it('should be able to customise the trailing control column', async () => {
const component = await getComponent({
...getProps(),
expandedDoc: {
id: 'test',
raw: {
_index: 'test_i',
_id: 'test',
},
flattened: { test: jest.fn() },
},
setExpandedDoc: jest.fn(),
renderDocumentView: jest.fn(),
externalControlColumns: [testLeadingControlColumn],
customControlColumnsConfiguration: customControlColumnsConfiguration.mockImplementation(
() => {
return {
leadingControlColumns: [],
trailingControlColumns: [testLeadingControlColumn, testTrailingControlColumns[0]],
};
}
),
});
expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy();
expect(
findTestSubject(component, 'test-trailing-column-popover-button').exists()
).toBeTruthy();
});
});
describe('externalControlColumns', () => {
it('should render external leading control columns', async () => {
const component = await getComponent({

View file

@ -52,12 +52,14 @@ import {
DataTableColumnTypes,
CustomCellRenderer,
CustomGridColumnsConfiguration,
CustomControlColumnConfiguration,
} 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,
@ -334,6 +336,10 @@ 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
*/
@ -412,6 +418,7 @@ export const UnifiedDataTable = ({
gridStyleOverride,
rowLineHeightOverride,
customGridColumnsConfiguration,
customControlColumnsConfiguration,
}: UnifiedDataTableProps) => {
const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } =
services;
@ -746,19 +753,31 @@ export const UnifiedDataTable = ({
onSort: onTableSort,
};
}
return { columns: sortingColumns, onSort: () => {} };
return {
columns: sortingColumns,
onSort: () => {},
};
}, [isSortEnabled, sortingColumns, isPlainRecord, inmemorySortingColumns, onTableSort]);
const canSetExpandedDoc = Boolean(setExpandedDoc && !!renderDocumentView);
const leadingControlColumns = useMemo(() => {
const leadingControlColumns: EuiDataGridControlColumn[] = useMemo(() => {
const internalControlColumns = getLeadControlColumns(canSetExpandedDoc).filter(({ id }) =>
controlColumnIds.includes(id)
);
return externalControlColumns
? [...internalControlColumns, ...externalControlColumns]
: internalControlColumns;
}, [canSetExpandedDoc, externalControlColumns, controlColumnIds]);
}, [canSetExpandedDoc, controlColumnIds, externalControlColumns]);
const controlColumnsConfig = customControlColumnsConfiguration?.({
controlColumns: getAllControlColumns(),
});
const customLeadingControlColumn =
controlColumnsConfig?.leadingControlColumns ?? leadingControlColumns;
const customTrailingControlColumn =
controlColumnsConfig?.trailingControlColumns ?? trailingControlColumns;
const additionalControls = useMemo(() => {
if (!externalAdditionalControls && !usedSelectedDocs.length) {
@ -907,7 +926,7 @@ export const UnifiedDataTable = ({
columns={euiGridColumns}
columnVisibility={columnsVisibility}
data-test-subj="docTable"
leadingControlColumns={leadingControlColumns}
leadingControlColumns={customLeadingControlColumn}
onColumnResize={onResize}
pagination={paginationObj}
renderCellValue={renderCellValue}
@ -921,7 +940,7 @@ export const UnifiedDataTable = ({
gridStyle={gridStyleOverride ?? GRID_STYLE}
renderCustomGridBody={renderCustomGridBody}
renderCustomToolbar={renderCustomToolbarFn}
trailingControlColumns={trailingControlColumns}
trailingControlColumns={customTrailingControlColumn}
/>
</div>
{loadingState !== DataLoadingState.loading &&

View file

@ -17,7 +17,7 @@ import type { DataView } 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 { CustomGridColumnsConfiguration, UnifiedDataTableSettings } from '../types';
import { ControlColumns, CustomGridColumnsConfiguration, UnifiedDataTableSettings } from '../types';
import type { ValueToStringConverter, DataTableColumnTypes } from '../types';
import { buildCellActions } from './default_cell_actions';
import { getSchemaByKbnType } from './data_table_schema';
@ -30,8 +30,11 @@ import { DataTableColumnHeader, DataTableTimeColumnHeader } from './data_table_c
const DataTableColumnHeaderMemoized = React.memo(DataTableColumnHeader);
const DataTableTimeColumnHeaderMemoized = React.memo(DataTableTimeColumnHeader);
export const OPEN_DETAILS = 'openDetails';
export const SELECT_ROW = 'select';
const openDetails = {
id: 'openDetails',
id: OPEN_DETAILS,
width: 26,
headerCellRender: () => (
<EuiScreenReaderOnly>
@ -46,7 +49,7 @@ const openDetails = {
};
const select = {
id: 'select',
id: SELECT_ROW,
width: 24,
rowCellRender: SelectButton,
headerCellRender: () => (
@ -60,6 +63,13 @@ const select = {
),
};
export function getAllControlColumns(): ControlColumns {
return {
[SELECT_ROW]: select,
[OPEN_DETAILS]: openDetails,
};
}
export function getLeadControlColumns(canSetExpandedDoc: boolean) {
if (!canSetExpandedDoc) {
return [select];

View file

@ -11,6 +11,7 @@ import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@el
import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
import { UnifiedDataTableContext } from '../table_context';
import { DataTableRowControl } from './data_table_row_control';
/**
* Button to expand a given row
@ -62,7 +63,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
}
return (
<div className="unifiedDataTable__rowControl">
<DataTableRowControl>
<EuiToolTip content={buttonLabel} delay="long" ref={toolTipRef}>
<EuiButtonIcon
id={rowIndex === 0 ? tourStep : undefined}
@ -81,6 +82,6 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
isSelected={isCurrentRowExpanded}
/>
</EuiToolTip>
</div>
</DataTableRowControl>
);
};

View file

@ -0,0 +1,13 @@
/*
* 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';
export const DataTableRowControl = ({ children }: { children: React.ReactNode }) => {
return <span className="unifiedDataTable__rowControl">{children}</span>;
};

View file

@ -11,6 +11,7 @@ import { EuiDataGridCellValueElementProps, type EuiDataGridColumn } from '@elast
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';
import { EuiDataGridControlColumn } from '@elastic/eui/src/components/datagrid/data_grid_types';
/**
* User configurable state of data grid, persisted in saved search
@ -55,3 +56,17 @@ export type CustomGridColumnsConfiguration = Record<
string,
(props: CustomGridColumnProps) => EuiDataGridColumn
>;
export interface ControlColumns {
select: EuiDataGridControlColumn;
openDetails: EuiDataGridControlColumn;
}
export interface ControlColumnsProps {
controlColumns: ControlColumns;
}
export type CustomControlColumnConfiguration = (props: ControlColumnsProps) => {
leadingControlColumns: EuiDataGridControlColumn[];
trailingControlColumns?: EuiDataGridControlColumn[];
};

View file

@ -120,10 +120,16 @@ describe('Discover documents layout', () => {
}),
};
const customControlColumnsConfiguration = () => ({
leadingControlColumns: [],
trailingControlColumns: [],
});
const customization: DiscoverCustomization = {
id: 'data_table',
customCellRenderer,
customGridColumnsConfiguration,
customControlColumnsConfiguration,
};
customisationService.set(customization);
@ -135,5 +141,8 @@ describe('Discover documents layout', () => {
expect(discoverGridComponent.prop('customGridColumnsConfiguration')).toEqual(
customGridColumnsConfiguration
);
expect(discoverGridComponent.prop('customControlColumnsConfiguration')).toEqual(
customControlColumnsConfiguration
);
});
});

View file

@ -255,9 +255,11 @@ function DiscoverDocumentsComponent({
[dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc]
);
const externalCustomRenderers = useDiscoverCustomization('data_table')?.customCellRenderer;
const customGridColumnsConfiguration =
useDiscoverCustomization('data_table')?.customGridColumnsConfiguration;
const {
customCellRenderer: externalCustomRenderers,
customGridColumnsConfiguration,
customControlColumnsConfiguration,
} = useDiscoverCustomization('data_table') || {};
const documents = useObservable(stateContainer.dataState.data$.documents$);
@ -427,6 +429,7 @@ function DiscoverDocumentsComponent({
headerRowHeight={3}
externalCustomRenderers={externalCustomRenderers}
customGridColumnsConfiguration={customGridColumnsConfiguration}
customControlColumnsConfiguration={customControlColumnsConfiguration}
/>
</CellActionsProvider>
</div>

View file

@ -6,10 +6,15 @@
* Side Public License, v 1.
*/
import { CustomCellRenderer, CustomGridColumnsConfiguration } from '@kbn/unified-data-table';
import {
CustomCellRenderer,
CustomControlColumnConfiguration,
CustomGridColumnsConfiguration,
} from '@kbn/unified-data-table';
export interface DataTableCustomization {
id: 'data_table';
customCellRenderer?: CustomCellRenderer;
customGridColumnsConfiguration?: CustomGridColumnsConfiguration;
customControlColumnsConfiguration?: CustomControlColumnConfiguration;
}

View file

@ -17,6 +17,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
export type { ISearchEmbeddable, SearchInput } from './embeddable';
export type { DiscoverAppState } from './application/main/services/discover_app_state_container';
export type { DiscoverStateContainer } from './application/main/services/discover_state';
export type { DataDocumentsMsg } from './application/main/services/discover_data_state_container';
export type { DiscoverContainerProps } from './components/discover_container';
export type {
CustomizationCallback,

View file

@ -15,9 +15,11 @@ export interface TabbedGridData {
columns: string[];
rows: string[][];
}
interface SelectOptions {
isAnchorRow?: boolean;
rowIndex?: number;
columnIndex?: number;
renderMoreRows?: boolean;
}
@ -242,13 +244,14 @@ export class DataGridService extends FtrService {
}
public async clickRowToggle(
options: SelectOptions = { isAnchorRow: false, rowIndex: 0 }
options: SelectOptions = { isAnchorRow: false, rowIndex: 0, columnIndex: 0 }
): Promise<void> {
const row = await this.getRow(options);
const rowColumns = await this.getRow(options);
const testSubj = options.isAnchorRow
? '~docTableExpandToggleColumnAnchor'
: '~docTableExpandToggleColumn';
const toggle = await row[0].findByTestSubject(testSubj);
const toggle = await rowColumns[options.columnIndex ?? 0].findByTestSubject(testSubj);
await toggle.scrollIntoViewIfNecessary();
await toggle.click();
@ -272,7 +275,20 @@ export class DataGridService extends FtrService {
const cellText = await cell.getVisibleText();
textArr.push(cellText.trim());
}
return Promise.resolve(textArr);
return textArr;
}
public async getControlColumnHeaderFields(): Promise<string[]> {
const result = await this.find.allByCssSelector(
'.euiDataGridHeaderCell--controlColumn > .euiDataGridHeaderCell__content'
);
const textArr = [];
for (const cell of result) {
const cellText = await cell.getVisibleText();
textArr.push(cellText.trim());
}
return textArr;
}
public async getRowActions(
@ -393,6 +409,7 @@ export class DataGridService extends FtrService {
const detailRows = await this.getDetailsRows();
return detailRows[0];
}
public async addInclusiveFilter(detailsRow: WebElementWrapper, fieldName: string): Promise<void> {
const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName);
const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow);

View file

@ -34,6 +34,14 @@ export const ORCHESTRATOR_NAMESPACE_FIELD = 'orchestrator.namespace';
export const CONTAINER_NAME_FIELD = 'container.name';
export const CONTAINER_ID_FIELD = 'container.id';
// Malformed Docs
export const MALFORMED_DOCS_FIELD = 'ignored_field_values';
// Error Stacktrace
export const ERROR_STACK_TRACE = 'error.stack_trace';
export const ERROR_EXCEPTION_STACKTRACE = 'error.exception.stacktrace';
export const ERROR_LOG_STACKTRACE = 'error.log.stacktrace';
// Virtual column fields
export const CONTENT_FIELD = 'content';
export const RESOURCE_FIELD = 'resource';
@ -41,7 +49,7 @@ export const RESOURCE_FIELD = 'resource';
// Sizing
export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;
export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
export const ACTIONS_COLUMN_WIDTH = 80;
// UI preferences
export const DEFAULT_COLUMNS = [
{

View file

@ -33,6 +33,10 @@ export interface LogDocument extends DataTableRecord {
'log.file.path'?: string;
'data_stream.namespace': string;
'data_stream.dataset': string;
'error.stack_trace'?: string;
'error.exception.stacktrace'?: string;
'error.log.stacktrace'?: string;
};
}
@ -74,3 +78,9 @@ export interface ResourceFields {
'container.id'?: string;
'cloud.instance.id'?: string;
}
export interface StackTraceFields {
'error.stack_trace'?: string;
'error.exception.stacktrace'?: string;
'error.log.stacktrace'?: string;
}

View file

@ -31,11 +31,13 @@ export function LogLevel({ level, dataTestSubj, renderInFlyout = false }: LogLev
? euiTheme.colors[LEVEL_DICT[level as keyof typeof LEVEL_DICT]]
: null;
const truncatedLogLevel = level.length > 10 ? level.substring(0, 10) + '...' : level;
if (renderInFlyout) {
return (
<ChipWithPopover
property={constants.LOG_LEVEL_FIELD}
text={level}
text={truncatedLogLevel}
borderColor={levelColor}
style={{ width: 'none' }}
dataTestSubj={dataTestSubj}
@ -50,7 +52,7 @@ export function LogLevel({ level, dataTestSubj, renderInFlyout = false }: LogLev
text={level}
rightSideIcon="arrowDown"
borderColor={levelColor}
style={{ width: '80px' }}
style={{ width: '80px', marginTop: '-3px' }}
/>
);
}

View file

@ -78,7 +78,7 @@ export function ChipWithPopover({
font-size: ${xsFontSize};
display: flex;
justify-content: center;
margin-top: -3px;
cursor: pointer;
`}
style={style}
>

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCode } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
export const flyoutContentLabel = i18n.translate('xpack.logsExplorer.flyoutDetail.label.message', {
defaultMessage: 'Content breakdown',
@ -22,6 +25,17 @@ 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 flyoutServiceLabel = i18n.translate('xpack.logsExplorer.flyoutDetail.label.service', {
defaultMessage: 'Service',
});
@ -208,17 +222,21 @@ export const closeCellActionPopoverText = i18n.translate(
}
);
export const contentHeaderTooltipParagraph1 = i18n.translate(
'xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1',
{
defaultMessage: "Fields that provide information on the document's source, such as:",
}
export const contentHeaderTooltipParagraph1 = (
<FormattedMessage
id="xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1"
defaultMessage="Displays the document's {logLevel} and {message} fields."
values={{
logLevel: <strong>log.level</strong>,
message: <strong>message</strong>,
}}
/>
);
export const contentHeaderTooltipParagraph2 = i18n.translate(
'xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2',
{
defaultMessage: 'When the message field is empty, one of the following is displayed',
defaultMessage: 'When the message field is empty, one of the following is displayed:',
}
);
@ -228,3 +246,63 @@ export const resourceHeaderTooltipParagraph = i18n.translate(
defaultMessage: "Fields that provide information on the document's source, such as:",
}
);
export const actionsHeaderTooltipParagraph = i18n.translate(
'xpack.logsExplorer.dataTable.header.actions.tooltip.paragraph',
{
defaultMessage: 'Fields that provide actionable information, such as:',
}
);
export const actionsHeaderTooltipExpandAction = i18n.translate(
'xpack.logsExplorer.dataTable.header.actions.tooltip.expand',
{ defaultMessage: 'Expand log details' }
);
export const actionsHeaderTooltipMalformedAction = (
<FormattedMessage
id="xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDoc"
defaultMessage="Access to malformed 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 malformedDocButtonLabelWhenPresent = i18n.translate(
'xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDocPresent',
{
defaultMessage:
"This document couldn't be parsed correctly. Not all fields are properly populated",
}
);
export const malformedDocButtonLabelWhenNotPresent = i18n.translate(
'xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDocNotPresent',
{
defaultMessage: 'All fields in this document were parsed correctly',
}
);
export const stacktraceAvailableControlButton = i18n.translate(
'xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available',
{
defaultMessage: 'Stacktraces available',
}
);
export const stacktraceNotAvailableControlButton = i18n.translate(
'xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable',
{
defaultMessage: 'Stacktraces not available',
}
);

View file

@ -0,0 +1,99 @@
/*
* 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,
actionsHeaderTooltipMalformedAction,
actionsHeaderTooltipParagraph,
actionsHeaderTooltipStacktraceAction,
actionsLabel,
actionsLabelLowerCase,
} from '../../common/translations';
import { HoverPopover } from '../../common/hover_popover';
import { TooltipButtonComponent } 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 (
<HoverPopover
button={<TooltipButtonComponent displayText={actionsLabelLowerCase} />}
title={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>{actionsHeaderTooltipMalformedAction}</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>
</HoverPopover>
);
};

View file

@ -42,7 +42,7 @@ export const ResourceColumnTooltip = ({ column, headerRowHeight }: CustomGridCol
constants.HOST_NAME_FIELD,
constants.CLOUD_INSTANCE_ID_FIELD,
].map((field) => (
<FieldWithToken field={field} />
<FieldWithToken field={field} key={field} />
))}
</div>
</HoverPopover>

View file

@ -0,0 +1,151 @@
/*
* 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, { 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 { LogsExplorerControllerStateService } from '../state_machines/logs_explorer_controller';
import {
malformedDocButtonLabelWhenNotPresent,
malformedDocButtonLabelWhenPresent,
stacktraceAvailableControlButton,
stacktraceNotAvailableControlButton,
} from '../components/common/translations';
import * as constants from '../../common/constants';
import { getStacktraceFields } from '../utils/get_stack_trace';
import { LogDocument } from '../../common/document';
import { ActionsColumnTooltip } from '../components/virtual_columns/column_tooltips/actions_column_tooltip';
const ConnectedMalformedDocs = ({
rowIndex,
service,
}: {
rowIndex: number;
service: LogsExplorerControllerStateService;
}) => {
const [state] = useActor(service);
if (state.matches('initialized') && state.context.rows) {
const row = state.context.rows[rowIndex];
return <MalformedDocs row={row} rowIndex={rowIndex} />;
}
return null;
};
const ConnectedStacktraceDocs = ({
rowIndex,
service,
}: {
rowIndex: number;
service: LogsExplorerControllerStateService;
}) => {
const [state] = useActor(service);
if (state.matches('initialized') && state.context.rows) {
const row = state.context.rows[rowIndex];
return <Stacktrace row={row} rowIndex={rowIndex} />;
}
return null;
};
const MalformedDocs = ({ row, rowIndex }: { row: DataTableRecord; rowIndex: number }) => {
const isMalformedDocumentExists = !!row.raw[constants.MALFORMED_DOCS_FIELD];
return isMalformedDocumentExists ? (
<DataTableRowControl>
<EuiToolTip content={malformedDocButtonLabelWhenPresent} delay="long">
<EuiButtonIcon
id={`malformedDocExists_${rowIndex}`}
size="xs"
iconSize="s"
data-test-subj={'docTableMalformedDocExist'}
color={'danger'}
aria-label={malformedDocButtonLabelWhenPresent}
iconType={'indexClose'}
/>
</EuiToolTip>
</DataTableRowControl>
) : (
<DataTableRowControl>
<EuiToolTip content={malformedDocButtonLabelWhenNotPresent} delay="long">
<EuiButtonIcon
id={`malformedDocExists_${rowIndex}`}
size="xs"
iconSize="s"
data-test-subj={'docTableMalformedDocDoesNotExist'}
color={'text'}
iconType={'pagesSelect'}
aria-label={malformedDocButtonLabelWhenNotPresent}
/>
</EuiToolTip>
</DataTableRowControl>
);
};
const Stacktrace = ({ row, rowIndex }: { row: DataTableRecord; rowIndex: number }) => {
const stacktrace = getStacktraceFields(row 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>
);
};
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} />
<ConnectedMalformedDocs rowIndex={rowIndex} service={service} />
<ConnectedStacktraceDocs rowIndex={rowIndex} service={service} />
</span>
);
},
};
return {
leadingControlColumns: [checkBoxColumn],
trailingControlColumns: [actionsColumn],
};
};

View file

@ -87,6 +87,9 @@ export const createLogsExplorerProfileCustomizations =
id: 'data_table',
customCellRenderer: createCustomCellRenderer({ data }),
customGridColumnsConfiguration: createCustomGridColumnsConfiguration(),
customControlColumnsConfiguration: await import('./custom_control_column').then((module) =>
module.createCustomControlColumnsConfiguration(service)
),
});
/**

View file

@ -40,4 +40,5 @@ export const DEFAULT_CONTEXT: DefaultLogsExplorerControllerState = {
from: 'now-15m/m',
to: 'now',
},
rows: [],
};

View file

@ -26,9 +26,9 @@ export const subscribeToDiscoverState =
throw new Error('Failed to subscribe to the Discover state: no state container in context.');
}
const { appState } = context.discoverStateContainer;
const { appState, dataState } = context.discoverStateContainer;
const subscription = appState.state$.subscribe({
const appStateSubscription = appState.state$.subscribe({
next: (newAppState) => {
if (isEmpty(newAppState)) {
return;
@ -41,8 +41,20 @@ export const subscribeToDiscoverState =
},
});
const dataStateSubscription = dataState.data$.documents$.subscribe({
next: (newDataState) => {
if (!isEmpty(newDataState?.result)) {
send({
type: 'RECEIVE_DISCOVER_DATA_STATE',
dataState: newDataState.result,
});
}
},
});
return () => {
subscription.unsubscribe();
appStateSubscription.unsubscribe();
dataStateSubscription.unsubscribe();
};
};
@ -71,6 +83,19 @@ export const updateContextFromDiscoverAppState = actions.assign<
return {};
});
export const updateContextFromDiscoverDataState = actions.assign<
LogsExplorerControllerContext,
LogsExplorerControllerEvent
>((context, event) => {
if ('dataState' in event && event.type === 'RECEIVE_DISCOVER_DATA_STATE') {
return {
rows: event.dataState,
};
}
return {};
});
export const updateDiscoverAppStateFromContext: ActionFunction<
LogsExplorerControllerContext,
LogsExplorerControllerEvent

View file

@ -25,6 +25,7 @@ import { createAndSetDataView } from './services/data_view_service';
import {
subscribeToDiscoverState,
updateContextFromDiscoverAppState,
updateContextFromDiscoverDataState,
updateDiscoverAppStateFromContext,
} from './services/discover_service';
import { validateSelection } from './services/selection_service';
@ -103,6 +104,7 @@ export const createPureLogsExplorerControllerStateMachine = (
id: 'timefilterService',
},
],
entry: ['resetRows'],
states: {
datasetSelection: {
initial: 'validatingSelection',
@ -196,6 +198,9 @@ export const createPureLogsExplorerControllerStateMachine = (
RECEIVE_DISCOVER_APP_STATE: {
actions: ['updateContextFromDiscoverAppState'],
},
RECEIVE_DISCOVER_DATA_STATE: {
actions: ['updateContextFromDiscoverDataState'],
},
RECEIVE_QUERY_STATE: {
actions: ['updateQueryStateFromQueryServiceState'],
},
@ -239,8 +244,12 @@ export const createPureLogsExplorerControllerStateMachine = (
}
: {}
),
resetRows: actions.assign((_context, event) => ({
rows: [],
})),
notifyDataViewUpdate: raise('DATA_VIEW_UPDATED'),
updateContextFromDiscoverAppState,
updateContextFromDiscoverDataState,
updateDiscoverAppStateFromContext,
updateContextFromTimefilter,
},

View file

@ -7,8 +7,13 @@
import { ControlGroupAPI } from '@kbn/controls-plugin/public';
import { QueryState, RefreshInterval, TimeRange } from '@kbn/data-plugin/common';
import { DiscoverAppState, DiscoverStateContainer } from '@kbn/discover-plugin/public';
import type {
DiscoverAppState,
DiscoverStateContainer,
DataDocumentsMsg,
} from '@kbn/discover-plugin/public';
import { DoneInvokeEvent } from 'xstate';
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
import { ControlPanels, DisplayOptions } from '../../../../common';
import type { DatasetEncodingError, DatasetSelection } from '../../../../common/dataset_selection';
@ -32,9 +37,14 @@ export interface WithDiscoverStateContainer {
discoverStateContainer: DiscoverStateContainer;
}
export interface WithDataTableRecord {
rows: DataTableRecord[];
}
export type DefaultLogsExplorerControllerState = WithDatasetSelection &
WithQueryState &
WithDisplayOptions;
WithDisplayOptions &
WithDataTableRecord;
export type LogsExplorerControllerTypeState =
| {
@ -59,6 +69,7 @@ export type LogsExplorerControllerTypeState =
WithControlPanels &
WithQueryState &
WithDisplayOptions &
WithDataTableRecord &
WithDiscoverStateContainer;
}
| {
@ -67,6 +78,7 @@ export type LogsExplorerControllerTypeState =
WithControlPanels &
WithQueryState &
WithDisplayOptions &
WithDataTableRecord &
WithDiscoverStateContainer;
}
| {
@ -75,6 +87,7 @@ export type LogsExplorerControllerTypeState =
WithControlPanels &
WithQueryState &
WithDisplayOptions &
WithDataTableRecord &
WithDiscoverStateContainer;
}
| {
@ -83,6 +96,7 @@ export type LogsExplorerControllerTypeState =
WithControlPanels &
WithQueryState &
WithDisplayOptions &
WithDataTableRecord &
WithDiscoverStateContainer;
}
| {
@ -91,6 +105,7 @@ export type LogsExplorerControllerTypeState =
WithControlPanels &
WithQueryState &
WithDisplayOptions &
WithDataTableRecord &
WithDiscoverStateContainer;
}
| {
@ -99,6 +114,7 @@ export type LogsExplorerControllerTypeState =
WithControlPanels &
WithQueryState &
WithDisplayOptions &
WithDataTableRecord &
WithDiscoverStateContainer;
}
| {
@ -108,6 +124,7 @@ export type LogsExplorerControllerTypeState =
WithControlPanels &
WithQueryState &
WithDisplayOptions &
WithDataTableRecord &
WithDiscoverStateContainer;
}
| {
@ -117,6 +134,7 @@ export type LogsExplorerControllerTypeState =
WithControlPanels &
WithQueryState &
WithDisplayOptions &
WithDataTableRecord &
WithDiscoverStateContainer;
};
@ -151,6 +169,10 @@ export type LogsExplorerControllerEvent =
type: 'RECEIVE_DISCOVER_APP_STATE';
appState: DiscoverAppState;
}
| {
type: 'RECEIVE_DISCOVER_DATA_STATE';
dataState: DataDocumentsMsg['result'];
}
| {
type: 'RECEIVE_TIMEFILTER_TIME';
time: TimeRange;

View file

@ -0,0 +1,15 @@
/*
* 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 { LogDocument } from '../../common/document';
type Field = keyof LogDocument['flattened'];
export const getFieldFromDoc = <T extends Field>(doc: LogDocument, field: T) => {
const fieldValueArray = doc.flattened[field];
return fieldValueArray && fieldValueArray.length ? fieldValueArray[0] : undefined;
};

View file

@ -0,0 +1,22 @@
/*
* 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 { LogDocument, StackTraceFields } from '../../common/document';
import * as constants from '../../common/constants';
import { getFieldFromDoc } from './get_field_from_flattened_doc';
export const getStacktraceFields = (doc: LogDocument): StackTraceFields => {
const errorStackTrace = getFieldFromDoc(doc, constants.ERROR_STACK_TRACE);
const errorExceptionStackTrace = getFieldFromDoc(doc, constants.ERROR_EXCEPTION_STACKTRACE);
const errorLogStackTrace = getFieldFromDoc(doc, constants.ERROR_LOG_STACKTRACE);
return {
[constants.ERROR_STACK_TRACE]: errorStackTrace,
[constants.ERROR_EXCEPTION_STACKTRACE]: errorExceptionStackTrace,
[constants.ERROR_LOG_STACKTRACE]: errorLogStackTrace,
};
};

View file

@ -7,13 +7,7 @@
import { LogDocument, ResourceFields } from '../../common/document';
import * as constants from '../../common/constants';
type Field = keyof LogDocument['flattened'];
const getFieldFromDoc = <T extends Field>(doc: LogDocument, field: T) => {
const fieldValueArray = doc.flattened[field];
return fieldValueArray && fieldValueArray.length ? fieldValueArray[0] : undefined;
};
import { getFieldFromDoc } from './get_field_from_flattened_doc';
export const getUnformattedResourceFields = (doc: LogDocument): ResourceFields => {
const serviceName = getFieldFromDoc(doc, constants.SERVICE_NAME_FIELD);

View file

@ -77,7 +77,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.getCellElement(0, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('A sample log')).to.be(true);
@ -86,7 +86,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.getCellElement(1, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(false);
expect(cellValue.includes('A sample log')).to.be(true);
@ -95,7 +95,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.getCellElement(2, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('error.message')).to.be(true);
@ -105,7 +105,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.getCellElement(3, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('event.original')).to.be(true);
@ -115,7 +115,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.getCellElement(4, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
@ -131,7 +131,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.clickCellExpandButton(4, 3);
await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover');
});
});
@ -139,7 +139,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.clickCellExpandButton(3, 3);
await testSubjects.existOrFail('euiDataGridExpansionPopover');
});
});
@ -148,7 +148,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.getCellElement(0, 2);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('synth-service')).to.be(true);
expect(cellValue.includes('synth-host')).to.be(true);
@ -162,7 +162,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.getCellElement(0, 3);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -178,7 +178,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.getCellElement(0, 3);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -198,7 +198,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.getCellElement(0, 3);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -216,7 +216,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.getCellElement(0, 2);
const serviceNameChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_service.name'
);

View file

@ -0,0 +1,189 @@
/*
* 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 { log, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import moment from 'moment';
import { FtrProviderContext } from './config';
const MORE_THAN_1024_CHARS =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const PageObjects = getPageObjects(['discover', 'observabilityLogsExplorer']);
const synthtrace = getService('logSynthtraceEsClient');
const dataGrid = getService('dataGrid');
const from = '2024-02-06T10:24:14.035Z';
const to = '2024-02-06T10:25:14.091Z';
const TEST_TIMEOUT = 10 * 1000; // 10 secs
const navigateToLogsExplorer = () =>
PageObjects.observabilityLogsExplorer.navigateTo({
pageState: {
time: {
from,
to,
mode: 'absolute',
},
},
});
describe('When the logs explorer loads', () => {
before(async () => {
await synthtrace.index(generateLogsData({ to }));
await navigateToLogsExplorer();
});
after(async () => {
await synthtrace.clean();
});
describe('should render custom control columns properly', async () => {
it('should render control column with proper header', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
// First control column has no title, so empty string, last control column has title
expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', 'actions']);
});
});
it('should render the expand icon in the last control column', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const expandButton = await cellElement.findByTestSubject('docTableExpandToggleColumn');
expect(expandButton).to.not.be.empty();
});
});
it('should render the malformed icon in the last control column if malformed doc exists', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(1, 4);
const malformedButton = await cellElement.findByTestSubject('docTableMalformedDocExist');
expect(malformedButton).to.not.be.empty();
});
});
it('should render the disabled malformed icon in the last control column when malformed doc does not exists', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const malformedDisableButton = await cellElement.findByTestSubject(
'docTableMalformedDocDoesNotExist'
);
expect(malformedDisableButton).to.not.be.empty();
});
});
it('should render the stacktrace icon in the last control column when stacktrace exists', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(4, 4);
const stacktraceButton = await cellElement.findByTestSubject('docTableStacktraceExist');
expect(stacktraceButton).to.not.be.empty();
});
});
it('should render the stacktrace icon disabled in the last control column when stacktrace does not exists', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(1, 4);
const stacktraceButton = await cellElement.findByTestSubject(
'docTableStacktraceDoesNotExist'
);
expect(stacktraceButton).to.not.be.empty();
});
});
});
});
}
function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
const logs = timerange(moment(to).subtract(1, 'second'), moment(to))
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log
.create()
.message('A sample log')
.logLevel('info')
.timestamp(timestamp)
.defaults({ 'service.name': 'synth-service' });
})
);
const malformedDocs = timerange(
moment(to).subtract(2, 'second'),
moment(to).subtract(1, 'second')
)
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log
.create()
.message('A malformed doc')
.logLevel(MORE_THAN_1024_CHARS)
.timestamp(timestamp)
.defaults({ 'service.name': 'synth-service' });
})
);
const logsWithErrorMessage = timerange(
moment(to).subtract(3, 'second'),
moment(to).subtract(2, 'second')
)
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log.create().logLevel('info').timestamp(timestamp).defaults({
'error.stack_trace': 'Error message in error.stack_trace',
'service.name': 'node-service',
});
})
);
const logsWithErrorException = timerange(
moment(to).subtract(4, 'second'),
moment(to).subtract(3, 'second')
)
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log.create().logLevel('info').timestamp(timestamp).defaults({
'error.exception.stacktrace': 'Error message in error.exception.stacktrace',
'service.name': 'node-service',
});
})
);
const logsWithErrorInLog = timerange(
moment(to).subtract(5, 'second'),
moment(to).subtract(4, 'second')
)
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log.create().logLevel('info').timestamp(timestamp).defaults({
'error.log.stacktrace': 'Error message in error.log.stacktrace',
'service.name': 'node-service',
});
})
);
return [logs, malformedDocs, logsWithErrorMessage, logsWithErrorException, logsWithErrorInLog];
}

View file

@ -66,30 +66,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should mount the flyout customization content', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutDetail');
});
it('should display a timestamp badge', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutLogTimestamp');
});
it('should display a log level badge when available', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutLogLevel');
await dataGrid.closeFlyout();
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.missingOrFail('logsExplorerFlyoutLogLevel');
});
it('should display a message code block when available', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutLogMessage');
await dataGrid.closeFlyout();
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.missingOrFail('logsExplorerFlyoutLogMessage');
});
});

View file

@ -87,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the service container with all fields', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
await testSubjects.existOrFail('logsExplorerFlyoutService');
await testSubjects.existOrFail('logsExplorerFlyoutTrace');
@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the service container even when 1 field is missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
await testSubjects.missingOrFail('logsExplorerFlyoutService');
await testSubjects.existOrFail('logsExplorerFlyoutTrace');
@ -109,7 +109,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should not load the service container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 2 });
await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 });
await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
await dataGrid.closeFlyout();
});
@ -155,7 +155,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the cloud container with all fields', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud');
await testSubjects.existOrFail('logsExplorerFlyoutCloudProvider');
await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion');
@ -166,7 +166,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the cloud container even when some fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud');
await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider');
@ -179,7 +179,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should not load the cloud container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 2 });
await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 });
await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionCloud');
await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider');
await testSubjects.missingOrFail('logsExplorerFlyoutCloudRegion');
@ -225,7 +225,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the other container with all fields', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther');
await testSubjects.existOrFail('logsExplorerFlyoutLogPathFile');
await testSubjects.existOrFail('logsExplorerFlyoutNamespace');
@ -235,7 +235,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the other container even when some fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther');
await testSubjects.missingOrFail('logsExplorerFlyoutLogPathFile');

View file

@ -17,5 +17,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./flyout'));
loadTestFile(require.resolve('./header_menu'));
loadTestFile(require.resolve('./flyout_highlights.ts'));
loadTestFile(require.resolve('./custom_control_columns.ts'));
});
}

View file

@ -79,7 +79,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.getCellElement(0, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('A sample log')).to.be(true);
@ -88,7 +88,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.getCellElement(1, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(false);
expect(cellValue.includes('A sample log')).to.be(true);
@ -97,7 +97,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.getCellElement(2, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('error.message')).to.be(true);
@ -107,7 +107,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.getCellElement(3, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('event.original')).to.be(true);
@ -117,7 +117,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.getCellElement(4, 3);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
@ -133,7 +133,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.clickCellExpandButton(4, 3);
await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover');
});
});
@ -141,7 +141,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.clickCellExpandButton(3, 3);
await testSubjects.existOrFail('euiDataGridExpansionPopover');
});
});
@ -150,7 +150,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.getCellElement(0, 2);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('synth-service')).to.be(true);
expect(cellValue.includes('synth-host')).to.be(true);
@ -164,7 +164,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.getCellElement(0, 3);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -180,7 +180,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.getCellElement(0, 3);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -200,7 +200,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.getCellElement(0, 3);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -218,7 +218,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.getCellElement(0, 2);
const serviceNameChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_service.name'
);

View file

@ -0,0 +1,191 @@
/*
* 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 { log, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import moment from 'moment';
import { FtrProviderContext } from '../../../ftr_provider_context';
const MORE_THAN_1024_CHARS =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const PageObjects = getPageObjects(['discover', 'observabilityLogsExplorer', 'svlCommonPage']);
const synthtrace = getService('svlLogsSynthtraceClient');
const dataGrid = getService('dataGrid');
const from = '2024-02-06T10:24:14.035Z';
const to = '2024-02-06T10:25:14.091Z';
const TEST_TIMEOUT = 10 * 1000; // 10 secs
const navigateToLogsExplorer = () =>
PageObjects.observabilityLogsExplorer.navigateTo({
pageState: {
time: {
from,
to,
mode: 'absolute',
},
},
});
describe('When the logs explorer loads', () => {
before(async () => {
await synthtrace.index(generateLogsData({ to }));
await PageObjects.svlCommonPage.login();
await navigateToLogsExplorer();
});
after(async () => {
await synthtrace.clean();
await PageObjects.svlCommonPage.forceLogout();
});
describe('should render custom control columns properly', async () => {
it('should render control column with proper header', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
// First control column has no title, so empty string, last control column has title
expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', 'actions']);
});
});
it('should render the expand icon in the last control column', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const expandButton = await cellElement.findByTestSubject('docTableExpandToggleColumn');
expect(expandButton).to.not.be.empty();
});
});
it('should render the malformed icon in the last control column if malformed doc exists', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(1, 4);
const malformedButton = await cellElement.findByTestSubject('docTableMalformedDocExist');
expect(malformedButton).to.not.be.empty();
});
});
it('should render the disabled malformed icon in the last control column when malformed doc does not exists', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const malformedDisableButton = await cellElement.findByTestSubject(
'docTableMalformedDocDoesNotExist'
);
expect(malformedDisableButton).to.not.be.empty();
});
});
it('should render the stacktrace icon in the last control column when stacktrace exists', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(4, 4);
const stacktraceButton = await cellElement.findByTestSubject('docTableStacktraceExist');
expect(stacktraceButton).to.not.be.empty();
});
});
it('should render the stacktrace icon disabled in the last control column when stacktrace does not exists', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(1, 4);
const stacktraceButton = await cellElement.findByTestSubject(
'docTableStacktraceDoesNotExist'
);
expect(stacktraceButton).to.not.be.empty();
});
});
});
});
}
function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
const logs = timerange(moment(to).subtract(1, 'second'), moment(to))
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log
.create()
.message('A sample log')
.logLevel('info')
.timestamp(timestamp)
.defaults({ 'service.name': 'synth-service' });
})
);
const malformedDocs = timerange(
moment(to).subtract(2, 'second'),
moment(to).subtract(1, 'second')
)
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log
.create()
.message('A malformed doc')
.logLevel(MORE_THAN_1024_CHARS)
.timestamp(timestamp)
.defaults({ 'service.name': 'synth-service' });
})
);
const logsWithErrorMessage = timerange(
moment(to).subtract(3, 'second'),
moment(to).subtract(2, 'second')
)
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log.create().logLevel('info').timestamp(timestamp).defaults({
'error.stack_trace': 'Error message in error.stack_trace',
'service.name': 'node-service',
});
})
);
const logsWithErrorException = timerange(
moment(to).subtract(4, 'second'),
moment(to).subtract(3, 'second')
)
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log.create().logLevel('info').timestamp(timestamp).defaults({
'error.exception.stacktrace': 'Error message in error.exception.stacktrace',
'service.name': 'node-service',
});
})
);
const logsWithErrorInLog = timerange(
moment(to).subtract(5, 'second'),
moment(to).subtract(4, 'second')
)
.interval('1m')
.rate(1)
.generator((timestamp) =>
Array(count)
.fill(0)
.map(() => {
return log.create().logLevel('info').timestamp(timestamp).defaults({
'error.log.stacktrace': 'Error message in error.log.stacktrace',
'service.name': 'node-service',
});
})
);
return [logs, malformedDocs, logsWithErrorMessage, logsWithErrorException, logsWithErrorInLog];
}

View file

@ -68,30 +68,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should mount the flyout customization content', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutDetail');
});
it('should display a timestamp badge', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutLogTimestamp');
});
it('should display a log level badge when available', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutLogLevel');
await dataGrid.closeFlyout();
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.missingOrFail('logsExplorerFlyoutLogLevel');
});
it('should display a message code block when available', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutLogMessage');
await dataGrid.closeFlyout();
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.missingOrFail('logsExplorerFlyoutLogMessage');
});
});

View file

@ -89,7 +89,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the service container with all fields', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
await testSubjects.existOrFail('logsExplorerFlyoutService');
await testSubjects.existOrFail('logsExplorerFlyoutTrace');
@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the service container even when 1 field is missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
await testSubjects.missingOrFail('logsExplorerFlyoutService');
await testSubjects.existOrFail('logsExplorerFlyoutTrace');
@ -111,7 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should not load the service container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 2 });
await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 });
await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionServiceInfra');
await dataGrid.closeFlyout();
});
@ -159,7 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the cloud container with all fields', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud');
await testSubjects.existOrFail('logsExplorerFlyoutCloudProvider');
await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion');
@ -170,7 +170,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the cloud container even when some fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud');
await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider');
@ -183,7 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should not load the cloud container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 2 });
await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 });
await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionCloud');
await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider');
await testSubjects.missingOrFail('logsExplorerFlyoutCloudRegion');
@ -231,7 +231,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the other container with all fields', async () => {
await dataGrid.clickRowToggle();
await dataGrid.clickRowToggle({ columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther');
await testSubjects.existOrFail('logsExplorerFlyoutLogPathFile');
await testSubjects.existOrFail('logsExplorerFlyoutNamespace');
@ -241,7 +241,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should load the other container even when some fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 });
await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther');
await testSubjects.missingOrFail('logsExplorerFlyoutLogPathFile');

View file

@ -17,5 +17,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./flyout'));
loadTestFile(require.resolve('./header_menu'));
loadTestFile(require.resolve('./flyout_highlights.ts'));
loadTestFile(require.resolve('./custom_control_columns.ts'));
});
}