mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
Remaining work attributes table (#224723)
Closes https://github.com/elastic/kibana/issues/221928 #### Add ES|QL logic https://github.com/user-attachments/assets/d29f939a-7b82-4873-92d4-8210c2202339 #### Empty message for accordion - Empty message when there are no attributes fields at all - For now we kept the accordion closed when fields count is zero, with an empty message inside, waiting for UI/UX team to review this implementation <img width="524" alt="Screenshot 2025-06-24 at 12 27 18" src="https://github.com/user-attachments/assets/4015ed6a-5977-486d-93e6-d8b5714af9fd" /> #### Simplify attribute display names - the field name should not show the full field name. The tooltip will show both, simplify and full name, this is part of the implementation `FieldName` component from platform <img width="624" alt="Screenshot 2025-06-24 at 12 19 48" src="https://github.com/user-attachments/assets/634b4ef0-0934-4721-9217-334286b6464a" /> <img width="624" alt="Screenshot 2025-06-24 at 12 20 07" src="https://github.com/user-attachments/assets/bdc6de9c-784f-4c78-bf18-1f37b645429d" /> #### Filtering controls use full field name https://github.com/user-attachments/assets/7858d803-271e-4913-9aae-385dd7bc9e25 #### Add explanatory tooltip for attribute namespaces <img width="525" alt="Screenshot 2025-06-24 at 12 24 33" src="https://github.com/user-attachments/assets/a76b1419-c1d9-4e46-a289-a819b7533b18" /> <img width="525" alt="Screenshot 2025-06-24 at 12 24 51" src="https://github.com/user-attachments/assets/e48b19a3-85a8-4a13-b527-3a4494aef2af" /> <img width="525" alt="Screenshot 2025-06-24 at 12 24 57" src="https://github.com/user-attachments/assets/50501672-4d75-43ce-b61b-646108b4b14a" /> ### Test: #### How to generate OTel data - Follow https://github.com/smith/elastic-stack-docker-compose?tab=readme-ov-file#elastic-stack-docker-compose #### How to test - Make sure your solution view is Observability - update your `kibana.yml` ``` discover.experimental.enabledProfiles: - observability-root-profile-with-attributes-tab # if you want to test it with the additional profiles add the following to your `kibana.yaml` - observability-traces-data-source-profile - observability-traces-transaction-document-profile - observability-traces-span-document-profile ```
This commit is contained in:
parent
d532ff490a
commit
75ba373fbd
14 changed files with 454 additions and 108 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1391,6 +1391,7 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/
|
|||
/x-pack/test/api_integration/services/infraops_source_configuration.ts @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team # Assigned per https://github.com/elastic/kibana/pull/34916
|
||||
/x-pack/solutions/observability/plugins/observability/public/pages/overview @elastic/obs-ux-infra_services-team # Assigned to this team since it mostly uses infra/APM components
|
||||
/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces @elastic/obs-ux-infra_services-team
|
||||
/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/attributes @elastic/obs-ux-infra_services-team
|
||||
|
||||
## Logs UI code exceptions -> @elastic/obs-ux-logs-team
|
||||
/x-pack/test/api_integration/deployment_agnostic/apis/observability/data_quality/ @elastic/obs-ux-logs-team
|
||||
|
|
|
@ -19,6 +19,7 @@ import { getFieldTypeName } from '@kbn/field-utils';
|
|||
|
||||
interface Props {
|
||||
fieldName: string;
|
||||
displayNameOverride?: string;
|
||||
fieldType?: string;
|
||||
fieldMapping?: DataViewField;
|
||||
fieldIconProps?: Omit<FieldIconProps, 'type'>;
|
||||
|
@ -31,13 +32,15 @@ export function FieldName({
|
|||
fieldMapping,
|
||||
fieldType,
|
||||
fieldIconProps,
|
||||
displayNameOverride,
|
||||
scripted = false,
|
||||
highlight = '',
|
||||
}: Props) {
|
||||
const typeName = getFieldTypeName(fieldType);
|
||||
const displayName =
|
||||
fieldMapping && fieldMapping.displayName ? fieldMapping.displayName : fieldName;
|
||||
const tooltip = displayName !== fieldName ? `${displayName} (${fieldName})` : fieldName;
|
||||
const fieldMappingDisplayName = fieldMapping?.displayName ? fieldMapping.displayName : fieldName;
|
||||
const fieldDisplayName = displayNameOverride ?? fieldMappingDisplayName;
|
||||
|
||||
const tooltip = fieldDisplayName !== fieldName ? `${fieldDisplayName} (${fieldName})` : fieldName;
|
||||
const subTypeMulti = fieldMapping && getDataViewFieldSubtypeMulti(fieldMapping.spec);
|
||||
const isMultiField = !!subTypeMulti?.multi;
|
||||
|
||||
|
@ -71,7 +74,7 @@ export function FieldName({
|
|||
delay="long"
|
||||
anchorClassName="eui-textBreakAll"
|
||||
>
|
||||
<EuiHighlight search={highlight}>{displayName}</EuiHighlight>
|
||||
<EuiHighlight search={highlight}>{fieldDisplayName}</EuiHighlight>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ Array [
|
|||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
"displayNameOverride": undefined,
|
||||
"flattenedValue": "test",
|
||||
"isPinned": false,
|
||||
"name": "message",
|
||||
|
@ -45,6 +46,7 @@ Array [
|
|||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
"displayNameOverride": undefined,
|
||||
"flattenedValue": "test",
|
||||
"isPinned": false,
|
||||
"name": "message",
|
||||
|
@ -66,6 +68,7 @@ Array [
|
|||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
"displayNameOverride": undefined,
|
||||
"flattenedValue": "test",
|
||||
"isPinned": false,
|
||||
"name": "message",
|
||||
|
@ -90,6 +93,7 @@ Array [
|
|||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
"displayNameOverride": undefined,
|
||||
"flattenedValue": "test",
|
||||
"isPinned": false,
|
||||
"name": "message",
|
||||
|
@ -128,6 +132,7 @@ Array [
|
|||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
"displayNameOverride": undefined,
|
||||
"flattenedValue": "test",
|
||||
"isPinned": false,
|
||||
"name": "message",
|
||||
|
@ -149,6 +154,7 @@ Array [
|
|||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
"displayNameOverride": undefined,
|
||||
"flattenedValue": "test",
|
||||
"isPinned": false,
|
||||
"name": "message",
|
||||
|
@ -168,6 +174,7 @@ Array [
|
|||
"scripted": false,
|
||||
"type": "string",
|
||||
},
|
||||
"displayNameOverride": undefined,
|
||||
"flattenedValue": "test",
|
||||
"isPinned": false,
|
||||
"name": "message",
|
||||
|
|
|
@ -22,6 +22,7 @@ import { getDataViewFieldOrCreateFromColumnMeta } from '@kbn/data-view-utils';
|
|||
|
||||
export class FieldRow {
|
||||
readonly name: string;
|
||||
readonly displayNameOverride: string | undefined;
|
||||
readonly flattenedValue: unknown;
|
||||
readonly dataViewField: DataViewField | undefined;
|
||||
readonly isPinned: boolean;
|
||||
|
@ -41,6 +42,7 @@ export class FieldRow {
|
|||
|
||||
constructor({
|
||||
name,
|
||||
displayNameOverride,
|
||||
flattenedValue,
|
||||
hit,
|
||||
dataView,
|
||||
|
@ -49,6 +51,7 @@ export class FieldRow {
|
|||
columnsMeta,
|
||||
}: {
|
||||
name: string;
|
||||
displayNameOverride?: string;
|
||||
flattenedValue: unknown;
|
||||
hit: DataTableRecord;
|
||||
dataView: DataView;
|
||||
|
@ -63,6 +66,7 @@ export class FieldRow {
|
|||
this.#isFormattedAsText = false;
|
||||
|
||||
this.name = name;
|
||||
this.displayNameOverride = displayNameOverride;
|
||||
this.flattenedValue = flattenedValue;
|
||||
this.dataViewField = getDataViewFieldOrCreateFromColumnMeta({
|
||||
dataView,
|
||||
|
|
|
@ -73,7 +73,7 @@ const PAGE_SIZE_OPTIONS = [25, 50, 100, 250, 500];
|
|||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const PINNED_FIELDS_KEY = 'discover:pinnedFields';
|
||||
const PAGE_SIZE = 'discover:pageSize';
|
||||
const HIDE_NULL_VALUES = 'unifiedDocViewer:hideNullValues';
|
||||
export const HIDE_NULL_VALUES = 'unifiedDocViewer:hideNullValues';
|
||||
export const SHOW_ONLY_SELECTED_FIELDS = 'unifiedDocViewer:showOnlySelectedFields';
|
||||
|
||||
const GRID_COLUMN_FIELD_NAME = 'name';
|
||||
|
|
|
@ -49,13 +49,15 @@ export const TableCell: React.FC<TableCellProps> = React.memo(
|
|||
return null;
|
||||
}
|
||||
|
||||
const { flattenedValue, name, dataViewField, ignoredReason, fieldType } = row;
|
||||
const { flattenedValue, name, displayNameOverride, dataViewField, ignoredReason, fieldType } =
|
||||
row;
|
||||
|
||||
if (columnId === 'name') {
|
||||
return (
|
||||
<div>
|
||||
<FieldName
|
||||
fieldName={name}
|
||||
displayNameOverride={displayNameOverride}
|
||||
fieldType={fieldType}
|
||||
fieldMapping={dataViewField}
|
||||
scripted={dataViewField?.scripted}
|
||||
|
|
|
@ -7,17 +7,20 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiAccordion, EuiText, EuiNotificationBadge } from '@elastic/eui';
|
||||
import { EuiAccordion, EuiText, EuiNotificationBadge, EuiIconTip, useEuiTheme } from '@elastic/eui';
|
||||
import { DataTableColumnsMeta, DataTableRecord } from '@kbn/discover-utils';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import { AttributesTable } from './attributes_table';
|
||||
import { AttributesEmptyPrompt } from './attributes_empty_prompt';
|
||||
import { AttributeField } from './attributes_overview';
|
||||
|
||||
interface AttributesAccordionProps {
|
||||
id: string;
|
||||
title: string;
|
||||
ariaLabel: string;
|
||||
fields: string[];
|
||||
tooltipMessage: string;
|
||||
fields: AttributeField[];
|
||||
hit: DataTableRecord;
|
||||
dataView: DataView;
|
||||
columns?: string[];
|
||||
|
@ -26,12 +29,13 @@ interface AttributesAccordionProps {
|
|||
onAddColumn?: (col: string) => void;
|
||||
onRemoveColumn?: (col: string) => void;
|
||||
filter?: DocViewFilterFn;
|
||||
isEsqlMode: boolean;
|
||||
}
|
||||
|
||||
export const AttributesAccordion = ({
|
||||
id,
|
||||
title,
|
||||
ariaLabel,
|
||||
tooltipMessage,
|
||||
fields,
|
||||
hit,
|
||||
dataView,
|
||||
|
@ -41,34 +45,51 @@ export const AttributesAccordion = ({
|
|||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
filter,
|
||||
}: AttributesAccordionProps) => (
|
||||
<EuiAccordion
|
||||
id={id}
|
||||
buttonContent={
|
||||
<EuiText size="s">
|
||||
<strong aria-label={ariaLabel}>{title}</strong>
|
||||
</EuiText>
|
||||
}
|
||||
initialIsOpen={fields.length > 0}
|
||||
forceState={fields.length === 0 ? 'closed' : undefined}
|
||||
isDisabled={fields.length === 0}
|
||||
extraAction={
|
||||
<EuiNotificationBadge size="m" color="subdued">
|
||||
{fields.length}
|
||||
</EuiNotificationBadge>
|
||||
}
|
||||
paddingSize="m"
|
||||
>
|
||||
<AttributesTable
|
||||
hit={hit}
|
||||
dataView={dataView}
|
||||
columns={columns}
|
||||
columnsMeta={columnsMeta}
|
||||
fields={fields}
|
||||
searchTerm={searchTerm}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
filter={filter}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
);
|
||||
isEsqlMode = false,
|
||||
}: AttributesAccordionProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<EuiAccordion
|
||||
id={id}
|
||||
buttonContent={
|
||||
<EuiText size="s">
|
||||
<strong css={{ marginRight: euiTheme.size.xs }}>{title}</strong>
|
||||
<EuiIconTip
|
||||
aria-label={tooltipMessage}
|
||||
type="questionInCircle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
content={tooltipMessage}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
initialIsOpen={fields.length > 0}
|
||||
extraAction={
|
||||
<EuiNotificationBadge size="m" color="subdued">
|
||||
{fields.length}
|
||||
</EuiNotificationBadge>
|
||||
}
|
||||
paddingSize="m"
|
||||
>
|
||||
{fields.length === 0 ? (
|
||||
<AttributesEmptyPrompt />
|
||||
) : (
|
||||
<AttributesTable
|
||||
hit={hit}
|
||||
dataView={dataView}
|
||||
columns={columns}
|
||||
columnsMeta={columnsMeta}
|
||||
fields={fields}
|
||||
searchTerm={searchTerm}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
filter={filter}
|
||||
isEsqlMode={isEsqlMode}
|
||||
/>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", 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 React from 'react';
|
||||
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AttributesEmptyPrompt = () => (
|
||||
<EuiEmptyPrompt
|
||||
layout="horizontal"
|
||||
body={
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'unifiedDocViewer.docView.attributes.accordion.noFieldsMessage.noFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'No attributes fields found.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<>
|
||||
<strong>
|
||||
{i18n.translate(
|
||||
'unifiedDocViewer.docView.attributes.accordion.noFieldsMessage.tryText',
|
||||
{
|
||||
defaultMessage: 'Try:',
|
||||
}
|
||||
)}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>
|
||||
{i18n.translate(
|
||||
'unifiedDocViewer.docView.attributes.accordion.noFieldsMessage.fieldTypeFilterBullet',
|
||||
{
|
||||
defaultMessage: 'Using different field filters if applicable.',
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
</EuiText>
|
||||
}
|
||||
data-test-subj="attributesAccordionEmptyPrompt"
|
||||
/>
|
||||
);
|
|
@ -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<HTMLDivElement | null>(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 (
|
||||
<EuiFlexGroup
|
||||
ref={setContainerRef}
|
||||
|
@ -149,37 +193,71 @@ export function AttributesOverview({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
wrap
|
||||
direction="row"
|
||||
justifyContent="flexEnd"
|
||||
alignItems="center"
|
||||
gutterSize="m"
|
||||
>
|
||||
{isEsqlMode && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('unifiedDocViewer.hideNullValues.switchLabel', {
|
||||
defaultMessage: 'Hide null fields',
|
||||
description: 'Switch label to hide fields with null values in the table',
|
||||
})}
|
||||
checked={areNullValuesHidden ?? false}
|
||||
onChange={onHideNullValuesChange}
|
||||
compressed
|
||||
data-test-subj="unifiedDocViewerHideNullValuesSwitch"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
css={css`
|
||||
overflow: auto;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
{accordionConfigs.map(({ id, title, ariaLabel, fields }) => (
|
||||
<React.Fragment key={id}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AttributesAccordion
|
||||
id={id}
|
||||
title={title}
|
||||
ariaLabel={ariaLabel}
|
||||
fields={fields}
|
||||
hit={hit}
|
||||
dataView={dataView}
|
||||
columns={columns}
|
||||
columnsMeta={columnsMeta}
|
||||
searchTerm={searchTerm}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
filter={filter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
{noFields ? (
|
||||
<AttributesEmptyPrompt />
|
||||
) : (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
{accordionConfigs.map(({ id, title, ariaLabel, fields, tooltipMessage }) => (
|
||||
<React.Fragment key={id}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AttributesAccordion
|
||||
id={id}
|
||||
title={title}
|
||||
ariaLabel={ariaLabel}
|
||||
tooltipMessage={tooltipMessage}
|
||||
fields={fields}
|
||||
hit={hit}
|
||||
dataView={dataView}
|
||||
columns={columns}
|
||||
columnsMeta={columnsMeta}
|
||||
searchTerm={searchTerm}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
filter={filter}
|
||||
isEsqlMode={isEsqlMode}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -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'] = [
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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\./, '');
|
||||
}
|
|
@ -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' }]);
|
||||
});
|
||||
});
|
|
@ -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<string, unknown>;
|
||||
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 };
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue