+
+ {i18n.translate(
+ 'unifiedDocViewer.docView.attributes.accordion.noFieldsMessage.noFieldsLabel',
+ {
+ defaultMessage: 'No attributes fields found.',
+ }
+ )}
+
+ <>
+
+ {i18n.translate(
+ 'unifiedDocViewer.docView.attributes.accordion.noFieldsMessage.tryText',
+ {
+ defaultMessage: 'Try:',
+ }
+ )}
+
+
+ -
+ {i18n.translate(
+ 'unifiedDocViewer.docView.attributes.accordion.noFieldsMessage.fieldTypeFilterBullet',
+ {
+ defaultMessage: 'Using different field filters if applicable.',
+ }
+ )}
+
+
+ >
+
+ }
+ data-test-subj="attributesAccordionEmptyPrompt"
+ />
+);
diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/attributes_overview.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/attributes_overview.tsx
index 3205e6aadf25..e00130092f0a 100644
--- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/attributes_overview.tsx
+++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/attributes_overview.tsx
@@ -6,12 +6,20 @@
* 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 React, { useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
-import { EuiSpacer, EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import {
+ EuiSpacer,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSwitch,
+ EuiSwitchEvent,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { SHOW_MULTIFIELDS, getShouldShowFieldHandler } from '@kbn/discover-utils';
+import useLocalStorage from 'react-use/lib/useLocalStorage';
import {
LOCAL_STORAGE_KEY_SEARCH_TERM,
useTableFilters,
@@ -23,12 +31,21 @@ import {
} from '../../../doc_viewer_source/get_height';
import { AttributesAccordion } from './attributes_accordion';
import { getAttributesTitle } from './get_attributes_title';
+import { HIDE_NULL_VALUES } from '../../../doc_viewer_table/table';
+import { AttributesEmptyPrompt } from './attributes_empty_prompt';
+import { groupAttributesFields } from './group_attributes_fields';
+
+export interface AttributeField {
+ name: string; // full field name for filtering/actions
+ displayName: string; // stripped prefix for UI display
+}
export function AttributesOverview({
columns,
columnsMeta,
hit,
dataView,
+ textBasedHits,
filter,
decreaseAvailableHeightBy,
onAddColumn,
@@ -36,11 +53,13 @@ export function AttributesOverview({
}: DocViewRenderProps) {
const [containerRef, setContainerRef] = useState(null);
const { storage, uiSettings } = getUnifiedDocViewerServices();
+ const isEsqlMode = Array.isArray(textBasedHits);
const showMultiFields = uiSettings.get(SHOW_MULTIFIELDS);
const { searchTerm, onChangeSearchTerm } = useTableFilters({
storage,
storageKey: LOCAL_STORAGE_KEY_SEARCH_TERM,
});
+ const [areNullValuesHidden, setAreNullValuesHidden] = useLocalStorage(HIDE_NULL_VALUES, false);
const flattened = hit.flattened;
@@ -53,43 +72,45 @@ export function AttributesOverview({
const allFields = Object.keys(flattened);
- // it filters out multifields that have a parent, to prevent entries for multifields like this: field, field.keyword, field.whatever
- const filteredFields = useMemo(
- () => allFields.filter(shouldShowFieldHandler),
- [allFields, shouldShowFieldHandler]
+ const groupedFields = useMemo(
+ () =>
+ groupAttributesFields({
+ allFields,
+ flattened,
+ searchTerm,
+ shouldShowFieldHandler,
+ isEsqlMode,
+ areNullValuesHidden,
+ }),
+ [allFields, flattened, searchTerm, shouldShowFieldHandler, isEsqlMode, areNullValuesHidden]
);
- const groupedFields = useMemo(() => {
- const attributesFields: string[] = [];
- const resourceAttributesFields: string[] = [];
- const scopeAttributesFields: string[] = [];
- const lowerSearchTerm = searchTerm.toLowerCase();
- for (const fieldName of filteredFields) {
- const lowerFieldName = fieldName.toLowerCase();
- if (!lowerFieldName.includes(lowerSearchTerm)) {
- continue;
- }
- if (lowerFieldName.startsWith('resource.attributes.')) {
- resourceAttributesFields.push(fieldName);
- } else if (lowerFieldName.startsWith('scope.attributes.')) {
- scopeAttributesFields.push(fieldName);
- } else if (lowerFieldName.startsWith('attributes.')) {
- attributesFields.push(fieldName);
- }
- }
- return { attributesFields, resourceAttributesFields, scopeAttributesFields };
- }, [filteredFields, searchTerm]);
+ const { attributesFields, resourceAttributesFields, scopeAttributesFields } = groupedFields;
const containerHeight = containerRef
? getTabContentAvailableHeight(containerRef, decreaseAvailableHeightBy ?? DEFAULT_MARGIN_BOTTOM)
: 0;
+ const filterFieldsBySearchTerm = (fields: AttributeField[]) =>
+ fields.filter(
+ (field) =>
+ field.displayName.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ field.name.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
const accordionConfigs = [
{
id: 'signal_attributes',
title: attributesTitle,
ariaLabel: attributesTitle,
- fields: groupedFields.attributesFields,
+ fields: filterFieldsBySearchTerm(attributesFields),
+ tooltipMessage: i18n.translate(
+ 'unifiedDocViewer.docView.attributes.signalAttributesTooltip',
+ {
+ defaultMessage:
+ 'Metadata added by the instrumentation library to describe the telemetry data (e.g., HTTP method, user agent).',
+ }
+ ),
},
{
id: 'resource_attributes',
@@ -100,7 +121,14 @@ export function AttributesOverview({
'unifiedDocViewer.docView.attributes.resourceAttributesTitle.ariaLabel',
{ defaultMessage: 'Resource attributes' }
),
- fields: groupedFields.resourceAttributesFields,
+ fields: filterFieldsBySearchTerm(resourceAttributesFields),
+ tooltipMessage: i18n.translate(
+ 'unifiedDocViewer.docView.attributes.resourceAttributesTooltip',
+ {
+ defaultMessage:
+ 'Metadata originally set at the source of the telemetry, such as in the SDK or agent that generated the data.',
+ }
+ ),
},
{
id: 'scope_attributes',
@@ -111,10 +139,26 @@ export function AttributesOverview({
'unifiedDocViewer.docView.attributes.scopeAttributesTitle.ariaLabel',
{ defaultMessage: 'Scope attributes' }
),
- fields: groupedFields.scopeAttributesFields,
+ fields: filterFieldsBySearchTerm(scopeAttributesFields),
+ tooltipMessage: i18n.translate('unifiedDocViewer.docView.attributes.scopeAttributesTooltip', {
+ defaultMessage:
+ 'Metadata associated with the instrumentation scope (i.e., the library/module that produced the telemetry), helping identify its origin and version.',
+ }),
},
];
+ const onHideNullValuesChange = useCallback(
+ (e: EuiSwitchEvent) => {
+ setAreNullValuesHidden(e.target.checked);
+ },
+ [setAreNullValuesHidden]
+ );
+
+ const noFields =
+ attributesFields.length === 0 &&
+ resourceAttributesFields.length === 0 &&
+ scopeAttributesFields.length === 0;
+
return (
+
+
+ {isEsqlMode && (
+
+
+
+ )}
+
+
+
+
+
-
- {accordionConfigs.map(({ id, title, ariaLabel, fields }) => (
-
-
-
-
-
-
-
-
- ))}
-
+ {noFields ? (
+
+ ) : (
+
+ {accordionConfigs.map(({ id, title, ariaLabel, fields, tooltipMessage }) => (
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
);
diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/attributes_table.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/attributes_table.tsx
index fecb29c80f7d..f4a8440166a3 100644
--- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/attributes_table.tsx
+++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/attributes_table.tsx
@@ -18,14 +18,16 @@ import {
} from '../../../doc_viewer_table/table_cell_actions';
import { FieldRow } from '../../../doc_viewer_table/field_row';
import { getUnifiedDocViewerServices } from '../../../../plugin';
+import { AttributeField } from './attributes_overview';
interface AttributesTableProps
extends Pick<
DocViewRenderProps,
'hit' | 'dataView' | 'columnsMeta' | 'filter' | 'onAddColumn' | 'onRemoveColumn' | 'columns'
> {
- fields: string[];
+ fields: AttributeField[];
searchTerm: string;
+ isEsqlMode: boolean;
}
export const AttributesTable = ({
@@ -38,6 +40,7 @@ export const AttributesTable = ({
filter,
onAddColumn,
onRemoveColumn,
+ isEsqlMode,
}: AttributesTableProps) => {
const flattened = hit.flattened;
const { fieldFormats, toasts } = getUnifiedDocViewerServices();
@@ -55,18 +58,14 @@ export const AttributesTable = ({
};
}, [onRemoveColumn, onAddColumn, columns]);
- const displayedFields = useMemo(
- () => fields.filter((field) => field.toLowerCase().includes(searchTerm.toLowerCase())),
- [fields, searchTerm]
- );
-
const rows: FieldRow[] = useMemo(
() =>
- displayedFields.map(
+ fields.map(
(field) =>
new FieldRow({
- name: field,
- flattenedValue: flattened[field],
+ name: field.name,
+ displayNameOverride: field.displayName,
+ flattenedValue: flattened[field.name],
hit,
dataView,
fieldFormats,
@@ -74,16 +73,16 @@ export const AttributesTable = ({
columnsMeta,
})
),
- [displayedFields, flattened, hit, dataView, fieldFormats, columnsMeta]
+ [fields, flattened, hit, dataView, fieldFormats, columnsMeta]
);
const fieldCellActions = useMemo(
- () => getFieldCellActions({ rows, isEsqlMode: false, onFilter: filter, onToggleColumn }),
- [rows, filter, onToggleColumn]
+ () => getFieldCellActions({ rows, isEsqlMode, onFilter: filter, onToggleColumn }),
+ [rows, filter, onToggleColumn, isEsqlMode]
);
const fieldValueCellActions = useMemo(
- () => getFieldValueCellActions({ rows, isEsqlMode: false, toasts, onFilter: filter }),
- [rows, toasts, filter]
+ () => getFieldValueCellActions({ rows, isEsqlMode, toasts, onFilter: filter }),
+ [rows, toasts, filter, isEsqlMode]
);
const gridColumns: EuiDataGridProps['columns'] = [
diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/get_attribute_display_name.test.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/get_attribute_display_name.test.ts
new file mode 100644
index 000000000000..213350360575
--- /dev/null
+++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/get_attribute_display_name.test.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { getAttributeDisplayName } from './get_attribute_display_name';
+
+describe('getAttributeDisplayName', () => {
+ it('removes "attributes." prefix', () => {
+ expect(getAttributeDisplayName('attributes.foo')).toBe('foo');
+ });
+
+ it('removes "resource.attributes." prefix', () => {
+ expect(getAttributeDisplayName('resource.attributes.env')).toBe('env');
+ });
+
+ it('removes "scope.attributes." prefix', () => {
+ expect(getAttributeDisplayName('scope.attributes.lib')).toBe('lib');
+ });
+
+ it('returns the original name if no prefix matches', () => {
+ expect(getAttributeDisplayName('other')).toBe('other');
+ expect(getAttributeDisplayName('attributesfoo')).toBe('attributesfoo');
+ });
+});
diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/get_attribute_display_name.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/get_attribute_display_name.ts
new file mode 100644
index 000000000000..7f05c9b09346
--- /dev/null
+++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/get_attribute_display_name.ts
@@ -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", 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 function getAttributeDisplayName(fieldName: string): string {
+ return fieldName
+ .replace(/^resource\.attributes\./, '')
+ .replace(/^scope\.attributes\./, '')
+ .replace(/^attributes\./, '');
+}
diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/group_attributes_fields.test.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/group_attributes_fields.test.ts
new file mode 100644
index 000000000000..5933a01ddd8d
--- /dev/null
+++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/group_attributes_fields.test.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { groupAttributesFields } from './group_attributes_fields';
+
+describe('groupAttributesFields', () => {
+ const flattened = {
+ 'attributes.foo': 'bar',
+ 'resource.attributes.env': 'prod',
+ 'scope.attributes.lib': 'lib1',
+ other: 'value',
+ };
+
+ const allFields = Object.keys(flattened);
+
+ const alwaysShow = () => true;
+
+ it('groups fields correctly', () => {
+ const result = groupAttributesFields({
+ allFields,
+ flattened,
+ searchTerm: '',
+ shouldShowFieldHandler: alwaysShow,
+ isEsqlMode: false,
+ areNullValuesHidden: false,
+ });
+
+ expect(result.attributesFields).toEqual([{ name: 'attributes.foo', displayName: 'foo' }]);
+ expect(result.resourceAttributesFields).toEqual([
+ { name: 'resource.attributes.env', displayName: 'env' },
+ ]);
+ expect(result.scopeAttributesFields).toEqual([
+ { name: 'scope.attributes.lib', displayName: 'lib' },
+ ]);
+ });
+
+ it('filters by searchTerm', () => {
+ const result = groupAttributesFields({
+ allFields,
+ flattened,
+ searchTerm: 'env',
+ shouldShowFieldHandler: alwaysShow,
+ isEsqlMode: false,
+ areNullValuesHidden: false,
+ });
+
+ expect(result.resourceAttributesFields).toEqual([
+ { name: 'resource.attributes.env', displayName: 'env' },
+ ]);
+ expect(result.attributesFields).toEqual([]);
+ expect(result.scopeAttributesFields).toEqual([]);
+ });
+
+ it('filters out null values in ES|QL mode', () => {
+ const flattenedWithNull = { ...flattened, 'attributes.null': null };
+ const allFieldsWithNull = Object.keys(flattenedWithNull);
+
+ const result = groupAttributesFields({
+ allFields: allFieldsWithNull,
+ flattened: flattenedWithNull,
+ searchTerm: '',
+ shouldShowFieldHandler: alwaysShow,
+ isEsqlMode: true,
+ areNullValuesHidden: true,
+ });
+
+ expect(result.attributesFields).toEqual([{ name: 'attributes.foo', displayName: 'foo' }]);
+ });
+});
diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/group_attributes_fields.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/group_attributes_fields.ts
new file mode 100644
index 000000000000..1cdb54c2c8cc
--- /dev/null
+++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes/doc_viewer_attributes_overview/group_attributes_fields.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { AttributeField } from './attributes_overview';
+import { getAttributeDisplayName } from './get_attribute_display_name';
+
+interface GroupAttributesFieldsParams {
+ allFields: string[];
+ flattened: Record;
+ searchTerm: string;
+ shouldShowFieldHandler: (fieldName: string) => boolean;
+ isEsqlMode: boolean;
+ areNullValuesHidden?: boolean;
+}
+
+export function groupAttributesFields({
+ allFields,
+ flattened,
+ searchTerm,
+ shouldShowFieldHandler,
+ isEsqlMode,
+ areNullValuesHidden,
+}: GroupAttributesFieldsParams): {
+ attributesFields: AttributeField[];
+ resourceAttributesFields: AttributeField[];
+ scopeAttributesFields: AttributeField[];
+} {
+ const attributesFields: AttributeField[] = [];
+ const resourceAttributesFields: AttributeField[] = [];
+ const scopeAttributesFields: AttributeField[] = [];
+ const lowerSearchTerm = searchTerm.toLowerCase();
+
+ allFields.forEach((fieldName) => {
+ const lowerFieldName = fieldName.toLowerCase();
+
+ if (!shouldShowFieldHandler(fieldName)) return;
+ if (!lowerFieldName.includes(lowerSearchTerm)) return;
+ if (isEsqlMode && areNullValuesHidden && flattened[fieldName] == null) return;
+
+ const field: AttributeField = {
+ name: fieldName,
+ displayName: getAttributeDisplayName(fieldName),
+ };
+
+ if (lowerFieldName.startsWith('resource.attributes.')) {
+ resourceAttributesFields.push(field);
+ } else if (lowerFieldName.startsWith('scope.attributes.')) {
+ scopeAttributesFields.push(field);
+ } else if (lowerFieldName.startsWith('attributes.')) {
+ attributesFields.push(field);
+ }
+ });
+
+ return { attributesFields, resourceAttributesFields, scopeAttributesFields };
+}