mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51: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/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
|
/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/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
|
## 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
|
/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 {
|
interface Props {
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
displayNameOverride?: string;
|
||||||
fieldType?: string;
|
fieldType?: string;
|
||||||
fieldMapping?: DataViewField;
|
fieldMapping?: DataViewField;
|
||||||
fieldIconProps?: Omit<FieldIconProps, 'type'>;
|
fieldIconProps?: Omit<FieldIconProps, 'type'>;
|
||||||
|
@ -31,13 +32,15 @@ export function FieldName({
|
||||||
fieldMapping,
|
fieldMapping,
|
||||||
fieldType,
|
fieldType,
|
||||||
fieldIconProps,
|
fieldIconProps,
|
||||||
|
displayNameOverride,
|
||||||
scripted = false,
|
scripted = false,
|
||||||
highlight = '',
|
highlight = '',
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const typeName = getFieldTypeName(fieldType);
|
const typeName = getFieldTypeName(fieldType);
|
||||||
const displayName =
|
const fieldMappingDisplayName = fieldMapping?.displayName ? fieldMapping.displayName : fieldName;
|
||||||
fieldMapping && fieldMapping.displayName ? fieldMapping.displayName : fieldName;
|
const fieldDisplayName = displayNameOverride ?? fieldMappingDisplayName;
|
||||||
const tooltip = displayName !== fieldName ? `${displayName} (${fieldName})` : fieldName;
|
|
||||||
|
const tooltip = fieldDisplayName !== fieldName ? `${fieldDisplayName} (${fieldName})` : fieldName;
|
||||||
const subTypeMulti = fieldMapping && getDataViewFieldSubtypeMulti(fieldMapping.spec);
|
const subTypeMulti = fieldMapping && getDataViewFieldSubtypeMulti(fieldMapping.spec);
|
||||||
const isMultiField = !!subTypeMulti?.multi;
|
const isMultiField = !!subTypeMulti?.multi;
|
||||||
|
|
||||||
|
@ -71,7 +74,7 @@ export function FieldName({
|
||||||
delay="long"
|
delay="long"
|
||||||
anchorClassName="eui-textBreakAll"
|
anchorClassName="eui-textBreakAll"
|
||||||
>
|
>
|
||||||
<EuiHighlight search={highlight}>{displayName}</EuiHighlight>
|
<EuiHighlight search={highlight}>{fieldDisplayName}</EuiHighlight>
|
||||||
</EuiToolTip>
|
</EuiToolTip>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ Array [
|
||||||
"scripted": false,
|
"scripted": false,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"displayNameOverride": undefined,
|
||||||
"flattenedValue": "test",
|
"flattenedValue": "test",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"name": "message",
|
"name": "message",
|
||||||
|
@ -45,6 +46,7 @@ Array [
|
||||||
"scripted": false,
|
"scripted": false,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"displayNameOverride": undefined,
|
||||||
"flattenedValue": "test",
|
"flattenedValue": "test",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"name": "message",
|
"name": "message",
|
||||||
|
@ -66,6 +68,7 @@ Array [
|
||||||
"scripted": false,
|
"scripted": false,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"displayNameOverride": undefined,
|
||||||
"flattenedValue": "test",
|
"flattenedValue": "test",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"name": "message",
|
"name": "message",
|
||||||
|
@ -90,6 +93,7 @@ Array [
|
||||||
"scripted": false,
|
"scripted": false,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"displayNameOverride": undefined,
|
||||||
"flattenedValue": "test",
|
"flattenedValue": "test",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"name": "message",
|
"name": "message",
|
||||||
|
@ -128,6 +132,7 @@ Array [
|
||||||
"scripted": false,
|
"scripted": false,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"displayNameOverride": undefined,
|
||||||
"flattenedValue": "test",
|
"flattenedValue": "test",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"name": "message",
|
"name": "message",
|
||||||
|
@ -149,6 +154,7 @@ Array [
|
||||||
"scripted": false,
|
"scripted": false,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"displayNameOverride": undefined,
|
||||||
"flattenedValue": "test",
|
"flattenedValue": "test",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"name": "message",
|
"name": "message",
|
||||||
|
@ -168,6 +174,7 @@ Array [
|
||||||
"scripted": false,
|
"scripted": false,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"displayNameOverride": undefined,
|
||||||
"flattenedValue": "test",
|
"flattenedValue": "test",
|
||||||
"isPinned": false,
|
"isPinned": false,
|
||||||
"name": "message",
|
"name": "message",
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { getDataViewFieldOrCreateFromColumnMeta } from '@kbn/data-view-utils';
|
||||||
|
|
||||||
export class FieldRow {
|
export class FieldRow {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
|
readonly displayNameOverride: string | undefined;
|
||||||
readonly flattenedValue: unknown;
|
readonly flattenedValue: unknown;
|
||||||
readonly dataViewField: DataViewField | undefined;
|
readonly dataViewField: DataViewField | undefined;
|
||||||
readonly isPinned: boolean;
|
readonly isPinned: boolean;
|
||||||
|
@ -41,6 +42,7 @@ export class FieldRow {
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
name,
|
name,
|
||||||
|
displayNameOverride,
|
||||||
flattenedValue,
|
flattenedValue,
|
||||||
hit,
|
hit,
|
||||||
dataView,
|
dataView,
|
||||||
|
@ -49,6 +51,7 @@ export class FieldRow {
|
||||||
columnsMeta,
|
columnsMeta,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
|
displayNameOverride?: string;
|
||||||
flattenedValue: unknown;
|
flattenedValue: unknown;
|
||||||
hit: DataTableRecord;
|
hit: DataTableRecord;
|
||||||
dataView: DataView;
|
dataView: DataView;
|
||||||
|
@ -63,6 +66,7 @@ export class FieldRow {
|
||||||
this.#isFormattedAsText = false;
|
this.#isFormattedAsText = false;
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.displayNameOverride = displayNameOverride;
|
||||||
this.flattenedValue = flattenedValue;
|
this.flattenedValue = flattenedValue;
|
||||||
this.dataViewField = getDataViewFieldOrCreateFromColumnMeta({
|
this.dataViewField = getDataViewFieldOrCreateFromColumnMeta({
|
||||||
dataView,
|
dataView,
|
||||||
|
|
|
@ -73,7 +73,7 @@ const PAGE_SIZE_OPTIONS = [25, 50, 100, 250, 500];
|
||||||
const DEFAULT_PAGE_SIZE = 25;
|
const DEFAULT_PAGE_SIZE = 25;
|
||||||
const PINNED_FIELDS_KEY = 'discover:pinnedFields';
|
const PINNED_FIELDS_KEY = 'discover:pinnedFields';
|
||||||
const PAGE_SIZE = 'discover:pageSize';
|
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';
|
export const SHOW_ONLY_SELECTED_FIELDS = 'unifiedDocViewer:showOnlySelectedFields';
|
||||||
|
|
||||||
const GRID_COLUMN_FIELD_NAME = 'name';
|
const GRID_COLUMN_FIELD_NAME = 'name';
|
||||||
|
|
|
@ -49,13 +49,15 @@ export const TableCell: React.FC<TableCellProps> = React.memo(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { flattenedValue, name, dataViewField, ignoredReason, fieldType } = row;
|
const { flattenedValue, name, displayNameOverride, dataViewField, ignoredReason, fieldType } =
|
||||||
|
row;
|
||||||
|
|
||||||
if (columnId === 'name') {
|
if (columnId === 'name') {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FieldName
|
<FieldName
|
||||||
fieldName={name}
|
fieldName={name}
|
||||||
|
displayNameOverride={displayNameOverride}
|
||||||
fieldType={fieldType}
|
fieldType={fieldType}
|
||||||
fieldMapping={dataViewField}
|
fieldMapping={dataViewField}
|
||||||
scripted={dataViewField?.scripted}
|
scripted={dataViewField?.scripted}
|
||||||
|
|
|
@ -7,17 +7,20 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
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 { DataTableColumnsMeta, DataTableRecord } from '@kbn/discover-utils';
|
||||||
import { DataView } from '@kbn/data-views-plugin/common';
|
import { DataView } from '@kbn/data-views-plugin/common';
|
||||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||||
import { AttributesTable } from './attributes_table';
|
import { AttributesTable } from './attributes_table';
|
||||||
|
import { AttributesEmptyPrompt } from './attributes_empty_prompt';
|
||||||
|
import { AttributeField } from './attributes_overview';
|
||||||
|
|
||||||
interface AttributesAccordionProps {
|
interface AttributesAccordionProps {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
fields: string[];
|
tooltipMessage: string;
|
||||||
|
fields: AttributeField[];
|
||||||
hit: DataTableRecord;
|
hit: DataTableRecord;
|
||||||
dataView: DataView;
|
dataView: DataView;
|
||||||
columns?: string[];
|
columns?: string[];
|
||||||
|
@ -26,12 +29,13 @@ interface AttributesAccordionProps {
|
||||||
onAddColumn?: (col: string) => void;
|
onAddColumn?: (col: string) => void;
|
||||||
onRemoveColumn?: (col: string) => void;
|
onRemoveColumn?: (col: string) => void;
|
||||||
filter?: DocViewFilterFn;
|
filter?: DocViewFilterFn;
|
||||||
|
isEsqlMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AttributesAccordion = ({
|
export const AttributesAccordion = ({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
ariaLabel,
|
tooltipMessage,
|
||||||
fields,
|
fields,
|
||||||
hit,
|
hit,
|
||||||
dataView,
|
dataView,
|
||||||
|
@ -41,34 +45,51 @@ export const AttributesAccordion = ({
|
||||||
onAddColumn,
|
onAddColumn,
|
||||||
onRemoveColumn,
|
onRemoveColumn,
|
||||||
filter,
|
filter,
|
||||||
}: AttributesAccordionProps) => (
|
isEsqlMode = false,
|
||||||
<EuiAccordion
|
}: AttributesAccordionProps) => {
|
||||||
id={id}
|
const { euiTheme } = useEuiTheme();
|
||||||
buttonContent={
|
return (
|
||||||
<EuiText size="s">
|
<EuiAccordion
|
||||||
<strong aria-label={ariaLabel}>{title}</strong>
|
id={id}
|
||||||
</EuiText>
|
buttonContent={
|
||||||
}
|
<EuiText size="s">
|
||||||
initialIsOpen={fields.length > 0}
|
<strong css={{ marginRight: euiTheme.size.xs }}>{title}</strong>
|
||||||
forceState={fields.length === 0 ? 'closed' : undefined}
|
<EuiIconTip
|
||||||
isDisabled={fields.length === 0}
|
aria-label={tooltipMessage}
|
||||||
extraAction={
|
type="questionInCircle"
|
||||||
<EuiNotificationBadge size="m" color="subdued">
|
color="subdued"
|
||||||
{fields.length}
|
size="s"
|
||||||
</EuiNotificationBadge>
|
content={tooltipMessage}
|
||||||
}
|
iconProps={{
|
||||||
paddingSize="m"
|
className: 'eui-alignTop',
|
||||||
>
|
}}
|
||||||
<AttributesTable
|
/>
|
||||||
hit={hit}
|
</EuiText>
|
||||||
dataView={dataView}
|
}
|
||||||
columns={columns}
|
initialIsOpen={fields.length > 0}
|
||||||
columnsMeta={columnsMeta}
|
extraAction={
|
||||||
fields={fields}
|
<EuiNotificationBadge size="m" color="subdued">
|
||||||
searchTerm={searchTerm}
|
{fields.length}
|
||||||
onAddColumn={onAddColumn}
|
</EuiNotificationBadge>
|
||||||
onRemoveColumn={onRemoveColumn}
|
}
|
||||||
filter={filter}
|
paddingSize="m"
|
||||||
/>
|
>
|
||||||
</EuiAccordion>
|
{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
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* 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 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 { i18n } from '@kbn/i18n';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import { SHOW_MULTIFIELDS, getShouldShowFieldHandler } from '@kbn/discover-utils';
|
import { SHOW_MULTIFIELDS, getShouldShowFieldHandler } from '@kbn/discover-utils';
|
||||||
|
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||||
import {
|
import {
|
||||||
LOCAL_STORAGE_KEY_SEARCH_TERM,
|
LOCAL_STORAGE_KEY_SEARCH_TERM,
|
||||||
useTableFilters,
|
useTableFilters,
|
||||||
|
@ -23,12 +31,21 @@ import {
|
||||||
} from '../../../doc_viewer_source/get_height';
|
} from '../../../doc_viewer_source/get_height';
|
||||||
import { AttributesAccordion } from './attributes_accordion';
|
import { AttributesAccordion } from './attributes_accordion';
|
||||||
import { getAttributesTitle } from './get_attributes_title';
|
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({
|
export function AttributesOverview({
|
||||||
columns,
|
columns,
|
||||||
columnsMeta,
|
columnsMeta,
|
||||||
hit,
|
hit,
|
||||||
dataView,
|
dataView,
|
||||||
|
textBasedHits,
|
||||||
filter,
|
filter,
|
||||||
decreaseAvailableHeightBy,
|
decreaseAvailableHeightBy,
|
||||||
onAddColumn,
|
onAddColumn,
|
||||||
|
@ -36,11 +53,13 @@ export function AttributesOverview({
|
||||||
}: DocViewRenderProps) {
|
}: DocViewRenderProps) {
|
||||||
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
|
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
|
||||||
const { storage, uiSettings } = getUnifiedDocViewerServices();
|
const { storage, uiSettings } = getUnifiedDocViewerServices();
|
||||||
|
const isEsqlMode = Array.isArray(textBasedHits);
|
||||||
const showMultiFields = uiSettings.get(SHOW_MULTIFIELDS);
|
const showMultiFields = uiSettings.get(SHOW_MULTIFIELDS);
|
||||||
const { searchTerm, onChangeSearchTerm } = useTableFilters({
|
const { searchTerm, onChangeSearchTerm } = useTableFilters({
|
||||||
storage,
|
storage,
|
||||||
storageKey: LOCAL_STORAGE_KEY_SEARCH_TERM,
|
storageKey: LOCAL_STORAGE_KEY_SEARCH_TERM,
|
||||||
});
|
});
|
||||||
|
const [areNullValuesHidden, setAreNullValuesHidden] = useLocalStorage(HIDE_NULL_VALUES, false);
|
||||||
|
|
||||||
const flattened = hit.flattened;
|
const flattened = hit.flattened;
|
||||||
|
|
||||||
|
@ -53,43 +72,45 @@ export function AttributesOverview({
|
||||||
|
|
||||||
const allFields = Object.keys(flattened);
|
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 groupedFields = useMemo(
|
||||||
const filteredFields = useMemo(
|
() =>
|
||||||
() => allFields.filter(shouldShowFieldHandler),
|
groupAttributesFields({
|
||||||
[allFields, shouldShowFieldHandler]
|
allFields,
|
||||||
|
flattened,
|
||||||
|
searchTerm,
|
||||||
|
shouldShowFieldHandler,
|
||||||
|
isEsqlMode,
|
||||||
|
areNullValuesHidden,
|
||||||
|
}),
|
||||||
|
[allFields, flattened, searchTerm, shouldShowFieldHandler, isEsqlMode, areNullValuesHidden]
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupedFields = useMemo(() => {
|
const { attributesFields, resourceAttributesFields, scopeAttributesFields } = groupedFields;
|
||||||
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 containerHeight = containerRef
|
const containerHeight = containerRef
|
||||||
? getTabContentAvailableHeight(containerRef, decreaseAvailableHeightBy ?? DEFAULT_MARGIN_BOTTOM)
|
? getTabContentAvailableHeight(containerRef, decreaseAvailableHeightBy ?? DEFAULT_MARGIN_BOTTOM)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
const filterFieldsBySearchTerm = (fields: AttributeField[]) =>
|
||||||
|
fields.filter(
|
||||||
|
(field) =>
|
||||||
|
field.displayName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
field.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
const accordionConfigs = [
|
const accordionConfigs = [
|
||||||
{
|
{
|
||||||
id: 'signal_attributes',
|
id: 'signal_attributes',
|
||||||
title: attributesTitle,
|
title: attributesTitle,
|
||||||
ariaLabel: 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',
|
id: 'resource_attributes',
|
||||||
|
@ -100,7 +121,14 @@ export function AttributesOverview({
|
||||||
'unifiedDocViewer.docView.attributes.resourceAttributesTitle.ariaLabel',
|
'unifiedDocViewer.docView.attributes.resourceAttributesTitle.ariaLabel',
|
||||||
{ defaultMessage: 'Resource attributes' }
|
{ 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',
|
id: 'scope_attributes',
|
||||||
|
@ -111,10 +139,26 @@ export function AttributesOverview({
|
||||||
'unifiedDocViewer.docView.attributes.scopeAttributesTitle.ariaLabel',
|
'unifiedDocViewer.docView.attributes.scopeAttributesTitle.ariaLabel',
|
||||||
{ defaultMessage: 'Scope attributes' }
|
{ 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 (
|
return (
|
||||||
<EuiFlexGroup
|
<EuiFlexGroup
|
||||||
ref={setContainerRef}
|
ref={setContainerRef}
|
||||||
|
@ -149,37 +193,71 @@ export function AttributesOverview({
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
</EuiFlexItem>
|
</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
|
<EuiFlexItem
|
||||||
grow={true}
|
grow={true}
|
||||||
css={css`
|
css={css`
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
{noFields ? (
|
||||||
{accordionConfigs.map(({ id, title, ariaLabel, fields }) => (
|
<AttributesEmptyPrompt />
|
||||||
<React.Fragment key={id}>
|
) : (
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||||
<AttributesAccordion
|
{accordionConfigs.map(({ id, title, ariaLabel, fields, tooltipMessage }) => (
|
||||||
id={id}
|
<React.Fragment key={id}>
|
||||||
title={title}
|
<EuiFlexItem grow={false}>
|
||||||
ariaLabel={ariaLabel}
|
<AttributesAccordion
|
||||||
fields={fields}
|
id={id}
|
||||||
hit={hit}
|
title={title}
|
||||||
dataView={dataView}
|
ariaLabel={ariaLabel}
|
||||||
columns={columns}
|
tooltipMessage={tooltipMessage}
|
||||||
columnsMeta={columnsMeta}
|
fields={fields}
|
||||||
searchTerm={searchTerm}
|
hit={hit}
|
||||||
onAddColumn={onAddColumn}
|
dataView={dataView}
|
||||||
onRemoveColumn={onRemoveColumn}
|
columns={columns}
|
||||||
filter={filter}
|
columnsMeta={columnsMeta}
|
||||||
/>
|
searchTerm={searchTerm}
|
||||||
</EuiFlexItem>
|
onAddColumn={onAddColumn}
|
||||||
<EuiFlexItem grow={false}>
|
onRemoveColumn={onRemoveColumn}
|
||||||
<EuiSpacer size="s" />
|
filter={filter}
|
||||||
</EuiFlexItem>
|
isEsqlMode={isEsqlMode}
|
||||||
</React.Fragment>
|
/>
|
||||||
))}
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
</EuiFlexItem>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
)}
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,14 +18,16 @@ import {
|
||||||
} from '../../../doc_viewer_table/table_cell_actions';
|
} from '../../../doc_viewer_table/table_cell_actions';
|
||||||
import { FieldRow } from '../../../doc_viewer_table/field_row';
|
import { FieldRow } from '../../../doc_viewer_table/field_row';
|
||||||
import { getUnifiedDocViewerServices } from '../../../../plugin';
|
import { getUnifiedDocViewerServices } from '../../../../plugin';
|
||||||
|
import { AttributeField } from './attributes_overview';
|
||||||
|
|
||||||
interface AttributesTableProps
|
interface AttributesTableProps
|
||||||
extends Pick<
|
extends Pick<
|
||||||
DocViewRenderProps,
|
DocViewRenderProps,
|
||||||
'hit' | 'dataView' | 'columnsMeta' | 'filter' | 'onAddColumn' | 'onRemoveColumn' | 'columns'
|
'hit' | 'dataView' | 'columnsMeta' | 'filter' | 'onAddColumn' | 'onRemoveColumn' | 'columns'
|
||||||
> {
|
> {
|
||||||
fields: string[];
|
fields: AttributeField[];
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
isEsqlMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AttributesTable = ({
|
export const AttributesTable = ({
|
||||||
|
@ -38,6 +40,7 @@ export const AttributesTable = ({
|
||||||
filter,
|
filter,
|
||||||
onAddColumn,
|
onAddColumn,
|
||||||
onRemoveColumn,
|
onRemoveColumn,
|
||||||
|
isEsqlMode,
|
||||||
}: AttributesTableProps) => {
|
}: AttributesTableProps) => {
|
||||||
const flattened = hit.flattened;
|
const flattened = hit.flattened;
|
||||||
const { fieldFormats, toasts } = getUnifiedDocViewerServices();
|
const { fieldFormats, toasts } = getUnifiedDocViewerServices();
|
||||||
|
@ -55,18 +58,14 @@ export const AttributesTable = ({
|
||||||
};
|
};
|
||||||
}, [onRemoveColumn, onAddColumn, columns]);
|
}, [onRemoveColumn, onAddColumn, columns]);
|
||||||
|
|
||||||
const displayedFields = useMemo(
|
|
||||||
() => fields.filter((field) => field.toLowerCase().includes(searchTerm.toLowerCase())),
|
|
||||||
[fields, searchTerm]
|
|
||||||
);
|
|
||||||
|
|
||||||
const rows: FieldRow[] = useMemo(
|
const rows: FieldRow[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
displayedFields.map(
|
fields.map(
|
||||||
(field) =>
|
(field) =>
|
||||||
new FieldRow({
|
new FieldRow({
|
||||||
name: field,
|
name: field.name,
|
||||||
flattenedValue: flattened[field],
|
displayNameOverride: field.displayName,
|
||||||
|
flattenedValue: flattened[field.name],
|
||||||
hit,
|
hit,
|
||||||
dataView,
|
dataView,
|
||||||
fieldFormats,
|
fieldFormats,
|
||||||
|
@ -74,16 +73,16 @@ export const AttributesTable = ({
|
||||||
columnsMeta,
|
columnsMeta,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[displayedFields, flattened, hit, dataView, fieldFormats, columnsMeta]
|
[fields, flattened, hit, dataView, fieldFormats, columnsMeta]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fieldCellActions = useMemo(
|
const fieldCellActions = useMemo(
|
||||||
() => getFieldCellActions({ rows, isEsqlMode: false, onFilter: filter, onToggleColumn }),
|
() => getFieldCellActions({ rows, isEsqlMode, onFilter: filter, onToggleColumn }),
|
||||||
[rows, filter, onToggleColumn]
|
[rows, filter, onToggleColumn, isEsqlMode]
|
||||||
);
|
);
|
||||||
const fieldValueCellActions = useMemo(
|
const fieldValueCellActions = useMemo(
|
||||||
() => getFieldValueCellActions({ rows, isEsqlMode: false, toasts, onFilter: filter }),
|
() => getFieldValueCellActions({ rows, isEsqlMode, toasts, onFilter: filter }),
|
||||||
[rows, toasts, filter]
|
[rows, toasts, filter, isEsqlMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const gridColumns: EuiDataGridProps['columns'] = [
|
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