[Discover] Highlight filtered values in badges for custom cell renderers (#213941)

## Summary

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

This PR adds the functionality to properly highlight filtered values
within badges.

Previously, the content was treated as `text` instead of `html`, which
prevented the highlighted values from being displayed correctly.

The content is now rendered with the `<mark>` tag, allowing matching
values to be properly highlighted within the badges.

>[!NOTE]
>By looking at the code I assumed the `<mark>` tag is the only one we
introduce, so the proposed solution only handles that.


|Before|After|
|-|-|
|![Screenshot 2025-03-11 at 15 02
02](https://github.com/user-attachments/assets/779a860e-52c1-446a-b23a-09432ec01132)|![Screenshot
2025-03-11 at 15 02
31](https://github.com/user-attachments/assets/1e4d4a97-fc06-4302-88fe-d6060b6f99bf)|

### How to test

- Make sure you are in a space with Observability as solution view
- Select the "All logs" data view
- Add any filter that matches the displayed badges value
This commit is contained in:
Irene Blanco 2025-03-17 16:18:36 +01:00 committed by GitHub
parent 26d84c4e13
commit 44d49a9501
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 110 additions and 15 deletions

5
.github/CODEOWNERS vendored
View file

@ -1168,8 +1168,9 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/
# TODO: this deprecation_logs folder should be owned by kibana management team after 9.0
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/deprecation_logs @elastic/kibana-data-discovery @elastic/kibana-core
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability @elastic/kibana-data-discovery @elastic/obs-ux-logs-team
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/traces_document_profile @elastic/obs-ux-infra_services-team
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/traces_data_source_profile @elastic/obs-ux-infra_services-team
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile @elastic/obs-ux-infra_services-team
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_data_source_profile @elastic/obs-ux-infra_services-team
src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile @elastic/obs-ux-logs-team @elastic/obs-ux-infra_services-team
# Platform Docs
/x-pack/test_serverless/functional/test_suites/security/screenshot_creation/index.ts @elastic/platform-docs

View file

@ -36,6 +36,7 @@ import {
filterOutText,
openCellActionPopoverAriaText,
} from './translations';
import { truncateAndPreserveHighlightTags } from './utils';
interface CellActionsPopoverProps {
onFilter?: DocViewFilterFn;
@ -192,6 +193,8 @@ export function FieldBadgeWithActions({
rawValue,
color = 'hollow',
}: FieldBadgeWithActionsPropsAndDependencies) {
const MAX_LENGTH = 20;
return (
<CellActionsPopover
onFilter={onFilter}
@ -201,19 +204,14 @@ export function FieldBadgeWithActions({
renderValue={renderValue}
renderPopoverTrigger={({ popoverTriggerProps }) => (
<EuiBadge {...popoverTriggerProps} color={color} iconType={icon} iconSide="left">
{truncateMiddle(value)}
<span
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: truncateAndPreserveHighlightTags(value, MAX_LENGTH),
}}
/>
</EuiBadge>
)}
/>
);
}
const MAX_LENGTH = 20;
function truncateMiddle(value: string): string {
if (value.length < MAX_LENGTH) {
return value;
}
const halfLength = MAX_LENGTH / 2;
return `${value.slice(0, halfLength)}...${value.slice(-halfLength)}`;
}

View file

@ -164,7 +164,7 @@ export const createResourceFields = ({
fieldFormats,
dataView,
dataView.getFieldByName(name),
'text'
'html'
);
return {

View file

@ -0,0 +1,10 @@
/*
* 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 * from './truncate_preserve_highlight_tags';

View file

@ -0,0 +1,52 @@
/*
* 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 { truncateAndPreserveHighlightTags } from '.';
describe('truncateAndPreserveHighlightTags', () => {
const MAX_LENGTH = 10;
const SHORT_TEXT = 'short';
const LONG_TEXT = 'Long text that needs truncation';
describe("when there aren't <mark> tags", () => {
describe('and text is shorter than maxLength', () => {
const result = truncateAndPreserveHighlightTags(SHORT_TEXT, MAX_LENGTH);
it('should return the original string', () => {
expect(result).toBe(SHORT_TEXT);
});
});
describe('and text is longer than or equal to maxLength', () => {
const result = truncateAndPreserveHighlightTags(LONG_TEXT, MAX_LENGTH);
it('should truncate the middle of a long string ', () => {
expect(result).toBe('Long ...ation');
});
});
});
describe('when there are <mark> tags', () => {
describe('and text is shorter than maxLength', () => {
const result = truncateAndPreserveHighlightTags(`<mark>${SHORT_TEXT}</mark>`, MAX_LENGTH);
it('should return the original string with the tags', () => {
expect(result).toBe(`<mark>${SHORT_TEXT}</mark>`);
});
});
describe('and text is longer than or equal to maxLength', () => {
const result = truncateAndPreserveHighlightTags(`<mark>${LONG_TEXT}</mark>`, MAX_LENGTH);
it('should truncate the middle of a long string and add the tags ', () => {
expect(result).toBe('<mark>Long ...ation</mark>');
});
});
});
});

View file

@ -0,0 +1,34 @@
/*
* 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".
*/
function extractTextAndMarkTags(html: string) {
const markTags: string[] = [];
const cleanText = html.replace(/<\/?mark>/g, (match) => {
markTags.push(match);
return '';
});
return { cleanText, markTags };
}
export function truncateAndPreserveHighlightTags(value: string, maxLength: number): string {
const { cleanText, markTags } = extractTextAndMarkTags(value);
if (cleanText.length < maxLength) {
return value;
}
const halfLength = maxLength / 2;
const truncatedText = `${cleanText.slice(0, halfLength)}...${cleanText.slice(-halfLength)}`;
if (markTags.length === 2) {
return `${markTags[0]}${truncatedText}${markTags[1]}`;
}
return truncatedText;
}

View file

@ -51,7 +51,7 @@ export const getServiceNameCell =
props.fieldFormats,
props.dataView,
field,
'text'
'html'
);
return (