diff --git a/src/platform/packages/shared/kbn-lens-common/embeddable/types.ts b/src/platform/packages/shared/kbn-lens-common/embeddable/types.ts index e0051164d9185..0bf82e18d9edd 100644 --- a/src/platform/packages/shared/kbn-lens-common/embeddable/types.ts +++ b/src/platform/packages/shared/kbn-lens-common/embeddable/types.ts @@ -298,6 +298,10 @@ export type LensComponentProps = Simplify< * Toggle inline editing feature */ canEditInline?: boolean; + /** + * Optional search term to highlight in the panel title + */ + titleHighlight?: string; } >; diff --git a/src/platform/packages/shared/kbn-unified-metrics-grid/moon.yml b/src/platform/packages/shared/kbn-unified-metrics-grid/moon.yml index ddc8ea897b353..e0eb6191e3114 100644 --- a/src/platform/packages/shared/kbn-unified-metrics-grid/moon.yml +++ b/src/platform/packages/shared/kbn-unified-metrics-grid/moon.yml @@ -34,7 +34,6 @@ dependsOn: - '@kbn/shared-ux-toolbar-selector' - '@kbn/ui-actions-plugin' - '@kbn/react-hooks' - - '@kbn/data-grid-in-table-search' - '@kbn/es-query' - '@kbn/discover-utils' - '@kbn/fields-metadata-plugin' diff --git a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/chart_title.test.tsx b/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/chart_title.test.tsx deleted file mode 100644 index 08a20c00754e6..0000000000000 --- a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/chart_title.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", 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 '@testing-library/jest-dom'; -import { render } from '@testing-library/react'; -import { ChartTitle } from './chart_title'; - -describe('ChartTitle', () => { - it('should render plain text when searchTerm is empty', () => { - const { getByText } = render(); - expect(getByText('CPU Usage')).toBeInTheDocument(); - }); - - it('should highlight matching searchTerm (case-insensitive)', () => { - const { container } = render(); - // Should highlight "CPU" - const mark = container.querySelector('mark'); - expect(mark).toBeInTheDocument(); - expect(mark?.textContent?.toLowerCase()).toBe('cpu'); - }); - - it('should highlight only matching searchTerm', () => { - const { container } = render( - - ); - // Should highlight only "cpu.load" - const mark = container.querySelector('mark'); - - expect(mark).toBeInTheDocument(); - expect(mark?.textContent?.toLowerCase()).toBe('cpu.load'); - }); - - it('should highlight all occurrences of the searchTerm', () => { - const { container } = render(); - // There should be two elements for "m" - const marks = container.querySelectorAll('mark'); - expect(marks.length).toBe(2); - expect(marks[0].textContent?.toLowerCase()).toBe('m'); - expect(marks[1].textContent?.toLowerCase()).toBe('m'); - }); - - it('should render text with no highlights if searchTerm does not match', () => { - const { container, getByText } = render(); - expect(container.querySelector('mark')).not.toBeInTheDocument(); - expect(getByText('Memory')).toBeInTheDocument(); - }); - - it('handles empty text gracefully', () => { - const { container } = render(); - expect(container.textContent).toBe(''); - expect(container.querySelector('mark')).not.toBeInTheDocument(); - }); -}); diff --git a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/chart_title.tsx b/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/chart_title.tsx deleted file mode 100644 index 96a4551ca0337..0000000000000 --- a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/chart_title.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", 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 { useEuiTheme, EuiHighlight, EuiTextTruncate } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { getHighlightColors } from '@kbn/data-grid-in-table-search/src/get_highlight_colors'; -import React, { useMemo } from 'react'; - -export const ChartTitle = ({ - highlight, - title, -}: { - highlight?: string; - title: string; -}): React.ReactNode => { - const { euiTheme } = useEuiTheme(); - const colors = useMemo(() => getHighlightColors(euiTheme), [euiTheme]); - - const { headerStyles, chartTitleCss } = useMemo(() => { - return { - headerStyles: css` - position: absolute; - width: 100%; - max-height: ${euiTheme.size.l}; - z-index: ${Number(euiTheme.levels.content) + 1}; - transition: outline-color ${euiTheme.animation.extraFast}, - z-index ${euiTheme.animation.extraFast}; - transition-delay: ${euiTheme.animation.fast}; - - overflow: hidden; - height: 100%; - line-height: ${euiTheme.size.l}; - padding: 0px ${euiTheme.size.s}; - - pointer-events: none; - `, - chartTitleCss: css` - font-weight: ${euiTheme.font.weight.bold}; - `, - }; - }, [ - euiTheme.size.l, - euiTheme.size.s, - euiTheme.levels.content, - euiTheme.animation.extraFast, - euiTheme.animation.fast, - euiTheme.font.weight.bold, - ]); - - return ( -
- - {highlight ? ( - - {title} - - ) : ( - - )} - -
- ); -}; diff --git a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/index.tsx b/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/index.tsx index 9dd48dfdad43b..afaed63abdded 100644 --- a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/index.tsx +++ b/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/index.tsx @@ -81,13 +81,6 @@ export const Chart = ({ height: ${ChartSizes[size]}px; outline: ${euiTheme.border.width.thin} solid ${euiTheme.colors.lightShade}; border-radius: ${euiTheme.border.radius.medium}; - - &:hover { - .metricsExperienceChartTitle { - z-index: ${Number(euiTheme.levels.menu) + 1}; - transition: none; - } - } `} ref={chartRef} > diff --git a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/lens_wrapper.test.tsx b/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/lens_wrapper.test.tsx new file mode 100644 index 0000000000000..cb32597da830e --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/lens_wrapper.test.tsx @@ -0,0 +1,199 @@ +/* + * 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 '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import { EuiThemeProvider } from '@elastic/eui'; +import { LensWrapper } from './lens_wrapper'; +import type { LensWrapperProps } from './lens_wrapper'; + +// Mock the EmbeddableComponent +const mockEmbeddableComponent = jest.fn((props) => ( +
+ Mock EmbeddableComponent +
+)); + +// Mock useLensExtraActions +jest.mock('./hooks/use_lens_extra_actions', () => ({ + useLensExtraActions: jest.fn(() => []), +})); + +describe('LensWrapper', () => { + const mockLensProps = { + attributes: { + title: 'Test Chart', + visualizationType: 'bar', + state: { + datasourceStates: {}, + visualization: {}, + query: { query: '', language: 'kuery' }, + filters: [], + }, + references: [], + }, + timeRange: { + from: 'now-15m', + to: 'now', + }, + }; + + const mockServices = { + lens: { + EmbeddableComponent: mockEmbeddableComponent, + }, + }; + + const defaultProps: LensWrapperProps = { + lensProps: mockLensProps, + services: mockServices as any, + abortController: undefined, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('titleHighlight prop', () => { + it('passes titleHighlight prop to EmbeddableComponent', () => { + const { getByTestId } = render( + + + + ); + + expect(mockEmbeddableComponent).toHaveBeenCalledWith( + expect.objectContaining({ + titleHighlight: 'cpu', + }), + expect.anything() + ); + + const embeddableElement = getByTestId('embeddable-component'); + expect(embeddableElement).toHaveAttribute('data-title-highlight', 'cpu'); + }); + + it('passes titleHighlight when undefined', () => { + render( + + + + ); + + expect(mockEmbeddableComponent).toHaveBeenCalledWith( + expect.objectContaining({ + titleHighlight: undefined, + }), + expect.anything() + ); + }); + + it('passes titleHighlight along with other props to EmbeddableComponent', () => { + render( + + + + ); + + expect(mockEmbeddableComponent).toHaveBeenCalledWith( + expect.objectContaining({ + titleHighlight: 'memory', + syncTooltips: true, + syncCursor: true, + title: mockLensProps.attributes.title, + withDefaultActions: true, + }), + expect.anything() + ); + }); + }); + + describe('header visibility', () => { + it('header remains visible when titleHighlight is provided', () => { + const { getByTestId } = render( + + + + ); + + // Verify that the embeddable component is rendered (header will be rendered by EmbeddableComponent) + expect(getByTestId('embeddable-component')).toBeInTheDocument(); + }); + + it('does not hide .embPanel__header with CSS', () => { + const { container } = render( + + + + ); + + // Verify no CSS rules hiding the header + const style = container.querySelector('style'); + if (style) { + expect(style.textContent).not.toContain('.embPanel__header'); + expect(style.textContent).not.toContain('visibility: hidden'); + } + }); + }); + + describe('integration with EmbeddableComponent', () => { + it('passes all required props to EmbeddableComponent', () => { + const onBrushEnd = jest.fn(); + const onFilter = jest.fn(); + const abortController = new AbortController(); + + render( + + + + ); + + expect(mockEmbeddableComponent).toHaveBeenCalledWith( + expect.objectContaining({ + titleHighlight: 'test', + title: mockLensProps.attributes.title, + ...mockLensProps, + onBrushEnd, + onFilter, + abortController, + withDefaultActions: true, + disabledActions: expect.arrayContaining([ + 'ACTION_CUSTOMIZE_PANEL', + 'ACTION_EXPORT_CSV', + 'alertRule', + ]), + }), + expect.anything() + ); + }); + + it('wraps EmbeddableComponent in PresentationPanelQuickActionContext', () => { + const { getByTestId } = render( + + + + ); + + // The component should be wrapped in the context provider + expect(getByTestId('embeddable-component')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/lens_wrapper.tsx b/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/lens_wrapper.tsx index 4ba9162984d70..243517e78ae91 100644 --- a/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/lens_wrapper.tsx +++ b/src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/lens_wrapper.tsx @@ -13,7 +13,6 @@ import { PresentationPanelQuickActionContext } from '@kbn/presentation-panel-plu import type { LensProps } from './hooks/use_lens_props'; import { useLensExtraActions } from './hooks/use_lens_extra_actions'; import { ACTION_EXPLORE_IN_DISCOVER_TAB } from '../../common/constants'; -import { ChartTitle } from './chart_title'; import type { UnifiedMetricsGridProps } from '../../types'; export type LensWrapperProps = { @@ -58,10 +57,6 @@ export function LensWrapper({ width: 100%; } - & .embPanel__header { - visibility: hidden; - } - & .lnsExpressionRenderer { width: 100%; margin: auto; @@ -117,10 +112,10 @@ export function LensWrapper({ - void; -} & Pick; +} & Pick< + PresentationPanelInternalProps, + 'showBadges' | 'getActions' | 'showNotifications' | 'titleHighlight' +>; export const PresentationPanelHeader = < ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi @@ -38,6 +41,7 @@ export const PresentationPanelHeader = < setDragHandle, showBadges = true, showNotifications = true, + titleHighlight, }: PresentationPanelHeaderProps) => { const { euiTheme } = useEuiTheme(); @@ -108,6 +112,7 @@ export const PresentationPanelHeader = < hideTitle={hideTitle} panelTitle={panelTitle} panelDescription={panelDescription} + titleHighlight={titleHighlight} /> {showBadges && badgeElements} diff --git a/src/platform/plugins/private/presentation_panel/public/panel_component/panel_header/presentation_panel_title.test.tsx b/src/platform/plugins/private/presentation_panel/public/panel_component/panel_header/presentation_panel_title.test.tsx new file mode 100644 index 0000000000000..ea7f2881d08ea --- /dev/null +++ b/src/platform/plugins/private/presentation_panel/public/panel_component/panel_header/presentation_panel_title.test.tsx @@ -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 React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { EuiThemeProvider } from '@elastic/eui'; +import { BehaviorSubject } from 'rxjs'; +import { PresentationPanelTitle } from './presentation_panel_title'; +import type { DefaultPresentationPanelApi } from '../types'; + +describe('PresentationPanelTitle', () => { + const mockApi: DefaultPresentationPanelApi = { + uuid: 'test', + title$: new BehaviorSubject('CPU Usage'), + }; + + const defaultProps = { + api: mockApi, + headerId: 'test-header-id', + }; + + const renderWithTheme = (component: React.ReactElement) => { + return render({component}); + }; + + describe('titleHighlight functionality', () => { + it('renders plain text when titleHighlight is not provided', () => { + const { container } = renderWithTheme( + + ); + const titleElement = screen.getByTestId('embeddablePanelTitle'); + expect(titleElement).toHaveTextContent('CPU Usage'); + expect(container.querySelector('mark')).not.toBeInTheDocument(); + }); + + it('renders EuiHighlight component when titleHighlight is provided', () => { + const { container } = renderWithTheme( + + ); + const mark = container.querySelector('mark'); + expect(mark).toBeInTheDocument(); + expect(mark?.textContent?.toLowerCase()).toBe('cpu'); + }); + }); +}); diff --git a/src/platform/plugins/private/presentation_panel/public/panel_component/panel_header/presentation_panel_title.tsx b/src/platform/plugins/private/presentation_panel/public/panel_component/panel_header/presentation_panel_title.tsx index 10912e388de74..025ccb07606a5 100644 --- a/src/platform/plugins/private/presentation_panel/public/panel_component/panel_header/presentation_panel_title.tsx +++ b/src/platform/plugins/private/presentation_panel/public/panel_component/panel_header/presentation_panel_title.tsx @@ -8,6 +8,7 @@ */ import { + EuiHighlight, EuiIcon, EuiLink, EuiScreenReaderOnly, @@ -31,6 +32,7 @@ export const PresentationPanelTitle = ({ hideTitle, panelTitle, panelDescription, + titleHighlight, }: { api: unknown; headerId: string; @@ -38,6 +40,7 @@ export const PresentationPanelTitle = ({ panelTitle?: string; panelDescription?: string; viewMode?: ViewMode; + titleHighlight?: string; }) => { const { euiTheme } = useEuiTheme(); @@ -60,10 +63,19 @@ export const PresentationPanelTitle = ({ } `; + const titleContent = + titleHighlight && panelTitle ? ( + + {panelTitle ?? ''} + + ) : ( + panelTitle + ); + if (viewMode !== 'edit' || !isApiCompatibleWithCustomizePanelAction(api)) { return ( - {panelTitle} + {titleContent} ); } @@ -79,10 +91,10 @@ export const PresentationPanelTitle = ({ })} data-test-subj="embeddablePanelTitle" > - {panelTitle} + {titleContent} ); - }, [onClick, hideTitle, panelTitle, viewMode, api, euiTheme]); + }, [onClick, hideTitle, panelTitle, viewMode, api, euiTheme, titleHighlight]); const describedPanelTitleElement = useMemo(() => { if (hideTitle) return null; diff --git a/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.test.tsx b/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.test.tsx index 783aa1b1e816e..3c493e2810eb6 100644 --- a/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.test.tsx +++ b/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.test.tsx @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { EuiThemeProvider } from '@elastic/eui'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import type { DataView } from '@kbn/data-views-plugin/common'; import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks'; @@ -330,5 +331,27 @@ describe('Presentation panel', () => { await renderPresentationPanel({ api }); expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument(); }); + + it('passes titleHighlight prop through to PresentationPanelHeader', async () => { + const api: DefaultPresentationPanelApi = { + uuid: 'test', + title$: new BehaviorSubject('CPU Usage'), + }; + const { container } = render( + + + + ); + await waitFor(() => { + expect(screen.getByTestId('embeddablePanelTitle')).toBeInTheDocument(); + }); + await waitFor(() => { + const mark = container.querySelector('mark'); + expect(mark).toBeInTheDocument(); + }); + }); }); }); diff --git a/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.tsx b/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.tsx index db9a769fa38e9..e09c2fbf13cef 100644 --- a/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.tsx +++ b/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.tsx @@ -37,6 +37,7 @@ export const PresentationPanelInternal = < showNotifications, getActions, actionPredicate, + titleHighlight, Component, componentProps, @@ -143,6 +144,7 @@ export const PresentationPanelInternal = < showNotifications={showNotifications} panelTitle={panelTitle ?? defaultPanelTitle} panelDescription={panelDescription ?? defaultPanelDescription} + titleHighlight={titleHighlight} /> )} {blockingError && api && ( diff --git a/src/platform/plugins/private/presentation_panel/public/panel_component/types.ts b/src/platform/plugins/private/presentation_panel/public/panel_component/types.ts index d0c40a06f6041..80b12472f2d02 100644 --- a/src/platform/plugins/private/presentation_panel/public/panel_component/types.ts +++ b/src/platform/plugins/private/presentation_panel/public/panel_component/types.ts @@ -65,6 +65,11 @@ export interface PresentationPanelInternalProps< * logic, then this could be removed. */ setDragHandles?: (refs: Array) => void; + + /** + * Optional search term to highlight in the panel title + */ + titleHighlight?: string; } /** diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx index 5717baaaf759f..409a1c1b6367d 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx @@ -42,6 +42,7 @@ type PanelProps = Pick< | 'hideHeader' | 'hideInspector' | 'getActions' + | 'titleHighlight' >; /** @@ -67,6 +68,7 @@ export function LensRenderer({ forceDSL, hidePanelTitles, lastReloadRequestTime, + titleHighlight, ...props }: LensRendererProps) { // Use the settings interface to store panel settings @@ -131,6 +133,7 @@ export function LensRenderer({ showNotifications: false, showShadow: false, showBadges: false, + titleHighlight, getActions: async (triggerId, context) => { const actions = withDefaultActions ? await lensApi?.getTriggerCompatibleActions(triggerId, context) @@ -139,7 +142,7 @@ export function LensRenderer({ return (extraActions ?? []).concat(actions || []); }, }; - }, [showInspector, withDefaultActions, extraActions, lensApi]); + }, [showInspector, withDefaultActions, extraActions, lensApi, titleHighlight]); return (