[Logs Explorer] Add resource column with tooltip (#175287)

## Summary

Closes https://github.com/elastic/kibana/issues/171731

This PR does the following things -

- Add a new virtual column `Resource`
- Add custom tooltip for he resource column
- Refactor TS types for the Log Document and place them in proper files
and folders

### Demo

![Resource
Column](ee68604e-2085-4f37-bd6b-33a60fe0ebbb)

### Why is this PR still a WIP

- [x] Fix tests
- [x] Add more tests

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Achyut Jhunjhunwala 2024-01-26 10:36:36 +01:00 committed by GitHub
parent beff74e19e
commit 2c65c43ace
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 564 additions and 201 deletions

View file

@ -29,6 +29,8 @@ export type LogDocument = Fields &
'orchestrator.cluster.name'?: string;
'orchestrator.cluster.id'?: string;
'orchestrator.resource.id'?: string;
'orchestrator.namespace'?: string;
'container.name'?: string;
'cloud.provider'?: string;
'cloud.region'?: string;
'cloud.availability_zone'?: string;

View file

@ -24,9 +24,9 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
const CLOUD_REGION = ['eu-central-1', 'us-east-1', 'area-51'];
const CLUSTER = [
{ clusterId: generateShortId(), clusterName: 'synth-cluster-1' },
{ clusterId: generateShortId(), clusterName: 'synth-cluster-2' },
{ clusterId: generateShortId(), clusterName: 'synth-cluster-3' },
{ clusterId: generateShortId(), clusterName: 'synth-cluster-1', namespace: 'default' },
{ clusterId: generateShortId(), clusterName: 'synth-cluster-2', namespace: 'production' },
{ clusterId: generateShortId(), clusterName: 'synth-cluster-3', namespace: 'kube' },
];
const SERVICE_NAMES = Array(3)
@ -48,9 +48,11 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.service(SERVICE_NAMES[index])
.defaults({
'trace.id': generateShortId(),
'agent.name': 'synth-agent',
'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],
@ -77,10 +79,12 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.defaults({
'trace.id': generateShortId(),
'error.message': MESSAGE_LOG_LEVELS[index].message,
'agent.name': 'synth-agent',
'agent.name': 'nodejs',
'orchestrator.cluster.name': CLUSTER[index].clusterName,
'orchestrator.cluster.id': CLUSTER[index].clusterId,
'orchestrator.resource.id': generateShortId(),
'orchestrator.namespace': CLUSTER[index].namespace,
'container.name': `${SERVICE_NAMES[index]}-${generateShortId()}`,
'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)],
'cloud.region': CLOUD_REGION[index],
'cloud.availability_zone': `${CLOUD_REGION[index]}a`,
@ -107,10 +111,12 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.defaults({
'trace.id': generateShortId(),
'error.message': MESSAGE_LOG_LEVELS[index].message,
'agent.name': 'synth-agent',
'agent.name': 'nodejs',
'orchestrator.cluster.name': CLUSTER[index].clusterName,
'orchestrator.cluster.id': CLUSTER[index].clusterId,
'orchestrator.resource.id': generateShortId(),
'orchestrator.namespace': CLUSTER[index].namespace,
'container.name': `${SERVICE_NAMES[index]}-${generateShortId()}`,
'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)],
'cloud.region': CLOUD_REGION[index],
'cloud.availability_zone': `${CLOUD_REGION[index]}a`,
@ -137,10 +143,12 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.defaults({
'trace.id': generateShortId(),
'event.original': MESSAGE_LOG_LEVELS[index].message,
'agent.name': 'synth-agent',
'agent.name': 'nodejs',
'orchestrator.cluster.name': CLUSTER[index].clusterName,
'orchestrator.cluster.id': CLUSTER[index].clusterId,
'orchestrator.resource.id': generateShortId(),
'orchestrator.namespace': CLUSTER[index].namespace,
'container.name': `${SERVICE_NAMES[index]}-${generateShortId()}`,
'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)],
'cloud.region': CLOUD_REGION[index],
'cloud.availability_zone': `${CLOUD_REGION[index]}a`,
@ -166,10 +174,12 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.service(SERVICE_NAMES[index])
.defaults({
'trace.id': generateShortId(),
'agent.name': 'synth-agent',
'agent.name': 'nodejs',
'orchestrator.cluster.name': CLUSTER[index].clusterName,
'orchestrator.cluster.id': CLUSTER[index].clusterId,
'orchestrator.resource.id': generateShortId(),
'orchestrator.namespace': CLUSTER[index].namespace,
'container.name': `${SERVICE_NAMES[index]}-${generateShortId()}`,
'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)],
'cloud.region': CLOUD_REGION[index],
'cloud.availability_zone': `${CLOUD_REGION[index]}a`,

View file

@ -21,3 +21,6 @@ export function AgentIcon({ agentName, size = 'l', ...props }: AgentIconProps) {
return <EuiIcon type={icon} size={size} title={agentName} {...props} />;
}
// eslint-disable-next-line import/no-default-export
export default AgentIcon;

View file

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

View file

@ -15,7 +15,7 @@ const ColumnHeaderTruncateContainer = ({
headerRowHeight = 1,
children,
}: {
headerRowHeight: number;
headerRowHeight?: number;
children: React.ReactNode;
}) => {
return (

View file

@ -14,23 +14,29 @@ export const LOG_LEVEL_FIELD = 'log.level';
export const MESSAGE_FIELD = 'message';
export const ERROR_MESSAGE_FIELD = 'error.message';
export const EVENT_ORIGINAL_FIELD = 'event.original';
export const SERVICE_NAME_FIELD = 'service.name';
export const TRACE_ID_FIELD = 'trace.id';
export const LOG_FILE_PATH_FIELD = 'log.file.path';
export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace';
export const DATASTREAM_DATASET_FIELD = 'data_stream.dataset';
// Resource Fields
export const AGENT_NAME_FIELD = 'agent.name';
export const ORCHESTRATOR_CLUSTER_NAME_FIELD = 'orchestrator.cluster.name';
export const ORCHESTRATOR_RESOURCE_ID_FIELD = 'orchestrator.resource.id';
export const CLOUD_PROVIDER_FIELD = 'cloud.provider';
export const CLOUD_REGION_FIELD = 'cloud.region';
export const CLOUD_AVAILABILITY_ZONE_FIELD = 'cloud.availability_zone';
export const CLOUD_PROJECT_ID_FIELD = 'cloud.project.id';
export const CLOUD_INSTANCE_ID_FIELD = 'cloud.instance.id';
export const LOG_FILE_PATH_FIELD = 'log.file.path';
export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace';
export const DATASTREAM_DATASET_FIELD = 'data_stream.dataset';
export const SERVICE_NAME_FIELD = 'service.name';
export const ORCHESTRATOR_CLUSTER_NAME_FIELD = 'orchestrator.cluster.name';
export const ORCHESTRATOR_RESOURCE_ID_FIELD = 'orchestrator.resource.id';
export const ORCHESTRATOR_NAMESPACE_FIELD = 'orchestrator.namespace';
export const CONTAINER_NAME_FIELD = 'container.name';
export const CONTAINER_ID_FIELD = 'container.id';
// Virtual column fields
export const CONTENT_FIELD = 'content';
export const RESOURCE_FIELD = 'resource';
// Sizing
export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;
@ -39,11 +45,7 @@ export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
// UI preferences
export const DEFAULT_COLUMNS = [
{
field: SERVICE_NAME_FIELD,
width: DATA_GRID_COLUMN_WIDTH_SMALL,
},
{
field: HOST_NAME_FIELD,
field: RESOURCE_FIELD,
width: DATA_GRID_COLUMN_WIDTH_MEDIUM,
},
{

View file

@ -5,24 +5,7 @@
* 2.0.
*/
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
export interface LogExplorerCustomizations {
flyout?: {
renderContent?: RenderContentCustomization<LogExplorerFlyoutContentProps>;
};
}
export interface LogExplorerFlyoutContentProps {
actions: {
addFilter: DocViewRenderProps['filter'];
addColumn: DocViewRenderProps['onAddColumn'];
removeColumn: DocViewRenderProps['onRemoveColumn'];
};
dataView: DocViewRenderProps['dataView'];
doc: LogDocument;
}
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
export interface LogDocument extends DataTableRecord {
flattened: {
@ -37,7 +20,11 @@ export interface LogDocument extends DataTableRecord {
'trace.id'?: string;
'agent.name'?: string;
'orchestrator.cluster.name'?: string;
'orchestrator.cluster.id'?: string;
'orchestrator.resource.id'?: string;
'orchestrator.namespace'?: string;
'container.name'?: string;
'container.id'?: string;
'cloud.provider'?: string;
'cloud.region'?: string;
'cloud.availability_zone'?: string;
@ -61,7 +48,10 @@ export interface FlyoutDoc {
'trace.id'?: string;
'agent.name'?: string;
'orchestrator.cluster.name'?: string;
'orchestrator.cluster.id'?: string;
'orchestrator.resource.id'?: string;
'orchestrator.namespace'?: string;
'container.name'?: string;
'cloud.provider'?: string;
'cloud.region'?: string;
'cloud.availability_zone'?: string;
@ -72,8 +62,15 @@ export interface FlyoutDoc {
'data_stream.dataset': string;
}
export type RenderContentCustomization<Props> = (
renderPreviousContent: RenderPreviousContent<Props>
) => (props: Props) => React.ReactNode;
export type RenderPreviousContent<Props> = (props: Props) => React.ReactNode;
export interface ResourceFields {
'host.name'?: string;
'service.name'?: string;
'agent.name'?: string;
'orchestrator.cluster.name'?: string;
'orchestrator.cluster.id'?: string;
'orchestrator.resource.id'?: string;
'orchestrator.namespace'?: string;
'container.name'?: string;
'container.id'?: string;
'cloud.instance.id'?: string;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { EuiPopover, EuiPopoverTitle } from '@elastic/eui';
export const HoverPopover = ({
@ -20,10 +20,14 @@ export const HoverPopover = ({
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const leaveTimer = useRef<NodeJS.Timeout | null>(null);
const onMouseEnter = () => {
const clearTimer = () => {
if (leaveTimer.current) {
clearTimeout(leaveTimer.current);
}
};
const onMouseEnter = () => {
clearTimer();
setIsPopoverOpen(true);
};
@ -31,6 +35,12 @@ export const HoverPopover = ({
leaveTimer.current = setTimeout(() => setIsPopoverOpen(false), 100);
};
useEffect(() => {
return () => {
clearTimer();
};
}, []);
return (
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<EuiPopover

View file

@ -7,7 +7,7 @@
import React from 'react';
import { useEuiTheme } from '@elastic/eui';
import { FlyoutDoc } from '../flyout_detail/types';
import { FlyoutDoc } from '../../../common/document';
import { ChipWithPopover } from './popover_chip';
import * as constants from '../../../common/constants';

View file

@ -11,7 +11,6 @@ import {
type EuiBadgeProps,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPopover,
useEuiFontSize,
EuiPopoverFooter,
@ -24,6 +23,7 @@ import { FilterInButton } from './filter_in_button';
import { FilterOutButton } from './filter_out_button';
import { CopyButton } from './copy_button';
import { dynamic } from '../../utils/dynamic';
const DataTablePopoverCellValue = dynamic(
() => import('@kbn/unified-data-table/src/components/data_table_cell_value')
);
@ -38,7 +38,7 @@ interface ChipWithPopoverProps {
*/
text: string;
dataTestSubj?: string;
leftSideIcon?: EuiBadgeProps['iconType'];
leftSideIcon?: React.ReactNode;
rightSideIcon?: EuiBadgeProps['iconType'];
borderColor?: string | null;
style?: React.CSSProperties;
@ -78,15 +78,12 @@ export function ChipWithPopover({
font-size: ${xsFontSize};
display: flex;
justify-content: center;
margin-top: -3px;
`}
style={style}
>
<EuiFlexGroup gutterSize="xs">
{leftSideIcon && (
<EuiFlexItem>
<EuiIcon type={leftSideIcon} />
</EuiFlexItem>
)}
{leftSideIcon && <EuiFlexItem>{leftSideIcon}</EuiFlexItem>}
<EuiFlexItem>{text}</EuiFlexItem>
</EuiFlexGroup>
</EuiBadge>

View file

@ -15,6 +15,10 @@ export const contentLabel = i18n.translate('xpack.logExplorer.dataTable.header.p
defaultMessage: 'Content',
});
export const resourceLabel = i18n.translate('xpack.logExplorer.dataTable.header.popover.resource', {
defaultMessage: 'Resource',
});
export const flyoutServiceLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.service', {
defaultMessage: 'Service',
});
@ -211,3 +215,10 @@ export const contentHeaderTooltipParagraph2 = i18n.translate(
defaultMessage: 'When the message field is empty, one of the following is displayed',
}
);
export const resourceHeaderTooltipParagraph = i18n.translate(
'xpack.logExplorer.dataTable.header.resource.tooltip.paragraph',
{
defaultMessage: "Fields that provide information on the document's source, such as:",
}
);

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { LogExplorerFlyoutContentProps } from './types';
import { LogExplorerFlyoutContentProps } from '../../customizations/types';
import { useDocDetail } from '../../hooks/use_doc_detail';
import { FlyoutHeader } from './flyout_header';
import { FlyoutHighlights } from './flyout_highlights';

View file

@ -15,7 +15,7 @@ import {
useGeneratedHtmlId,
EuiTitle,
} from '@elastic/eui';
import { FlyoutDoc } from './types';
import { FlyoutDoc } from '../../../common/document';
import { getMessageWithFallbacks } from '../../hooks/use_doc_detail';
import { LogLevel } from '../common/log_level';
import { Timestamp } from './sub_components/timestamp';

View file

@ -8,7 +8,7 @@ import React from 'react';
import { CloudProvider, CloudProviderIcon } from '@kbn/custom-icons';
import { useMeasure } from 'react-use/lib';
import { first } from 'lodash';
import { FlyoutDoc, LogDocument } from './types';
import { FlyoutDoc, LogDocument } from '../../../common/document';
import * as constants from '../../../common/constants';
import { HighlightField } from './sub_components/highlight_field';
import {

View file

@ -6,4 +6,3 @@
*/
export * from './flyout_detail';
export * from './types';

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiBadge } from '@elastic/eui';
import { FlyoutDoc } from '../types';
import { FlyoutDoc } from '../../../../common/document';
interface TimestampProps {
timestamp: FlyoutDoc['@timestamp'];

View file

@ -1,8 +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.
*/
export type { FlyoutDoc, LogDocument, LogExplorerFlyoutContentProps } from '../../controller';

View file

@ -0,0 +1,43 @@
/*
* 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 type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
import { LogExplorerDiscoverServices } from '../../controller';
import { VirtualColumnServiceProvider } from '../../hooks/use_virtual_column_services';
import { CONTENT_FIELD, RESOURCE_FIELD } from '../../../common/constants';
import { Content } from './content';
import { Resource } from './resource';
export const renderCell =
(type: string, { data }: { data: LogExplorerDiscoverServices['data'] }) =>
(props: DataGridCellValueElementProps) => {
const { dataView } = props;
const virtualColumnServices = {
data,
dataView,
};
let renderedCell = null;
switch (type) {
case CONTENT_FIELD:
renderedCell = <Content {...props} />;
break;
case RESOURCE_FIELD:
renderedCell = <Resource {...props} />;
break;
default:
break;
}
return (
<VirtualColumnServiceProvider services={virtualColumnServices}>
{renderedCell}
</VirtualColumnServiceProvider>
);
};

View file

@ -8,7 +8,8 @@
import React from 'react';
import { CustomGridColumnProps } from '@kbn/unified-data-table';
import { ContentColumnTooltip } from './column_tooltips/content_column_tooltip';
import { CONTENT_FIELD } from '../../../common/constants';
import { CONTENT_FIELD, RESOURCE_FIELD } from '../../../common/constants';
import { ResourceColumnTooltip } from './column_tooltips/resource_column_tooltip';
export const renderColumn =
(field: string) =>
@ -17,6 +18,11 @@ export const renderColumn =
case CONTENT_FIELD:
column.display = <ContentColumnTooltip column={column} headerRowHeight={headerRowHeight} />;
break;
case RESOURCE_FIELD:
column.display = (
<ResourceColumnTooltip column={column} headerRowHeight={headerRowHeight} />
);
break;
default:
break;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToken, useEuiTheme } from '@elastic/eui';
import { EuiText, useEuiTheme } from '@elastic/eui';
import React from 'react';
import { CustomGridColumnProps } from '@kbn/unified-data-table';
import { css } from '@emotion/react';
@ -14,12 +14,10 @@ import {
contentHeaderTooltipParagraph2,
contentLabel,
} from '../../common/translations';
import { dynamic } from '../../../utils/dynamic';
import { HoverPopover } from '../../common/hover_popover';
const ColumnHeaderTruncateContainer = dynamic(
() => import('@kbn/unified-data-table/src/components/column_header_truncate_container')
);
import { TooltipButtonComponent } from './tooltip_button';
import { FieldWithToken } from './field_with_token';
import * as constants from '../../../../common/constants';
export const ContentColumnTooltip = ({ column, headerRowHeight }: CustomGridColumnProps) => {
const { euiTheme } = useEuiTheme();
@ -27,13 +25,16 @@ export const ContentColumnTooltip = ({ column, headerRowHeight }: CustomGridColu
margin-bottom: ${euiTheme.size.s};
`;
const contentButtonComponent = (
<ColumnHeaderTruncateContainer headerRowHeight={headerRowHeight}>
{column.displayAsText} <EuiIcon type="questionInCircle" />
</ColumnHeaderTruncateContainer>
);
return (
<HoverPopover button={contentButtonComponent} title={contentLabel}>
<HoverPopover
button={
<TooltipButtonComponent
displayText={column.displayAsText}
headerRowHeight={headerRowHeight}
/>
}
title={contentLabel}
>
<div style={{ width: '230px' }}>
<EuiText size="s" css={spacingCSS}>
<p>{contentHeaderTooltipParagraph1}</p>
@ -41,36 +42,8 @@ export const ContentColumnTooltip = ({ column, headerRowHeight }: CustomGridColu
<EuiText size="s" css={spacingCSS}>
<p>{contentHeaderTooltipParagraph2}</p>
</EuiText>
<EuiFlexGroup
responsive={false}
alignItems="center"
justifyContent="flexStart"
gutterSize="xs"
>
<EuiFlexItem grow={false}>
<EuiToken iconType="tokenKeyword" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<strong>error.message</strong>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
responsive={false}
alignItems="center"
justifyContent="flexStart"
gutterSize="xs"
>
<EuiFlexItem grow={false}>
<EuiToken iconType="tokenEvent" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<strong>event.original</strong>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<FieldWithToken field={constants.ERROR_MESSAGE_FIELD} />
<FieldWithToken field={constants.EVENT_ORIGINAL_FIELD} iconType="tokenEvent" />
</div>
</HoverPopover>
);

View file

@ -0,0 +1,43 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiToken } from '@elastic/eui';
import React from 'react';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
const spacingXsCss = css`
margin-bottom: ${euiThemeVars.euiSizeXS};
`;
export const FieldWithToken = ({
field,
iconType = 'tokenKeyword',
}: {
field: string;
iconType?: string;
}) => {
return (
<div css={spacingXsCss}>
<EuiFlexGroup
responsive={false}
alignItems="center"
justifyContent="flexStart"
gutterSize="xs"
>
<EuiFlexItem grow={false}>
<EuiToken iconType={iconType} size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<strong>{field}</strong>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
};

View file

@ -0,0 +1,50 @@
/*
* 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 { EuiText } from '@elastic/eui';
import type { CustomGridColumnProps } from '@kbn/unified-data-table';
import { euiThemeVars } from '@kbn/ui-theme';
import { resourceHeaderTooltipParagraph, resourceLabel } 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 ResourceColumnTooltip = ({ column, headerRowHeight }: CustomGridColumnProps) => {
return (
<HoverPopover
button={
<TooltipButtonComponent
displayText={column.displayAsText}
headerRowHeight={headerRowHeight}
/>
}
title={resourceLabel}
>
<div style={{ width: '230px' }}>
<EuiText size="s" css={spacingCSS}>
<p>{resourceHeaderTooltipParagraph}</p>
</EuiText>
{[
constants.SERVICE_NAME_FIELD,
constants.CONTAINER_NAME_FIELD,
constants.ORCHESTRATOR_NAMESPACE_FIELD,
constants.HOST_NAME_FIELD,
constants.CLOUD_INSTANCE_ID_FIELD,
].map((field) => (
<FieldWithToken field={field} />
))}
</div>
</HoverPopover>
);
};

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 { EuiIcon } from '@elastic/eui';
import React from 'react';
import ColumnHeaderTruncateContainer from '@kbn/unified-data-table/src/components/column_header_truncate_container';
export const TooltipButtonComponent = ({
displayText,
headerRowHeight,
}: {
displayText?: string;
headerRowHeight?: number;
}) => (
<ColumnHeaderTruncateContainer headerRowHeight={headerRowHeight}>
{displayText} <EuiIcon type="questionInCircle" />
</ColumnHeaderTruncateContainer>
);

View file

@ -12,11 +12,10 @@ import { getShouldShowFieldHandler } from '@kbn/discover-utils';
import { i18n } from '@kbn/i18n';
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
import { useDocDetail, getMessageWithFallbacks } from '../../hooks/use_doc_detail';
import { LogDocument, LogExplorerDiscoverServices } from '../../controller';
import { LogDocument } from '../../../common/document';
import { LogLevel } from '../common/log_level';
import * as constants from '../../../common/constants';
import { dynamic } from '../../utils/dynamic';
import { VirtualColumnServiceProvider } from '../../hooks/use_virtual_column_services';
import './virtual_column.scss';
const SourceDocument = dynamic(
@ -72,7 +71,7 @@ const SourcePopoverContent = ({
);
};
const Content = ({
export const Content = ({
row,
dataView,
fieldFormats,
@ -116,18 +115,3 @@ const Content = ({
</span>
);
};
export const renderContent =
({ data }: { data: LogExplorerDiscoverServices['data'] }) =>
(props: DataGridCellValueElementProps) => {
const { dataView } = props;
const virtualColumnServices = {
data,
dataView,
};
return (
<VirtualColumnServiceProvider services={virtualColumnServices}>
<Content {...props} />
</VirtualColumnServiceProvider>
);
};

View file

@ -0,0 +1,67 @@
/*
* 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 type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
import { first } from 'lodash';
import { AgentName } from '@kbn/elastic-agent-utils';
import { ChipWithPopover } from '../common/popover_chip';
import * as constants from '../../../common/constants';
import { getUnformattedResourceFields } from '../../utils/resource';
import { LogDocument } from '../../../common/document';
import { dynamic } from '../../utils/dynamic';
const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon'));
export const Resource = ({ row }: DataGridCellValueElementProps) => {
const resourceDoc = getUnformattedResourceFields(row as LogDocument);
return (
<div>
{(resourceDoc[constants.SERVICE_NAME_FIELD] as string) && (
<ChipWithPopover
property={constants.SERVICE_NAME_FIELD}
text={resourceDoc[constants.SERVICE_NAME_FIELD] as string}
rightSideIcon="arrowDown"
leftSideIcon={
<AgentIcon
agentName={first((resourceDoc[constants.AGENT_NAME_FIELD] ?? []) as AgentName[])}
size="m"
/>
}
/>
)}
{resourceDoc[constants.CONTAINER_NAME_FIELD] && (
<ChipWithPopover
property={constants.CONTAINER_NAME_FIELD}
text={resourceDoc[constants.CONTAINER_NAME_FIELD] as string}
rightSideIcon="arrowDown"
/>
)}
{resourceDoc[constants.HOST_NAME_FIELD] && (
<ChipWithPopover
property={constants.HOST_NAME_FIELD}
text={resourceDoc[constants.HOST_NAME_FIELD]}
rightSideIcon="arrowDown"
/>
)}
{resourceDoc[constants.ORCHESTRATOR_NAMESPACE_FIELD] && (
<ChipWithPopover
property={constants.ORCHESTRATOR_NAMESPACE_FIELD}
text={resourceDoc[constants.ORCHESTRATOR_NAMESPACE_FIELD] as string}
rightSideIcon="arrowDown"
/>
)}
{resourceDoc[constants.CLOUD_INSTANCE_ID_FIELD] && (
<ChipWithPopover
property={constants.CLOUD_INSTANCE_ID_FIELD}
text={resourceDoc[constants.CLOUD_INSTANCE_ID_FIELD] as string}
rightSideIcon="arrowDown"
/>
)}
</div>
);
};

View file

@ -13,7 +13,7 @@ import { interpret } from 'xstate';
import { DatasetsService } from '../services/datasets';
import { createLogExplorerControllerStateMachine } from '../state_machines/log_explorer_controller';
import { LogExplorerStartDeps } from '../types';
import { LogExplorerCustomizations } from './controller_customizations';
import { LogExplorerCustomizations } from '../customizations/types';
import { createDataServiceProxy } from './custom_data_service';
import { createUiSettingsServiceProxy } from './custom_ui_settings_service';
import {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export * from './controller_customizations';
export * from '../customizations/types';
export * from './create_controller';
export * from './provider';
export * from './types';

View file

@ -20,7 +20,7 @@ import {
LogExplorerControllerStateMachine,
LogExplorerControllerStateService,
} from '../state_machines/log_explorer_controller';
import { LogExplorerCustomizations } from './controller_customizations';
import { LogExplorerCustomizations } from '../customizations/types';
export interface LogExplorerController {
actions: {};

View file

@ -6,11 +6,12 @@
*/
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { CONTENT_FIELD } from '../../common/constants';
import { renderContent } from '../components/virtual_columns/content';
import { CONTENT_FIELD, RESOURCE_FIELD } from '../../common/constants';
import { renderCell } from '../components/virtual_columns/cell_renderer';
export const createCustomCellRenderer = ({ data }: { data: DataPublicPluginStart }) => {
return {
[CONTENT_FIELD]: renderContent({ data }),
[CONTENT_FIELD]: renderCell(CONTENT_FIELD, { data }),
[RESOURCE_FIELD]: renderCell(RESOURCE_FIELD, { data }),
};
};

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import { CONTENT_FIELD } from '../../common/constants';
import { CONTENT_FIELD, RESOURCE_FIELD } from '../../common/constants';
import { renderColumn } from '../components/virtual_columns/column';
export const createCustomGridColumnsConfiguration = () => ({
[CONTENT_FIELD]: renderColumn(CONTENT_FIELD),
[RESOURCE_FIELD]: renderColumn(RESOURCE_FIELD),
});

View file

@ -9,8 +9,9 @@ import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
import { FlyoutDetail } from '../components/flyout_detail/flyout_detail';
import { LogExplorerFlyoutContentProps } from '../components/flyout_detail';
import { LogDocument, useLogExplorerControllerContext } from '../controller';
import { LogExplorerFlyoutContentProps } from './types';
import { useLogExplorerControllerContext } from '../controller';
import { LogDocument } from '../../common/document';
const CustomFlyoutContent = ({
filter,

View file

@ -0,0 +1,32 @@
/*
* 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 { DocViewRenderProps } from '@kbn/unified-doc-viewer/src/services/types';
import { LogDocument } from '../../common/document';
export type RenderPreviousContent<Props> = (props: Props) => React.ReactNode;
export type RenderContentCustomization<Props> = (
renderPreviousContent: RenderPreviousContent<Props>
) => (props: Props) => React.ReactNode;
export interface LogExplorerFlyoutContentProps {
actions: {
addFilter: DocViewRenderProps['filter'];
addColumn: DocViewRenderProps['onAddColumn'];
removeColumn: DocViewRenderProps['onRemoveColumn'];
};
dataView: DocViewRenderProps['dataView'];
doc: LogDocument;
}
export interface LogExplorerCustomizations {
flyout?: {
renderContent?: RenderContentCustomization<LogExplorerFlyoutContentProps>;
};
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import createContainer from 'constate';
import type { LogExplorerFlyoutContentProps } from '../components/flyout_detail/types';
import type { LogExplorerFlyoutContentProps } from '../customizations/types';
interface UseFlyoutActionsDeps {
value: LogExplorerFlyoutContentProps['actions'];

View file

@ -7,11 +7,8 @@
import { formatFieldValue } from '@kbn/discover-utils';
import * as constants from '../../common/constants';
import { useKibanaContextForPlugin } from '../utils/use_kibana';
import {
FlyoutDoc,
LogExplorerFlyoutContentProps,
LogDocument,
} from '../components/flyout_detail/types';
import { LogExplorerFlyoutContentProps } from '../customizations/types';
import { FlyoutDoc, LogDocument } from '../../common/document';
export function useDocDetail(
doc: LogDocument,

View file

@ -8,14 +8,17 @@
import type { PluginInitializerContext } from '@kbn/core/public';
import type { LogExplorerConfig } from '../common/plugin_config';
import { LogExplorerPlugin } from './plugin';
export type {
CreateLogExplorerController,
LogExplorerController,
LogExplorerCustomizations,
LogExplorerFlyoutContentProps,
LogExplorerPublicState,
LogExplorerPublicStateUpdate,
} from './controller';
export type {
LogExplorerCustomizations,
LogExplorerFlyoutContentProps,
} from './customizations/types';
export type { LogExplorerControllerContext } from './state_machines/log_explorer_controller';
export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types';
export {

View file

@ -0,0 +1,40 @@
/*
* 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, 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;
};
export const getUnformattedResourceFields = (doc: LogDocument): ResourceFields => {
const serviceName = getFieldFromDoc(doc, constants.SERVICE_NAME_FIELD);
const hostName = getFieldFromDoc(doc, constants.HOST_NAME_FIELD);
const agentName = getFieldFromDoc(doc, constants.AGENT_NAME_FIELD);
const orchestratorClusterName = getFieldFromDoc(doc, constants.ORCHESTRATOR_CLUSTER_NAME_FIELD);
const orchestratorResourceId = getFieldFromDoc(doc, constants.ORCHESTRATOR_RESOURCE_ID_FIELD);
const orchestratorNamespace = getFieldFromDoc(doc, constants.ORCHESTRATOR_NAMESPACE_FIELD);
const containerName = getFieldFromDoc(doc, constants.CONTAINER_NAME_FIELD);
const containerId = getFieldFromDoc(doc, constants.CONTAINER_ID_FIELD);
const cloudInstanceId = getFieldFromDoc(doc, constants.CLOUD_INSTANCE_ID_FIELD);
return {
[constants.SERVICE_NAME_FIELD]: serviceName,
[constants.HOST_NAME_FIELD]: hostName,
[constants.AGENT_NAME_FIELD]: agentName,
[constants.ORCHESTRATOR_CLUSTER_NAME_FIELD]: orchestratorClusterName,
[constants.ORCHESTRATOR_RESOURCE_ID_FIELD]: orchestratorResourceId,
[constants.ORCHESTRATOR_NAMESPACE_FIELD]: orchestratorNamespace,
[constants.CONTAINER_NAME_FIELD]: containerName,
[constants.CONTAINER_ID_FIELD]: containerId,
[constants.CLOUD_INSTANCE_ID_FIELD]: cloudInstanceId,
};
};

View file

@ -41,6 +41,8 @@
"@kbn/unified-field-list",
"@kbn/unified-search-plugin",
"@kbn/xstate-utils",
"@kbn/elastic-agent-utils",
"@kbn/ui-theme",
],
"exclude": [
"target/**/*"

View file

@ -9,7 +9,7 @@ import moment from 'moment/moment';
import { log, timerange } from '@kbn/apm-synthtrace-client';
import { FtrProviderContext } from './config';
const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'content'];
const defaultLogColumns = ['@timestamp', 'resource', 'content'];
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
@ -58,8 +58,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
mode: 'absolute',
},
columns: [
{ field: 'service.name' },
{ field: 'host.name' },
{ field: 'resource' },
{ field: 'content' },
{ field: 'data_stream.namespace' },
],
@ -78,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, 5);
const cellElement = await dataGrid.getCellElement(0, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('A sample log')).to.be(true);
@ -87,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, 5);
const cellElement = await dataGrid.getCellElement(1, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(false);
expect(cellValue.includes('A sample log')).to.be(true);
@ -96,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, 5);
const cellElement = await dataGrid.getCellElement(2, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('error.message')).to.be(true);
@ -106,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, 5);
const cellElement = await dataGrid.getCellElement(3, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('event.original')).to.be(true);
@ -116,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, 5);
const cellElement = await dataGrid.getCellElement(4, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
@ -132,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, 5);
await dataGrid.clickCellExpandButton(4, 4);
await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover');
});
});
@ -140,19 +139,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('on cell expansion with message field should open regular popover', async () => {
await navigateToLogExplorer();
await retry.tryForTime(TEST_TIMEOUT, async () => {
await dataGrid.clickCellExpandButton(3, 5);
await dataGrid.clickCellExpandButton(3, 4);
await testSubjects.existOrFail('euiDataGridExpansionPopover');
});
});
});
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 cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('synth-service')).to.be(true);
expect(cellValue.includes('synth-host')).to.be(true);
});
});
});
describe('virtual column cell actions', async () => {
beforeEach(async () => {
await navigateToLogExplorer();
});
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, 5);
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -168,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, 5);
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -188,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, 5);
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -203,6 +213,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.missingOrFail('dataTablePopoverChip_log.level');
});
});
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 serviceNameChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_service.name'
);
await serviceNameChip.click();
// Find Filter In button
const filterInButton = await testSubjects.find(
'dataTableCellAction_addToFilterAction_service.name'
);
await filterInButton.click();
const rowWithLogLevelInfo = await testSubjects.findAll(
'dataTablePopoverChip_service.name'
);
expect(rowWithLogLevelInfo.length).to.be(2);
});
});
});
});
}
@ -215,7 +247,12 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
Array(count)
.fill(0)
.map(() => {
return log.create().message('A sample log').logLevel('info').timestamp(timestamp);
return log
.create()
.message('A sample log')
.logLevel('info')
.timestamp(timestamp)
.defaults({ 'service.name': 'synth-service' });
})
);
@ -229,7 +266,11 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
Array(count)
.fill(0)
.map(() => {
return log.create().message('A sample log').timestamp(timestamp);
return log
.create()
.message('A sample log')
.timestamp(timestamp)
.defaults({ 'service.name': 'synth-service' });
})
);
@ -243,11 +284,10 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
Array(count)
.fill(0)
.map(() => {
return log
.create()
.logLevel('info')
.timestamp(timestamp)
.defaults({ 'error.message': 'message in error object' });
return log.create().logLevel('info').timestamp(timestamp).defaults({
'error.message': 'message in error object',
'service.name': 'node-service',
});
})
);
@ -261,11 +301,10 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
Array(count)
.fill(0)
.map(() => {
return log
.create()
.logLevel('info')
.timestamp(timestamp)
.defaults({ 'event.original': 'message in event original' });
return log.create().logLevel('info').timestamp(timestamp).defaults({
'event.original': 'message in event original',
'service.name': 'node-service',
});
})
);

View file

@ -69,8 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
expect(await PageObjects.discover.getColumnHeaders()).to.eql([
'@timestamp',
'service.name',
'host.name',
'resource',
'content',
]);
});

View file

@ -9,7 +9,7 @@ import { log, timerange } from '@kbn/apm-synthtrace-client';
import moment from 'moment';
import { FtrProviderContext } from '../../../ftr_provider_context';
const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'content'];
const defaultLogColumns = ['@timestamp', 'resource', 'content'];
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
@ -60,8 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
mode: 'absolute',
},
columns: [
{ field: 'service.name' },
{ field: 'host.name' },
{ field: 'resource' },
{ field: 'content' },
{ field: 'data_stream.namespace' },
],
@ -80,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, 5);
const cellElement = await dataGrid.getCellElement(0, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('A sample log')).to.be(true);
@ -89,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, 5);
const cellElement = await dataGrid.getCellElement(1, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(false);
expect(cellValue.includes('A sample log')).to.be(true);
@ -98,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, 5);
const cellElement = await dataGrid.getCellElement(2, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('error.message')).to.be(true);
@ -108,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, 5);
const cellElement = await dataGrid.getCellElement(3, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
expect(cellValue.includes('event.original')).to.be(true);
@ -118,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, 5);
const cellElement = await dataGrid.getCellElement(4, 4);
const cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('info')).to.be(true);
@ -134,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, 5);
await dataGrid.clickCellExpandButton(4, 4);
await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover');
});
});
@ -142,19 +141,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('on cell expansion with message field should open regular popover', async () => {
await navigateToLogExplorer();
await retry.tryForTime(TEST_TIMEOUT, async () => {
await dataGrid.clickCellExpandButton(3, 5);
await dataGrid.clickCellExpandButton(3, 4);
await testSubjects.existOrFail('euiDataGridExpansionPopover');
});
});
});
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 cellValue = await cellElement.getVisibleText();
expect(cellValue.includes('synth-service')).to.be(true);
expect(cellValue.includes('synth-host')).to.be(true);
});
});
});
describe('virtual column cell actions', async () => {
beforeEach(async () => {
await navigateToLogExplorer();
});
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, 5);
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -170,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, 5);
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -190,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, 5);
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
@ -205,6 +215,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.missingOrFail('dataTablePopoverChip_log.level');
});
});
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 serviceNameChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_service.name'
);
await serviceNameChip.click();
// Find Filter In button
const filterInButton = await testSubjects.find(
'dataTableCellAction_addToFilterAction_service.name'
);
await filterInButton.click();
const rowWithLogLevelInfo = await testSubjects.findAll(
'dataTablePopoverChip_service.name'
);
expect(rowWithLogLevelInfo.length).to.be(2);
});
});
});
});
}
@ -217,7 +249,12 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
Array(count)
.fill(0)
.map(() => {
return log.create().message('A sample log').logLevel('info').timestamp(timestamp);
return log
.create()
.message('A sample log')
.logLevel('info')
.timestamp(timestamp)
.defaults({ 'service.name': 'synth-service' });
})
);
@ -231,7 +268,11 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
Array(count)
.fill(0)
.map(() => {
return log.create().message('A sample log').timestamp(timestamp);
return log
.create()
.message('A sample log')
.timestamp(timestamp)
.defaults({ 'service.name': 'synth-service' });
})
);
@ -245,11 +286,10 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
Array(count)
.fill(0)
.map(() => {
return log
.create()
.logLevel('info')
.timestamp(timestamp)
.defaults({ 'error.message': 'message in error object' });
return log.create().logLevel('info').timestamp(timestamp).defaults({
'error.message': 'message in error object',
'service.name': 'node-service',
});
})
);
@ -263,11 +303,10 @@ function generateLogsData({ to, count = 1 }: { to: string; count?: number }) {
Array(count)
.fill(0)
.map(() => {
return log
.create()
.logLevel('info')
.timestamp(timestamp)
.defaults({ 'event.original': 'message in event original' });
return log.create().logLevel('info').timestamp(timestamp).defaults({
'event.original': 'message in event original',
'service.name': 'node-service',
});
})
);

View file

@ -91,8 +91,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
expect(await PageObjects.discover.getColumnHeaders()).to.eql([
'@timestamp',
'service.name',
'host.name',
'resource',
'content',
]);
});
@ -151,8 +150,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.discover.getColumnHeaders()).not.to.eql([
'@timestamp',
'content',
'service.name',
'host.name',
'resource',
]);
});