Skip to content

Commit 22a2ded

Browse files
crespocarloskibanamachineelasticmachine
authored andcommitted
[Metrics][Discover] Integrate metrics experience state with discover state (elastic#239732)
closes [elastic#236395](elastic#236395) ## Summary This PR integrates the Metrics Experience state with Discover state, enabling the integration with Discover tabs. The internal Metrics Experience redux state was removed in favor o Discover's. **Isolated tabs state** ![isolated_tab_state](https://github.com/user-attachments/assets/dc8d45e2-8d3d-4bf9-b70c-6260d21f44fb) **Duplicate tab action** ![duplicate_tab](https://github.com/user-attachments/assets/17387f00-d77a-45e9-8f9a-6d6301452958) ## How to test - Clone https://github.com/simianhacker/simian-forge/tree/main - Run ` ./forge --dataset hosts --count 25 --interval 30s` - For beats metrics, the system integration package MUST be installed (after installing it, make sure to delete all existing metrics data streams) - Set the following config to `kibana.dev.yml` ```yml feature_flags.overrides: metricsExperienceEnabled: true ``` - Navigate to Discover and Switch to ESQL mode - Create new tabs and modify their state - Duplicate tabs --------- Co-authored-by: kibanamachine <[email protected]> Co-authored-by: Elastic Machine <[email protected]>
1 parent cdf0fc8 commit 22a2ded

32 files changed

+412
-431
lines changed

src/platform/packages/shared/kbn-unified-histogram/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@kbn/data-views-plugin",
3838
"@kbn/fields-metadata-plugin",
3939
"@kbn/controls-plugin",
40+
"@kbn/restorable-state",
4041
"@kbn/chart-expressions-common",
4142
"@kbn/lens-common"
4243
]

src/platform/packages/shared/kbn-unified-histogram/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import type { PublishingSubject } from '@kbn/presentation-publishing';
3030
import type { SerializedStyles } from '@emotion/serialize';
3131
import type { ResizableLayoutProps } from '@kbn/resizable-layout';
3232
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
33+
import type { RestorableStateProviderProps } from '@kbn/restorable-state';
3334
import type { UseRequestParamsResult } from './hooks/use_request_params';
3435

3536
/**
@@ -261,12 +262,12 @@ export interface ChartSectionProps {
261262
/**
262263
* Supports customizing the chart (UnifiedHistogram) section in Discover
263264
*/
264-
export type ChartSectionConfiguration =
265+
export type ChartSectionConfiguration<T extends object = object> =
265266
| {
266267
/**
267268
* The component to render for the chart section
268269
*/
269-
Component: React.ComponentType<ChartSectionProps>;
270+
Component: React.ComponentType<ChartSectionProps & RestorableStateProviderProps<T>>;
270271
/**
271272
* Controls whether or not to replace the default histogram and activate the custom chart
272273
*/

src/platform/packages/shared/kbn-unified-metrics-grid/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
export { LazyUnifiedMetricsExperienceGrid as UnifiedMetricsExperienceGrid } from './src/components/lazy_unified_metrics_experience_grid';
1111
export { LazyTraceMetricsGrid as TraceMetricsGrid } from './src/components/trace_metrics_grid/lazy_trace_metrics_grid';
1212
export type { DataSource } from './src/components/trace_metrics_grid';
13+
export type { UnifiedMetricsGridRestorableState } from './src/restorable_state';

src/platform/packages/shared/kbn-unified-metrics-grid/src/common/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { ES_FIELD_TYPES } from '@kbn/field-types';
1111
export const FIELD_VALUE_SEPARATOR = String.fromCharCode(0x1d);
1212

1313
// Full screen classes
14-
export const METRICS_GRID_CLASS = 'metricsExperienceGrid';
15-
export const METRICS_GRID_WRAPPER_FULL_SCREEN_CLASS = 'metricsExperienceGridWrapper--fullScreen';
14+
export const METRICS_GRID_CLASS = 'metricsGrid';
15+
export const METRICS_GRID_WRAPPER_FULL_SCREEN_CLASS = 'metricsGridWrapper--fullScreen';
1616
export const METRICS_GRID_FULL_SCREEN_CLASS = `${METRICS_GRID_CLASS}--fullScreen`;
1717
export const METRICS_GRID_RESTRICT_BODY_CLASS = `${METRICS_GRID_CLASS}--restrictBody`;
1818

src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/chart_title.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import { ChartTitle } from './chart_title';
1414

1515
describe('ChartTitle', () => {
1616
it('should render plain text when searchTerm is empty', () => {
17-
const { getByText } = render(<ChartTitle searchTerm="" title="CPU Usage" />);
17+
const { getByText } = render(<ChartTitle highlight="" title="CPU Usage" />);
1818
expect(getByText('CPU Usage')).toBeInTheDocument();
1919
});
2020

2121
it('should highlight matching searchTerm (case-insensitive)', () => {
22-
const { container } = render(<ChartTitle searchTerm="cpu" title="CPU Usage" />);
22+
const { container } = render(<ChartTitle highlight="cpu" title="CPU Usage" />);
2323
// Should highlight "CPU"
2424
const mark = container.querySelector('mark');
2525
expect(mark).toBeInTheDocument();
@@ -28,7 +28,7 @@ describe('ChartTitle', () => {
2828

2929
it('should highlight only matching searchTerm', () => {
3030
const { container } = render(
31-
<ChartTitle searchTerm="cpu.load" title="system.cpu.load_average.15m" />
31+
<ChartTitle highlight="cpu.load" title="system.cpu.load_average.15m" />
3232
);
3333
// Should highlight only "cpu.load"
3434
const mark = container.querySelector('mark');
@@ -38,7 +38,7 @@ describe('ChartTitle', () => {
3838
});
3939

4040
it('should highlight all occurrences of the searchTerm', () => {
41-
const { container } = render(<ChartTitle searchTerm="m" title="Memory Usage" />);
41+
const { container } = render(<ChartTitle highlight="m" title="Memory Usage" />);
4242
// There should be two <mark> elements for "m"
4343
const marks = container.querySelectorAll('mark');
4444
expect(marks.length).toBe(2);
@@ -47,13 +47,13 @@ describe('ChartTitle', () => {
4747
});
4848

4949
it('should render text with no highlights if searchTerm does not match', () => {
50-
const { container, getByText } = render(<ChartTitle searchTerm="xyz" title="Memory" />);
50+
const { container, getByText } = render(<ChartTitle highlight="xyz" title="Memory" />);
5151
expect(container.querySelector('mark')).not.toBeInTheDocument();
5252
expect(getByText('Memory')).toBeInTheDocument();
5353
});
5454

5555
it('handles empty text gracefully', () => {
56-
const { container } = render(<ChartTitle searchTerm="cpu" title="" />);
56+
const { container } = render(<ChartTitle highlight="cpu" title="" />);
5757
expect(container.textContent).toBe('');
5858
expect(container.querySelector('mark')).not.toBeInTheDocument();
5959
});

src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/chart_title.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { getHighlightColors } from '@kbn/data-grid-in-table-search/src/get_highl
1212
import React, { useMemo } from 'react';
1313

1414
export const ChartTitle = ({
15-
searchTerm,
15+
highlight,
1616
title,
1717
}: {
18-
searchTerm?: string;
18+
highlight?: string;
1919
title: string;
2020
}): React.ReactNode => {
2121
const { euiTheme } = useEuiTheme();
@@ -55,7 +55,7 @@ export const ChartTitle = ({
5555
return (
5656
<div css={headerStyles} className="metricsExperienceChartTitle">
5757
<span css={chartTitleCss}>
58-
{searchTerm ? (
58+
{highlight ? (
5959
<EuiHighlight
6060
css={css`
6161
& mark {
@@ -65,7 +65,7 @@ export const ChartTitle = ({
6565
`}
6666
strict={false}
6767
highlightAll
68-
search={searchTerm}
68+
search={highlight}
6969
>
7070
{title}
7171
</EuiHighlight>

src/platform/packages/shared/kbn-unified-metrics-grid/src/components/chart/lens_wrapper.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import type { ChartSectionProps } from '@kbn/unified-histogram/types';
1313
import type { LensProps } from './hooks/use_lens_props';
1414
import { useLensExtraActions } from './hooks/use_lens_extra_actions';
1515
import { ChartTitle } from './chart_title';
16-
import { useMetricsGridState } from '../../hooks';
1716

1817
export type LensWrapperProps = {
1918
lensProps: LensProps;
19+
titleHighlight?: string;
2020
onViewDetails?: () => void;
2121
onCopyToDashboard?: () => void;
2222
syncTooltips?: boolean;
@@ -31,15 +31,14 @@ export function LensWrapper({
3131
onBrushEnd,
3232
onFilter,
3333
abortController,
34+
titleHighlight,
3435
onViewDetails,
3536
onCopyToDashboard,
3637
syncTooltips,
3738
syncCursor,
3839
}: LensWrapperProps) {
3940
const { euiTheme } = useEuiTheme();
4041

41-
const { searchTerm } = useMetricsGridState();
42-
4342
const { EmbeddableComponent } = services.lens;
4443

4544
const chartCss = css`
@@ -81,7 +80,7 @@ export function LensWrapper({
8180

8281
return (
8382
<div css={chartCss}>
84-
<ChartTitle searchTerm={searchTerm} title={lensProps.attributes.title} />
83+
<ChartTitle highlight={titleHighlight} title={lensProps.attributes.title} />
8584
<EmbeddableComponent
8685
{...lensProps}
8786
title={lensProps.attributes.title}

src/platform/packages/shared/kbn-unified-metrics-grid/src/components/index.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
*/
99

1010
import React from 'react';
11-
import { Provider } from 'react-redux';
1211
import { QueryClient, QueryClientProvider } from '@kbn/react-query';
1312
import type { ChartSectionProps } from '@kbn/unified-histogram/types';
1413
import type { MetricsExperienceClient } from '@kbn/metrics-experience-plugin/public';
1514
import { MetricsExperienceGrid } from './metrics_experience_grid';
16-
import { store } from '../store';
17-
import { MetricsExperienceProvider } from '../context/metrics_experience_provider';
15+
import { MetricsExperienceClientProvider } from '../context/metrics_experience_client_provider';
16+
import { withRestorableState } from '../restorable_state';
17+
import { MetricsExperienceStateProvider } from '../context/metrics_experience_state_provider';
1818

1919
const queryClient = new QueryClient({
2020
defaultOptions: {
@@ -27,23 +27,27 @@ const queryClient = new QueryClient({
2727
},
2828
});
2929

30-
export const UnifiedMetricsExperienceGrid = (
30+
const InternalUnifiedMetricsExperienceGrid = (
3131
props: ChartSectionProps & { client?: MetricsExperienceClient }
3232
) => {
3333
if (!props.client) {
3434
return null;
3535
}
3636

3737
return (
38-
<MetricsExperienceProvider value={{ client: props.client }}>
39-
<Provider store={store}>
38+
<MetricsExperienceClientProvider value={{ client: props.client }}>
39+
<MetricsExperienceStateProvider>
4040
<QueryClientProvider client={queryClient}>
4141
<MetricsExperienceGrid {...props} />
4242
</QueryClientProvider>
43-
</Provider>
44-
</MetricsExperienceProvider>
43+
</MetricsExperienceStateProvider>
44+
</MetricsExperienceClientProvider>
4545
);
4646
};
4747

48+
const UnifiedMetricsExperienceGridWithRestorableState = withRestorableState(
49+
InternalUnifiedMetricsExperienceGrid
50+
);
51+
4852
// eslint-disable-next-line import/no-default-export
49-
export default UnifiedMetricsExperienceGrid;
53+
export default UnifiedMetricsExperienceGridWithRestorableState;

src/platform/packages/shared/kbn-unified-metrics-grid/src/components/metrics_experience_grid.test.tsx

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@ import { Subject } from 'rxjs';
2323
import type { MetricField, Dimension } from '@kbn/metrics-experience-plugin/common/types';
2424
import { ES_FIELD_TYPES } from '@kbn/field-types';
2525
import { fieldsMetadataPluginPublicMock } from '@kbn/fields-metadata-plugin/public/mocks';
26+
import * as metricsExperienceStateProvider from '../context/metrics_experience_state_provider';
2627

27-
jest.mock('../store/hooks');
28+
jest.mock('../context/metrics_experience_state_provider');
2829
jest.mock('../hooks');
2930
jest.mock('./chart', () => ({
3031
Chart: jest.fn(() => <div data-test-subj="metric-chart" />),
3132
}));
3233

33-
const useMetricsGridStateMock = hooks.useMetricsGridState as jest.MockedFunction<
34-
typeof hooks.useMetricsGridState
35-
>;
34+
const useMetricsExperienceStateMock =
35+
metricsExperienceStateProvider.useMetricsExperienceState as jest.MockedFunction<
36+
typeof metricsExperienceStateProvider.useMetricsExperienceState
37+
>;
38+
3639
const useMetricFieldsQueryMock = hooks.useMetricFieldsQuery as jest.MockedFunction<
3740
typeof hooks.useMetricFieldsQuery
3841
>;
@@ -91,26 +94,21 @@ describe('MetricsExperienceGrid', () => {
9194
isComponentVisible: true,
9295
};
9396

94-
beforeAll(() => {
95-
jest.useFakeTimers();
96-
});
97-
9897
beforeEach(() => {
98+
jest.useFakeTimers();
9999
jest.clearAllMocks();
100100

101-
useMetricsGridStateMock.mockReturnValue({
101+
useMetricsExperienceStateMock.mockReturnValue({
102102
currentPage: 0,
103103
dimensions: [],
104104
valueFilters: [],
105105
onDimensionsChange: jest.fn(),
106106
onPageChange: jest.fn(),
107107
onValuesChange: jest.fn(),
108-
onClearValues: jest.fn(),
109-
onClearAllDimensions: jest.fn(),
110108
isFullscreen: false,
111109
searchTerm: '',
112-
onSearchTermChange: () => {},
113-
onToggleFullscreen: () => {},
110+
onSearchTermChange: jest.fn(),
111+
onToggleFullscreen: jest.fn(),
114112
});
115113

116114
usePaginatedFieldsMock.mockReturnValue({
@@ -134,13 +132,14 @@ describe('MetricsExperienceGrid', () => {
134132
metricsGridWrapper: null,
135133
setMetricsGridWrapper: jest.fn(),
136134
styles: {
137-
'metricsExperienceGrid--fullScreen': 'mock-fullscreen-class',
138-
'metricsExperienceGrid--restrictBody': 'mock-restrict-body-class',
135+
'metricsGrid--fullScreen': 'mock-fullscreen-class',
136+
'metricsGrid--restrictBody': 'mock-restrict-body-class',
139137
},
140138
});
141139
});
142140

143-
afterAll(() => {
141+
afterEach(() => {
142+
jest.runOnlyPendingTimers();
144143
jest.useRealTimers();
145144
});
146145

@@ -222,19 +221,17 @@ describe('MetricsExperienceGrid', () => {
222221
});
223222

224223
it('render <ValuesSelector /> when dimensions are selected', async () => {
225-
useMetricsGridStateMock.mockReturnValue({
224+
useMetricsExperienceStateMock.mockReturnValue({
226225
currentPage: 0,
227226
dimensions: ['foo'],
228227
valueFilters: [`foo${FIELD_VALUE_SEPARATOR}bar`],
229228
onDimensionsChange: jest.fn(),
230229
onPageChange: jest.fn(),
231230
onValuesChange: jest.fn(),
232-
onClearValues: jest.fn(),
233-
onClearAllDimensions: jest.fn(),
234231
isFullscreen: false,
235232
searchTerm: '',
236-
onSearchTermChange: () => {},
237-
onToggleFullscreen: () => {},
233+
onSearchTermChange: jest.fn(),
234+
onToggleFullscreen: jest.fn(),
238235
});
239236

240237
const { getByTestId } = render(<MetricsExperienceGrid {...defaultProps} />, {
@@ -249,15 +246,13 @@ describe('MetricsExperienceGrid', () => {
249246
it('shows and updates the search input when the search button is clicked', async () => {
250247
const onSearchTermChange = jest.fn();
251248

252-
useMetricsGridStateMock.mockReturnValue({
249+
useMetricsExperienceStateMock.mockReturnValue({
253250
currentPage: 0,
254251
dimensions: [],
255252
valueFilters: [],
256253
onDimensionsChange: jest.fn(),
257254
onPageChange: jest.fn(),
258255
onValuesChange: jest.fn(),
259-
onClearValues: jest.fn(),
260-
onClearAllDimensions: jest.fn(),
261256
isFullscreen: false,
262257
searchTerm: '',
263258
onSearchTermChange,
@@ -297,15 +292,13 @@ describe('MetricsExperienceGrid', () => {
297292
const onToggleFullscreen = jest.fn();
298293
const isFullscreen = false;
299294

300-
useMetricsGridStateMock.mockReturnValue({
295+
useMetricsExperienceStateMock.mockReturnValue({
301296
currentPage: 0,
302297
dimensions: [],
303298
valueFilters: [],
304299
onDimensionsChange: jest.fn(),
305300
onPageChange: jest.fn(),
306301
onValuesChange: jest.fn(),
307-
onClearValues: jest.fn(),
308-
onClearAllDimensions: jest.fn(),
309302
isFullscreen,
310303
searchTerm: '',
311304
onSearchTermChange: jest.fn(),
@@ -339,15 +332,13 @@ describe('MetricsExperienceGrid', () => {
339332
noData: false,
340333
}));
341334

342-
useMetricsGridStateMock.mockReturnValue({
335+
useMetricsExperienceStateMock.mockReturnValue({
343336
currentPage: 0,
344337
dimensions: [],
345338
valueFilters: [],
346339
onDimensionsChange: jest.fn(),
347340
onPageChange: jest.fn(),
348341
onValuesChange: jest.fn(),
349-
onClearValues: jest.fn(),
350-
onClearAllDimensions: jest.fn(),
351342
isFullscreen: false,
352343
searchTerm: 'cpu',
353344
onSearchTermChange,

0 commit comments

Comments
 (0)