[One Discover] Add row indicators for log documents (#190676)

## 📓 Summary

Closes #190457 

This change extracts the logic around the logs document
controls/indicator from Logs Explorer and registers the same controls in
Discover through the extension point added in #188762.

The controls are registered only for the `logs-data-source-profile`
contextual profile.


https://github.com/user-attachments/assets/40adfb19-357f-46e1-9d69-fc9c0860c832

## 👣 Next steps
- [ ] https://github.com/elastic/kibana/issues/190670
- [ ] https://github.com/elastic/kibana/issues/190460

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Antonio Ghiani 2024-09-11 15:11:31 +02:00 committed by GitHub
parent c4dd4d1d99
commit ac66e7cb44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 406 additions and 226 deletions

View file

@ -34,6 +34,8 @@ export {
buildDataTableRecord,
buildDataTableRecordList,
createLogsContextService,
createDegradedDocsControl,
createStacktraceControl,
fieldConstants,
formatFieldValue,
formatHit,

View file

@ -0,0 +1,121 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiCode, EuiSpacer } from '@elastic/eui';
import {
RowControlColumn,
RowControlComponent,
RowControlProps,
RowControlRowProps,
} from './types';
import { DEGRADED_DOCS_FIELDS } from '../../field_constants';
interface DegradedDocsControlProps extends Partial<RowControlProps> {
enabled?: boolean;
}
/**
* Degraded docs control factory function.
* @param props Optional props for the generated Control component, useful to override onClick, etc
*/
export const createDegradedDocsControl = (props?: DegradedDocsControlProps): RowControlColumn => ({
id: 'connectedDegradedDocs',
headerAriaLabel: actionsHeaderAriaLabelDegradedAction,
renderControl: (Control, rowProps) => {
return <DegradedDocs Control={Control} rowProps={rowProps} {...props} />;
},
});
const actionsHeaderAriaLabelDegradedAction = i18n.translate(
'discover.customControl.degradedDocArialLabel',
{ defaultMessage: 'Access to degraded docs' }
);
const degradedDocButtonLabelWhenPresent = i18n.translate(
'discover.customControl.degradedDocPresent',
{
defaultMessage:
"This document couldn't be parsed correctly. Not all fields are properly populated",
}
);
const degradedDocButtonLabelWhenNotPresent = i18n.translate(
'discover.customControl.degradedDocNotPresent',
{ defaultMessage: 'All fields in this document were parsed correctly' }
);
const degradedDocButtonLabelWhenDisabled = i18n.translate(
'discover.customControl.degradedDocDisabled',
{
defaultMessage:
'Degraded document field detection is currently disabled for this search. To enable it, include the METADATA directive for the `_ignored` field in your ES|QL query. For example:',
}
);
const DegradedDocs = ({
Control,
enabled = true,
rowProps: { record },
...props
}: {
Control: RowControlComponent;
rowProps: RowControlRowProps;
} & DegradedDocsControlProps) => {
const isDegradedDocumentExists = DEGRADED_DOCS_FIELDS.some(
(field) => field in record.raw && record.raw[field] !== null
);
if (!enabled) {
const codeSample = 'FROM logs-* METADATA _ignored';
const tooltipContent = (
<div>
{degradedDocButtonLabelWhenDisabled}
<EuiSpacer size="s" />
<EuiCode>{codeSample}</EuiCode>
</div>
);
return (
<Control
disabled
data-test-subj="docTableDegradedDocDisabled"
tooltipContent={tooltipContent}
label={`${degradedDocButtonLabelWhenDisabled} ${codeSample}`}
iconType="indexClose"
onClick={undefined}
{...props}
/>
);
}
return isDegradedDocumentExists ? (
<Control
data-test-subj="docTableDegradedDocExist"
color="danger"
tooltipContent={degradedDocButtonLabelWhenPresent}
label={degradedDocButtonLabelWhenPresent}
iconType="indexClose"
onClick={undefined}
{...props}
/>
) : (
<Control
data-test-subj="docTableDegradedDocDoesNotExist"
color="text"
tooltipContent={degradedDocButtonLabelWhenNotPresent}
label={degradedDocButtonLabelWhenNotPresent}
iconType="indexClose"
onClick={undefined}
{...props}
/>
);
};

View file

@ -0,0 +1,11 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { createDegradedDocsControl } from './degraded_docs_control';
export { createStacktraceControl } from './stacktrace_control';

View file

@ -0,0 +1,79 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
RowControlColumn,
RowControlComponent,
RowControlProps,
RowControlRowProps,
} from './types';
import { LogDocument } from '../../data_types';
import { getStacktraceFields } from '../../utils/get_stack_trace_fields';
/**
* Stacktrace control factory function.
* @param props Optional props for the generated Control component, useful to override onClick, etc
*/
export const createStacktraceControl = (props?: Partial<RowControlProps>): RowControlColumn => ({
id: 'connectedStacktraceDocs',
headerAriaLabel: actionsHeaderAriaLabelStacktraceAction,
renderControl: (Control, rowProps) => {
return <Stacktrace Control={Control} rowProps={rowProps} {...props} />;
},
});
const actionsHeaderAriaLabelStacktraceAction = i18n.translate(
'discover.customControl.stacktraceArialLabel',
{ defaultMessage: 'Access to available stacktraces' }
);
const stacktraceAvailableControlButton = i18n.translate(
'discover.customControl.stacktrace.available',
{ defaultMessage: 'Stacktraces available' }
);
const stacktraceNotAvailableControlButton = i18n.translate(
'discover.customControl.stacktrace.notAvailable',
{ defaultMessage: 'Stacktraces not available' }
);
const Stacktrace = ({
Control,
rowProps: { record },
...props
}: {
Control: RowControlComponent;
rowProps: RowControlRowProps;
} & Partial<RowControlProps>) => {
const stacktrace = getStacktraceFields(record as LogDocument);
const hasValue = Object.values(stacktrace).some(Boolean);
return hasValue ? (
<Control
data-test-subj="docTableStacktraceExist"
label={stacktraceAvailableControlButton}
tooltipContent={stacktraceAvailableControlButton}
iconType="apmTrace"
onClick={undefined}
{...props}
/>
) : (
<Control
disabled
data-test-subj="docTableStacktraceDoesNotExist"
label={stacktraceNotAvailableControlButton}
tooltipContent={stacktraceNotAvailableControlButton}
iconType="apmTrace"
onClick={undefined}
{...props}
/>
);
};

View file

@ -0,0 +1,36 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiButtonIconProps, EuiDataGridControlColumn, IconType } from '@elastic/eui';
import React, { FC, ReactElement } from 'react';
import { DataTableRecord } from '../../types';
export interface RowControlRowProps {
rowIndex: number;
record: DataTableRecord;
}
export interface RowControlProps {
'data-test-subj'?: string;
color?: EuiButtonIconProps['color'];
disabled?: boolean;
label: string;
iconType: IconType;
onClick: ((props: RowControlRowProps) => void) | undefined;
tooltipContent?: React.ReactNode;
}
export type RowControlComponent = FC<RowControlProps>;
export interface RowControlColumn {
id: string;
headerAriaLabel: string;
headerCellRender?: EuiDataGridControlColumn['headerCellRender'];
renderControl: (Control: RowControlComponent, props: RowControlRowProps) => ReactElement;
}

View file

@ -35,7 +35,7 @@ export const CONTAINER_NAME_FIELD = 'container.name';
export const CONTAINER_ID_FIELD = 'container.id';
// Degraded Docs
export const DEGRADED_DOCS_FIELD = 'ignored_field_values';
export const DEGRADED_DOCS_FIELDS = ['ignored_field_values', '_ignored'] as const;
// Error Stacktrace
export const ERROR_STACK_TRACE = 'error.stack_trace';

View file

@ -12,3 +12,5 @@ export * as fieldConstants from './field_constants';
export * from './hooks';
export * from './utils';
export * from './data_types';
export * from './components/custom_control_columns';

View file

@ -11,6 +11,12 @@ import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'
import type { DatatableColumnMeta } from '@kbn/expressions-plugin/common';
export type { IgnoredReason, ShouldShowFieldInTableHandler } from './utils';
export type {
RowControlColumn,
RowControlComponent,
RowControlProps,
RowControlRowProps,
} from './components/custom_control_columns/types';
type DiscoverSearchHit = SearchHit<Record<string, unknown>>;

View file

@ -0,0 +1,27 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { getFieldFromDoc, LogDocument, StackTraceFields } from '..';
import {
ERROR_EXCEPTION_STACKTRACE,
ERROR_LOG_STACKTRACE,
ERROR_STACK_TRACE,
} from '../field_constants';
export const getStacktraceFields = (doc: LogDocument): StackTraceFields => {
const errorStackTrace = getFieldFromDoc(doc, ERROR_STACK_TRACE);
const errorExceptionStackTrace = getFieldFromDoc(doc, ERROR_EXCEPTION_STACKTRACE);
const errorLogStackTrace = getFieldFromDoc(doc, ERROR_LOG_STACKTRACE);
return {
[ERROR_STACK_TRACE]: errorStackTrace,
[ERROR_EXCEPTION_STACKTRACE]: errorExceptionStackTrace,
[ERROR_LOG_STACKTRACE]: errorLogStackTrace,
};
};

View file

@ -8,6 +8,7 @@
*/
export * from './build_data_record';
export * from './calc_field_counts';
export * from './format_hit';
export * from './format_value';
export * from './get_doc_id';
@ -15,6 +16,7 @@ export * from './get_ignored_reason';
export * from './get_log_document_overview';
export * from './get_message_field_with_fallbacks';
export * from './get_should_show_field_handler';
export * from './get_stack_trace_fields';
export * from './nested_fields';
export * from './get_field_value';
export * from './calc_field_counts';

View file

@ -11,7 +11,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx"
],
"exclude": [
"target/**/*"

View file

@ -8,4 +8,4 @@ This package contains utilities for ES|QL.
- *removeDropCommandsFromESQLQuery*: Use this function to remove all the occurences of the `drop` command from the query.
- *appendToESQLQuery*: Use this function to append more pipes in an existing ES|QL query. It adds the additional commands in a new line.
- *appendWhereClauseToESQLQuery*: Use this function to append where clause in an existing query.
- *retieveMetadataColumns*: Use this function to get if there is a metadata option in the from command, and retrieve the columns if so
- *retrieveMetadataColumns*: Use this function to get if there is a metadata option in the from command, and retrieve the columns if so

View file

@ -27,7 +27,7 @@ export {
hasStartEndParams,
prettifyQuery,
isQueryWrappedByPipes,
retieveMetadataColumns,
retrieveMetadataColumns,
TextBasedLanguages,
} from './src';

View file

@ -19,7 +19,7 @@ export {
getTimeFieldFromESQLQuery,
prettifyQuery,
isQueryWrappedByPipes,
retieveMetadataColumns,
retrieveMetadataColumns,
} from './utils/query_parsing_helpers';
export { appendToESQLQuery, appendWhereClauseToESQLQuery } from './utils/append_to_query';
export {

View file

@ -15,7 +15,7 @@ import {
getTimeFieldFromESQLQuery,
prettifyQuery,
isQueryWrappedByPipes,
retieveMetadataColumns,
retrieveMetadataColumns,
} from './query_parsing_helpers';
describe('esql query helpers', () => {
@ -217,16 +217,16 @@ describe('esql query helpers', () => {
});
});
describe('retieveMetadataColumns', () => {
describe('retrieveMetadataColumns', () => {
it('should return metadata columns if they exist', () => {
expect(retieveMetadataColumns('from a metadata _id, _ignored | eval b = 1')).toStrictEqual([
expect(retrieveMetadataColumns('from a metadata _id, _ignored | eval b = 1')).toStrictEqual([
'_id',
'_ignored',
]);
});
it('should return empty columns if metadata doesnt exist', () => {
expect(retieveMetadataColumns('from a | eval b = 1')).toStrictEqual([]);
expect(retrieveMetadataColumns('from a | eval b = 1')).toStrictEqual([]);
});
});
});

View file

@ -126,7 +126,7 @@ export const prettifyQuery = (query: string, isWrapped: boolean): string => {
return BasicPrettyPrinter.print(ast, { multiline: !isWrapped });
};
export const retieveMetadataColumns = (esql: string): string[] => {
export const retrieveMetadataColumns = (esql: string): string[] => {
const { ast } = getAstAndSyntaxErrors(esql);
const options: ESQLCommandOption[] = [];

View file

@ -18,7 +18,7 @@ import {
EuiSpacer,
EuiDataGridControlColumn,
} from '@elastic/eui';
import type { RowControlColumn } from '../src/types';
import { RowControlColumn } from '@kbn/discover-utils';
const SelectionHeaderCell = () => {
return (
@ -128,6 +128,7 @@ export const mockRowAdditionalLeadingControls = ['visBarVerticalStacked', 'heart
<Control
data-test-subj={`exampleRowControl-${iconType}`}
label={`Example ${iconType}`}
tooltipContent={`Example ${iconType}`}
iconType={iconType}
onClick={() => {
alert(`Example "${iconType}" control clicked. Row index: ${rowProps.rowIndex}`);

View file

@ -8,7 +8,7 @@
*/
import type { EuiDataGridControlColumn } from '@elastic/eui';
import type { RowControlColumn } from '../../../types';
import { RowControlColumn } from '@kbn/discover-utils';
import { getRowControlColumn } from './row_control_column';
import { getRowMenuControlColumn } from './row_menu_control_column';

View file

@ -25,7 +25,12 @@ describe('getRowControlColumn', () => {
id: 'test_row_control',
headerAriaLabel: 'row control',
renderControl: jest.fn((Control, rowProps) => (
<Control label={`test-${rowProps.rowIndex}`} iconType="heart" onClick={mockClick} />
<Control
label={`test-${rowProps.rowIndex}`}
tooltipContent={`test-${rowProps.rowIndex}`}
iconType="heart"
onClick={mockClick}
/>
)),
};
const rowControlColumn = getRowControlColumn(props);

View file

@ -15,8 +15,8 @@ import {
EuiScreenReaderOnly,
EuiToolTip,
} from '@elastic/eui';
import { RowControlColumn, RowControlProps } from '@kbn/discover-utils';
import { DataTableRowControl, Size } from '../../data_table_row_control';
import type { RowControlColumn, RowControlProps } from '../../../types';
import { DEFAULT_CONTROL_COLUMN_WIDTH } from '../../../constants';
import { useControlColumn } from '../../../hooks/use_control_column';
@ -30,10 +30,18 @@ export const RowControlCell = ({
const Control: React.FC<RowControlProps> = useMemo(
() =>
({ 'data-test-subj': dataTestSubj, color, disabled, label, iconType, onClick }) => {
({
'data-test-subj': dataTestSubj,
color,
disabled,
iconType,
label,
onClick,
tooltipContent,
}) => {
return (
<DataTableRowControl size={Size.normal}>
<EuiToolTip content={label} delay="long">
<EuiToolTip content={tooltipContent ?? label} delay="long">
<EuiButtonIcon
data-test-subj={dataTestSubj ?? `unifiedDataTable_rowControl_${props.columnId}`}
disabled={disabled}

View file

@ -26,7 +26,12 @@ describe('getRowMenuControlColumn', () => {
id: 'test_row_menu_control',
headerAriaLabel: 'row control',
renderControl: jest.fn((Control, rowProps) => (
<Control label={`test-${rowProps.rowIndex}`} iconType="heart" onClick={mockClick} />
<Control
label={`test-${rowProps.rowIndex}`}
tooltipContent={`test-${rowProps.rowIndex}`}
iconType="heart"
onClick={mockClick}
/>
)),
};
const rowMenuControlColumn = getRowMenuControlColumn([

View file

@ -20,8 +20,8 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { RowControlColumn, RowControlProps } from '@kbn/discover-utils';
import { DataTableRowControl, Size } from '../../data_table_row_control';
import type { RowControlColumn, RowControlProps } from '../../../types';
import { DEFAULT_CONTROL_COLUMN_WIDTH } from '../../../constants';
import { useControlColumn } from '../../../hooks/use_control_column';

View file

@ -40,6 +40,7 @@ import type { ToastsStart, IUiSettingsClient } from '@kbn/core/public';
import type { Serializable } from '@kbn/utility-types';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import {
RowControlColumn,
getShouldShowFieldHandler,
canPrependTimeFieldColumn,
getVisibleColumns,
@ -57,7 +58,6 @@ import {
DataTableColumnsMeta,
CustomCellRenderer,
CustomGridColumnsConfiguration,
RowControlColumn,
} from '../types';
import { getDisplayedColumns } from '../utils/columns';
import { convertValueToString } from '../utils/convert_value_to_string';

View file

@ -7,18 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { ReactElement, FC } from 'react';
import type {
EuiDataGridCellValueElementProps,
EuiDataGridColumn,
IconType,
EuiButtonIconProps,
} from '@elastic/eui';
import type { ReactElement } from 'react';
import type { EuiDataGridCellValueElementProps, EuiDataGridColumn } from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { EuiDataGridControlColumn } from '@elastic/eui/src/components/datagrid/data_grid_types';
export type { DataTableColumnsMeta } from '@kbn/discover-utils/src/types';
export type { DataGridDensity } from './constants';
@ -67,26 +60,3 @@ export type CustomGridColumnsConfiguration = Record<
string,
(props: CustomGridColumnProps) => EuiDataGridColumn
>;
export interface RowControlRowProps {
rowIndex: number;
record: DataTableRecord;
}
export interface RowControlProps {
'data-test-subj'?: string;
color?: EuiButtonIconProps['color'];
disabled?: boolean;
label: string;
iconType: IconType;
onClick: ((props: RowControlRowProps) => void) | undefined;
}
export type RowControlComponent = FC<RowControlProps>;
export interface RowControlColumn {
id: string;
headerAriaLabel: string;
headerCellRender?: EuiDataGridControlColumn['headerCellRender'];
renderControl: (Control: RowControlComponent, props: RowControlRowProps) => ReactElement;
}

View file

@ -493,6 +493,7 @@ function DiscoverDocumentsComponent({
additionalFieldGroups={additionalFieldGroups}
dataGridDensityState={density}
onUpdateDataGridDensity={onUpdateDensity}
query={query}
cellActionsTriggerId={DISCOVER_CELL_ACTIONS_TRIGGER.id}
cellActionsMetadata={cellActionsMetadata}
cellActionsHandling="append"

View file

@ -14,13 +14,19 @@ import {
type UnifiedDataTableProps,
} from '@kbn/unified-data-table';
import { useProfileAccessor } from '../../context_awareness';
import { DiscoverAppState } from '../../application/main/state_management/discover_app_state_container';
export interface DiscoverGridProps extends UnifiedDataTableProps {
query?: DiscoverAppState['query'];
}
/**
* Customized version of the UnifiedDataTable
* @constructor
*/
export const DiscoverGrid: React.FC<UnifiedDataTableProps> = ({
export const DiscoverGrid: React.FC<DiscoverGridProps> = ({
rowAdditionalLeadingControls: customRowAdditionalLeadingControls,
query,
...props
}) => {
const getRowIndicatorProvider = useProfileAccessor('getRowIndicatorProvider');
@ -34,8 +40,14 @@ export const DiscoverGrid: React.FC<UnifiedDataTableProps> = ({
const rowAdditionalLeadingControls = useMemo(() => {
return getRowAdditionalLeadingControlsAccessor(() => customRowAdditionalLeadingControls)({
dataView: props.dataView,
query,
});
}, [getRowAdditionalLeadingControlsAccessor, props.dataView, customRowAdditionalLeadingControls]);
}, [
getRowAdditionalLeadingControlsAccessor,
props.dataView,
query,
customRowAdditionalLeadingControls,
]);
return (
<UnifiedDataTable

View file

@ -8,8 +8,7 @@
*/
import { EuiBadge } from '@elastic/eui';
import { getFieldValue } from '@kbn/discover-utils';
import type { RowControlColumn } from '@kbn/unified-data-table';
import { getFieldValue, RowControlColumn } from '@kbn/discover-utils';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
import { euiThemeVars } from '@kbn/ui-theme';
@ -88,6 +87,7 @@ export const exampleDataSourceProfileProvider: DataSourceProfileProvider = {
<Control
data-test-subj={`exampleLogsControl_${iconType}`}
label={`Example ${iconType}`}
tooltipContent={`Example ${iconType}`}
iconType={iconType}
onClick={() => {
alert(`Example "${iconType}" control clicked. Row index: ${rowProps.rowIndex}`);

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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { createDegradedDocsControl, createStacktraceControl } from '@kbn/discover-utils';
import { retrieveMetadataColumns } from '@kbn/esql-utils';
import { AggregateQuery, isOfAggregateQueryType } from '@kbn/es-query';
import type { DataSourceProfileProvider } from '../../../profiles';
export const getRowAdditionalLeadingControls: DataSourceProfileProvider['profile']['getRowAdditionalLeadingControls'] =
(prev) => (params) => {
const additionalControls = prev(params) || [];
const { query } = params;
const isDegradedDocsControlEnabled = isOfAggregateQueryType(query)
? queryContainsMetadataIgnored(query)
: true;
return [
...additionalControls,
createDegradedDocsControl({ enabled: isDegradedDocsControlEnabled }),
createStacktraceControl(),
];
};
const queryContainsMetadataIgnored = (query: AggregateQuery) =>
retrieveMetadataColumns(query.esql).includes('_ignored');

View file

@ -160,4 +160,18 @@ describe('logsDataSourceProfileProvider', () => {
expect(cellRenderers?.['log_level.keyword']).toBeDefined();
});
});
describe('getRowAdditionalLeadingControls', () => {
it('should return the passed additional controls', () => {
const getRowAdditionalLeadingControls =
logsDataSourceProfileProvider.profile.getRowAdditionalLeadingControls?.(() => undefined);
const rowAdditionalLeadingControls = getRowAdditionalLeadingControls?.({
dataView: dataViewWithLogLevel,
});
expect(rowAdditionalLeadingControls).toHaveLength(2);
expect(rowAdditionalLeadingControls?.[0].id).toBe('connectedDegradedDocs');
expect(rowAdditionalLeadingControls?.[1].id).toBe('connectedStacktraceDocs');
});
});
});

View file

@ -12,6 +12,7 @@ import { ProfileProviderServices } from '../profile_provider_services';
import { getRowIndicatorProvider } from './accessors';
import { extractIndexPatternFrom } from '../extract_index_pattern_from';
import { getCellRenderers } from './accessors';
import { getRowAdditionalLeadingControls } from './accessors/get_row_additional_leading_controls';
export const createLogsDataSourceProfileProvider = (
services: ProfileProviderServices
@ -20,6 +21,7 @@ export const createLogsDataSourceProfileProvider = (
profile: {
getRowIndicatorProvider,
getCellRenderers,
getRowAdditionalLeadingControls,
},
resolve: (params) => {
const indexPattern = extractIndexPatternFrom(params);

View file

@ -17,6 +17,7 @@ import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import type { OmitIndexSignature } from 'type-fest';
import type { Trigger } from '@kbn/ui-actions-plugin/public';
import type { DiscoverDataSource } from '../../common/data_sources';
import { DiscoverAppState } from '../application/main/state_management/discover_app_state_container';
export interface DocViewerExtension {
title: string | undefined;
@ -47,6 +48,7 @@ export interface DefaultAppStateExtension {
export interface RowControlsExtensionParams {
dataView: DataView;
query?: DiscoverAppState['query'];
}
export const DISCOVER_CELL_ACTIONS_TRIGGER: Trigger = { id: 'DISCOVER_CELL_ACTIONS_TRIGGER_ID' };

View file

@ -205,7 +205,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
if (isLegacyDefault) {
await testSubjects.click('docTableCellFilter');
} else {
await dataGrid.clickCellFilterForButton(1, 3);
await dataGrid.clickCellFilterForButtonExcludingControlColumns(1, 1);
}
const filterCount = await filterBar.getFilterCount();
expect(filterCount).to.equal(1);

View file

@ -201,6 +201,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await dataGrid.getHeaders()).to.eql([
'Select column',
'Control column',
'Access to degraded docs',
'Access to available stacktraces',
'Numberbytes',
'machine.ram_range',
]);

View file

@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return text === 'Sep 22, 2015 @ 23:50:13.253';
});
await dataGrid.clickCellExpandButton(0, 3);
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 1);
let expandDocId = '';
await retry.waitForWithTimeout('expandDocId to be valid', 5000, async () => {
@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug(`row document timestamp: ${text}`);
return text === 'Sep 22, 2015 @ 23:50:13.253';
});
await dataGrid.clickCellExpandButton(0, 3);
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 1);
let expandDocId = '';
await retry.waitForWithTimeout('expandDocId to be valid', 5000, async () => {

View file

@ -487,7 +487,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should filter by scripted field value in Discover', async function () {
await PageObjects.header.waitUntilLoadingHasFinished();
await dataGrid.clickCellFilterForButton(0, 3);
await dataGrid.clickCellFilterForButtonExcludingControlColumns(0, 1);
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.try(async function () {

View file

@ -96,46 +96,3 @@ export const resourceHeaderTooltipParagraph = i18n.translate(
defaultMessage: "Fields that provide information on the document's source, such as:",
}
);
export const actionsHeaderAriaLabelDegradedAction = i18n.translate(
'xpack.logsExplorer.dataTable.controlColumnHeader.degradedDocArialLabel',
{
defaultMessage: 'Access to degraded docs',
}
);
export const actionsHeaderAriaLabelStacktraceAction = i18n.translate(
'xpack.logsExplorer.dataTable.controlColumnHeader.stacktraceArialLabel',
{
defaultMessage: 'Access to available stacktraces',
}
);
export const degradedDocButtonLabelWhenPresent = i18n.translate(
'xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDocPresent',
{
defaultMessage:
"This document couldn't be parsed correctly. Not all fields are properly populated",
}
);
export const degradedDocButtonLabelWhenNotPresent = i18n.translate(
'xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDocNotPresent',
{
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

@ -5,96 +5,10 @@
* 2.0.
*/
import React from 'react';
import { LogDocument } from '@kbn/discover-utils/src';
import type {
UnifiedDataTableProps,
RowControlComponent,
RowControlRowProps,
} from '@kbn/unified-data-table';
import {
actionsHeaderAriaLabelDegradedAction,
actionsHeaderAriaLabelStacktraceAction,
degradedDocButtonLabelWhenNotPresent,
degradedDocButtonLabelWhenPresent,
stacktraceAvailableControlButton,
stacktraceNotAvailableControlButton,
} from '../components/common/translations';
import * as constants from '../../common/constants';
import { getStacktraceFields } from '../utils/get_stack_trace';
const DegradedDocs = ({
Control,
rowProps: { record },
}: {
Control: RowControlComponent;
rowProps: RowControlRowProps;
}) => {
const isDegradedDocumentExists = constants.DEGRADED_DOCS_FIELD in record.raw;
return isDegradedDocumentExists ? (
<Control
data-test-subj="docTableDegradedDocExist"
color="danger"
label={degradedDocButtonLabelWhenPresent}
iconType="indexClose"
onClick={undefined}
/>
) : (
<Control
data-test-subj="docTableDegradedDocDoesNotExist"
color="text"
label={degradedDocButtonLabelWhenNotPresent}
iconType="indexClose"
onClick={undefined}
/>
);
};
const Stacktrace = ({
Control,
rowProps: { record },
}: {
Control: RowControlComponent;
rowProps: RowControlRowProps;
}) => {
const stacktrace = getStacktraceFields(record as LogDocument);
const hasValue = Object.values(stacktrace).some((value) => value);
return hasValue ? (
<Control
data-test-subj="docTableStacktraceExist"
label={stacktraceAvailableControlButton}
iconType="apmTrace"
onClick={undefined}
/>
) : (
<Control
disabled
data-test-subj="docTableStacktraceDoesNotExist"
label={stacktraceNotAvailableControlButton}
iconType="apmTrace"
onClick={undefined}
/>
);
};
import { createDegradedDocsControl, createStacktraceControl } from '@kbn/discover-utils';
import { type UnifiedDataTableProps } from '@kbn/unified-data-table';
export const getRowAdditionalControlColumns =
(): UnifiedDataTableProps['rowAdditionalLeadingControls'] => {
return [
{
id: 'connectedDegradedDocs',
headerAriaLabel: actionsHeaderAriaLabelDegradedAction,
renderControl: (Control, rowProps) => {
return <DegradedDocs Control={Control} rowProps={rowProps} />;
},
},
{
id: 'connectedStacktraceDocs',
headerAriaLabel: actionsHeaderAriaLabelStacktraceAction,
renderControl: (Control, rowProps) => {
return <Stacktrace Control={Control} rowProps={rowProps} />;
},
},
];
return [createDegradedDocsControl(), createStacktraceControl()];
};

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getFieldFromDoc, LogDocument, StackTraceFields } from '@kbn/discover-utils/src';
import * as constants from '../../common/constants';
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

@ -26216,10 +26216,6 @@
"xpack.logsExplorer.dataSourceSelector.sortOrders": "Sens de tri",
"xpack.logsExplorer.dataSourceSelector.TryEsql": "Langue : ES|QL",
"xpack.logsExplorer.dataSourceSelector.uncategorized": "Non catégorisé",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDocNotPresent": "Tous les champs de ce document ont été analysés correctement",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDocPresent": "Ce document n'a pas pu être analysé correctement. Tous les champs n'ont pas été remplis correctement",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available": "Traces d'appel disponibles",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "Traces d'appel indisponibles",
"xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1": "Affiche le {logLevel} du document et les champs {message}.",
"xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2": "Lorsque le champ de message est vide, l'une des informations suivantes s'affiche :",
"xpack.logsExplorer.dataTable.header.popover.content": "Contenu",

View file

@ -26205,10 +26205,6 @@
"xpack.logsExplorer.dataSourceSelector.sortOrders": "並べ替え方向",
"xpack.logsExplorer.dataSourceSelector.TryEsql": "言語ES|QL",
"xpack.logsExplorer.dataSourceSelector.uncategorized": "未分類",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDocNotPresent": "このドキュメントのすべてのフィールドは正しく解析されました",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDocPresent": "このドキュメントを正しく解析できませんでした。一部のフィールドが正しく入力されていません",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available": "スタックトレースがあります",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "スタックトレースがありません",
"xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1": "ドキュメントの{logLevel}と{message}フィールドを表示します。",
"xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2": "メッセージフィールドが空のときには、次のいずれかが表示されます。",
"xpack.logsExplorer.dataTable.header.popover.content": "コンテンツ",

View file

@ -26237,10 +26237,6 @@
"xpack.logsExplorer.dataSourceSelector.sortOrders": "排序方向",
"xpack.logsExplorer.dataSourceSelector.TryEsql": "语言ES|QL",
"xpack.logsExplorer.dataSourceSelector.uncategorized": "未分类",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDocNotPresent": "此文档中的所有字段均进行了正确解析",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.degradedDocPresent": "无法正确解析此文档。并非所有字段都进行了正确填充",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available": "堆栈跟踪可用",
"xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "堆栈跟踪不可用",
"xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1": "显示该文档的 {logLevel} 和 {message} 字段。",
"xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2": "消息字段为空时,将显示以下项之一:",
"xpack.logsExplorer.dataTable.header.popover.content": "内容",

View file

@ -204,6 +204,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await dataGrid.getHeaders()).to.eql([
'Select column',
'Control column',
'Access to degraded docs',
'Access to available stacktraces',
'Numberbytes',
'machine.ram_range',
]);

View file

@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return text === 'Sep 22, 2015 @ 23:50:13.253';
});
await dataGrid.clickCellExpandButton(0, 3);
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 1);
let expandDocId = '';
await retry.waitForWithTimeout('expandDocId to be valid', 5000, async () => {
@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug(`row document timestamp: ${text}`);
return text === 'Sep 22, 2015 @ 23:50:13.253';
});
await dataGrid.clickCellExpandButton(0, 3);
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 1);
let expandDocId = '';
await retry.waitForWithTimeout('expandDocId to be valid', 5000, async () => {