[Discover] Support Lens fetches across tabs (#218506)

## Summary

This PR implements support for Lens chart fetches across Discover tabs,
and restoring chart state when returning to a tab.

The Lens embeddable does not currently support continuing data fetching
after it's been unmounted, or fully support restoring chart state using
previously fetched data. The Vis team is aware of this request, but in
the meantime we're using an alternative approach that keeps Lens charts
rendered in memory for each tab that's been visited at least once. This
allows fetches to run in the background and displays the resulting chart
when switching back to tabs. Doing this involved some refactoring to
both Discover and Unified Histogram:
- Create a `ChartPortalsRenderer` wrapper component in Discover to lift
chart rendering high up in the React tree and render charts into
[reverse portals](https://github.com/httptoolkit/react-reverse-portal),
ensuring charts are not remounted when switching tabs and continue to be
rendered after switching away from a tab.
- Refactor Unified Histogram from a single "container" component into
three pieces: a `UnifiedHistogramLayout` component, a
`UnifiedHistogramChart` component, and a `useUnifiedHistogram` hook to
tie things together. This approach allows us to render the chart and
hook separately (in a reverse portal) from the layout, making it
possible to separate the lifecycle of both components without keeping
the rest of Discover's components in memory after switching tabs.
- **Important note:** This change had the side effect of bloating the
Unified Histogram page load bundle since we're now exporting a static
hook that isn't dynamically imported. We could have worked around this
by getting creative with dynamic imports, but doing that seemed hacky.
Instead I decided now was a good time to split Unified Histogram out
into a package in order to support tree shaking, which also has the
added benefits of one fewer plugins to load on startup, and simplifying
the Discover async bundles.

There is one flaw with this approach: the `useDiscoverHistogram` hook
currently depends on global services for retrieving the current query
and filters (including global filters) through the `useQuerySubscriber`
hook. This means the values can become out of sync in inactive tabs when
the user modifies them in the current tab. In practice this doesn't seem
to have an effect in most cases since we don't trigger new fetches in
inactive tabs, and sync the the current tab values to the global
services when switching back to a tab. However, we should still fix this
for the MVP with an approach that doesn't leak state since the current
approach is brittle. I avoided doing that in this PR since it would
likely cause more conflicts with #217706, and that PR may introduce a
solution to the issue anyway (syncing global state to the redux store,
which we can rely on in the hook instead).

Resolves #216475.

### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Davis McPhee 2025-05-02 13:39:25 -03:00 committed by GitHub
parent ee74bb4e65
commit d536f85005
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
117 changed files with 1197 additions and 1382 deletions

2
.github/CODEOWNERS vendored
View file

@ -547,6 +547,7 @@ src/platform/packages/shared/kbn-ui-theme @elastic/kibana-operations
src/platform/packages/shared/kbn-unified-data-table @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations src/platform/packages/shared/kbn-unified-data-table @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations
src/platform/packages/shared/kbn-unified-doc-viewer @elastic/kibana-data-discovery src/platform/packages/shared/kbn-unified-doc-viewer @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-unified-field-list @elastic/kibana-data-discovery src/platform/packages/shared/kbn-unified-field-list @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-unified-histogram @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-unified-tabs @elastic/kibana-data-discovery src/platform/packages/shared/kbn-unified-tabs @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-unsaved-changes-prompt @elastic/kibana-management src/platform/packages/shared/kbn-unsaved-changes-prompt @elastic/kibana-management
src/platform/packages/shared/kbn-use-tracked-promise @elastic/obs-ux-logs-team src/platform/packages/shared/kbn-use-tracked-promise @elastic/obs-ux-logs-team
@ -703,7 +704,6 @@ src/platform/plugins/shared/telemetry_management_section @elastic/kibana-core
src/platform/plugins/shared/ui_actions @elastic/appex-sharedux src/platform/plugins/shared/ui_actions @elastic/appex-sharedux
src/platform/plugins/shared/ui_actions_enhanced @elastic/appex-sharedux src/platform/plugins/shared/ui_actions_enhanced @elastic/appex-sharedux
src/platform/plugins/shared/unified_doc_viewer @elastic/kibana-data-discovery src/platform/plugins/shared/unified_doc_viewer @elastic/kibana-data-discovery
src/platform/plugins/shared/unified_histogram @elastic/kibana-data-discovery
src/platform/plugins/shared/unified_search @elastic/kibana-presentation src/platform/plugins/shared/unified_search @elastic/kibana-presentation
src/platform/plugins/shared/usage_collection @elastic/kibana-core src/platform/plugins/shared/usage_collection @elastic/kibana-core
src/platform/plugins/shared/vis_types/timeseries @elastic/kibana-visualizations src/platform/plugins/shared/vis_types/timeseries @elastic/kibana-visualizations

View file

@ -154,7 +154,7 @@
"unifiedDocViewer": ["src/platform/plugins/shared/unified_doc_viewer", "src/platform/packages/shared/kbn-unified-doc-viewer"], "unifiedDocViewer": ["src/platform/plugins/shared/unified_doc_viewer", "src/platform/packages/shared/kbn-unified-doc-viewer"],
"unifiedSearch": "src/platform/plugins/shared/unified_search", "unifiedSearch": "src/platform/plugins/shared/unified_search",
"unifiedFieldList": "src/platform/packages/shared/kbn-unified-field-list", "unifiedFieldList": "src/platform/packages/shared/kbn-unified-field-list",
"unifiedHistogram": "src/platform/plugins/shared/unified_histogram", "unifiedHistogram": "src/platform/packages/shared/kbn-unified-histogram",
"unifiedDataTable": "src/platform/packages/shared/kbn-unified-data-table", "unifiedDataTable": "src/platform/packages/shared/kbn-unified-data-table",
"unifiedTabs": "src/platform/packages/shared/kbn-unified-tabs", "unifiedTabs": "src/platform/packages/shared/kbn-unified-tabs",
"dataGridInTableSearch": "src/platform/packages/shared/kbn-data-grid-in-table-search", "dataGridInTableSearch": "src/platform/packages/shared/kbn-data-grid-in-table-search",

View file

@ -82,7 +82,6 @@ mapped_pages:
| [uiActions](uiactions-plugin.md) | UI Actions plugins provides API to manage *triggers* and *actions*. *Trigger* is an abstract description of user's intent to perform an action (like user clicking on a value inside chart). It allows us to do runtime binding between code from different plugins. For, example one such trigger is when somebody applies filters on dashboard; another one is when somebody opens a Dashboard panel context menu. *Actions* are pieces of code that execute in response to a trigger. For example, to the dashboard filtering trigger multiple actions can be attached. Once a user filters on the dashboard all possible actions are displayed to the user in a popup menu and the user has to chose one. In general this plugin provides: - Creating custom functionality (actions). - Creating custom user interaction events (triggers). - Attaching and detaching actions to triggers. - Emitting trigger events. - Executing actions attached to a given trigger. - Exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger. | | [uiActions](uiactions-plugin.md) | UI Actions plugins provides API to manage *triggers* and *actions*. *Trigger* is an abstract description of user's intent to perform an action (like user clicking on a value inside chart). It allows us to do runtime binding between code from different plugins. For, example one such trigger is when somebody applies filters on dashboard; another one is when somebody opens a Dashboard panel context menu. *Actions* are pieces of code that execute in response to a trigger. For example, to the dashboard filtering trigger multiple actions can be attached. Once a user filters on the dashboard all possible actions are displayed to the user in a popup menu and the user has to chose one. In general this plugin provides: - Creating custom functionality (actions). - Creating custom user interaction events (triggers). - Attaching and detaching actions to triggers. - Emitting trigger events. - Executing actions attached to a given trigger. - Exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger. |
| [uiActionsEnhanced](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/ui_actions_enhanced/README.md) | Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. | | [uiActionsEnhanced](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/ui_actions_enhanced/README.md) | Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. |
| [unifiedDocViewer](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/unified_doc_viewer/README.md) | This plugin contains services reliant on the plugin lifecycle for the unified doc viewer component (see @kbn/unified-doc-viewer). | | [unifiedDocViewer](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/unified_doc_viewer/README.md) | This plugin contains services reliant on the plugin lifecycle for the unified doc viewer component (see @kbn/unified-doc-viewer). |
| [unifiedHistogram](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/unified_histogram/README.md) | Unified Histogram is a UX Building Block including a layout with a resizable histogram and a main display. It manages its own state and data fetching, and can easily be dropped into pages with minimal setup. |
| [unifiedSearch](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/unified_search/README.md) | Contains all the components of Kibana's unified search experience. Specifically: | | [unifiedSearch](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/unified_search/README.md) | Contains all the components of Kibana's unified search experience. Specifically: |
| [urlForwarding](https://github.com/elastic/kibana/blob/main/src/platform/plugins/private/url_forwarding/README.md) | This plugins contains helpers to redirect legacy URLs. It can be used to forward old URLs to their new counterparts. | | [urlForwarding](https://github.com/elastic/kibana/blob/main/src/platform/plugins/private/url_forwarding/README.md) | This plugins contains helpers to redirect legacy URLs. It can be used to forward old URLs to their new counterparts. |
| [usageCollection](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/usage_collection/README.mdx) | The Usage Collection Service defines a set of APIs for other plugins to report the usage of their features. At the same time, it provides necessary the APIs for other services (i.e.: telemetry, monitoring, ...) to consume that usage data. | | [usageCollection](https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/usage_collection/README.mdx) | The Usage Collection Service defines a set of APIs for other plugins to report the usage of their features. At the same time, it provides necessary the APIs for other services (i.e.: telemetry, monitoring, ...) to consume that usage data. |

View file

@ -998,7 +998,7 @@
"@kbn/unified-doc-viewer-plugin": "link:src/platform/plugins/shared/unified_doc_viewer", "@kbn/unified-doc-viewer-plugin": "link:src/platform/plugins/shared/unified_doc_viewer",
"@kbn/unified-field-list": "link:src/platform/packages/shared/kbn-unified-field-list", "@kbn/unified-field-list": "link:src/platform/packages/shared/kbn-unified-field-list",
"@kbn/unified-field-list-examples-plugin": "link:examples/unified_field_list_examples", "@kbn/unified-field-list-examples-plugin": "link:examples/unified_field_list_examples",
"@kbn/unified-histogram-plugin": "link:src/platform/plugins/shared/unified_histogram", "@kbn/unified-histogram": "link:src/platform/packages/shared/kbn-unified-histogram",
"@kbn/unified-search-plugin": "link:src/platform/plugins/shared/unified_search", "@kbn/unified-search-plugin": "link:src/platform/plugins/shared/unified_search",
"@kbn/unified-tabs": "link:src/platform/packages/shared/kbn-unified-tabs", "@kbn/unified-tabs": "link:src/platform/packages/shared/kbn-unified-tabs",
"@kbn/unified-tabs-examples-plugin": "link:examples/unified_tabs_examples", "@kbn/unified-tabs-examples-plugin": "link:examples/unified_tabs_examples",

View file

@ -168,7 +168,6 @@ pageLoadAssetSize:
uiActions: 35121 uiActions: 35121
uiActionsEnhanced: 38494 uiActionsEnhanced: 38494
unifiedDocViewer: 25099 unifiedDocViewer: 25099
unifiedHistogram: 19928
unifiedSearch: 23000 unifiedSearch: 23000
upgradeAssistant: 81241 upgradeAssistant: 81241
uptime: 60000 uptime: 60000

View file

@ -17,7 +17,7 @@ import {
import type { ResizeTrigger } from '@elastic/eui/src/components/resizable_container/types'; import type { ResizeTrigger } from '@elastic/eui/src/components/resizable_container/types';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import { isEqual, round } from 'lodash'; import { isEqual, round } from 'lodash';
import type { ReactElement } from 'react'; import type { ReactNode } from 'react';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { ResizableLayoutDirection } from '../types'; import { ResizableLayoutDirection } from '../types';
import { getContainerSize, percentToPixels, pixelsToPercent } from './utils'; import { getContainerSize, percentToPixels, pixelsToPercent } from './utils';
@ -47,8 +47,8 @@ export const PanelsResizable = ({
fixedPanelSizePct: number; fixedPanelSizePct: number;
flexPanelSizePct: number; flexPanelSizePct: number;
}; };
fixedPanel: ReactElement; fixedPanel: ReactNode;
flexPanel: ReactElement; flexPanel: ReactNode;
resizeButtonClassName?: string; resizeButtonClassName?: string;
['data-test-subj']?: string; ['data-test-subj']?: string;
onFixedPanelSizeChange?: (fixedPanelSize: number) => void; onFixedPanelSizeChange?: (fixedPanelSize: number) => void;

View file

@ -9,7 +9,7 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import type { ReactElement } from 'react'; import type { ReactNode } from 'react';
import React from 'react'; import React from 'react';
import { ResizableLayoutDirection } from '../types'; import { ResizableLayoutDirection } from '../types';
@ -23,8 +23,8 @@ export const PanelsStatic = ({
className?: string; className?: string;
direction: ResizableLayoutDirection; direction: ResizableLayoutDirection;
hideFixedPanel?: boolean; hideFixedPanel?: boolean;
fixedPanel: ReactElement; fixedPanel: ReactNode;
flexPanel: ReactElement; flexPanel: ReactNode;
}) => { }) => {
// By default a flex item has overflow: visible, min-height: auto, and min-width: auto. // By default a flex item has overflow: visible, min-height: auto, and min-width: auto.
// This can cause the item to overflow the flexbox parent when its content is too large. // This can cause the item to overflow the flexbox parent when its content is too large.

View file

@ -7,7 +7,7 @@
* 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 { ReactElement, useState } from 'react'; import { ReactNode, useState } from 'react';
import React from 'react'; import React from 'react';
import { round } from 'lodash'; import { round } from 'lodash';
import { PanelsResizable } from './panels_resizable'; import { PanelsResizable } from './panels_resizable';
@ -47,11 +47,11 @@ export interface ResizableLayoutProps {
/** /**
* The fixed panel * The fixed panel
*/ */
fixedPanel: ReactElement; fixedPanel: ReactNode;
/** /**
* The flex panel * The flex panel
*/ */
flexPanel: ReactElement; flexPanel: ReactNode;
/** /**
* Class name for the resize button * Class name for the resize button
*/ */

View file

@ -72,7 +72,6 @@ test.describe(
'kbn-ui-shared-deps-npm', 'kbn-ui-shared-deps-npm',
'lens', 'lens',
'maps', 'maps',
'unifiedHistogram',
'unifiedSearch', 'unifiedSearch',
]); ]);
// Validate individual plugin bundle sizes // Validate individual plugin bundle sizes

View file

@ -0,0 +1,3 @@
# @kbn/unified-histogram
Components for the Discover histogram chart

View file

@ -12,8 +12,8 @@ import React from 'react';
import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils'; import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
import { DataViewField } from '@kbn/data-views-plugin/common'; import { DataViewField } from '@kbn/data-views-plugin/common';
import { UnifiedHistogramBreakdownContext } from '../types'; import { UnifiedHistogramBreakdownContext } from '../../types';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
import { BreakdownFieldSelector } from './breakdown_field_selector'; import { BreakdownFieldSelector } from './breakdown_field_selector';
describe('BreakdownFieldSelector', () => { describe('BreakdownFieldSelector', () => {

View file

@ -21,7 +21,7 @@ import { type DataView, DataViewField } from '@kbn/data-views-plugin/common';
import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils'; import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { UnifiedHistogramBreakdownContext } from '../types'; import { UnifiedHistogramBreakdownContext } from '../../types';
import { import {
ToolbarSelector, ToolbarSelector,
ToolbarSelectorProps, ToolbarSelectorProps,

View file

@ -13,18 +13,18 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { Capabilities } from '@kbn/core/public'; import type { Capabilities } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public';
import type { Suggestion } from '@kbn/lens-plugin/public'; import type { Suggestion } from '@kbn/lens-plugin/public';
import type { UnifiedHistogramFetchStatus } from '../types'; import type { UnifiedHistogramFetchStatus } from '../../types';
import { Chart, type ChartProps } from './chart'; import { UnifiedHistogramChart, type UnifiedHistogramChartProps } from './chart';
import type { ReactWrapper } from 'enzyme'; import type { ReactWrapper } from 'enzyme';
import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { unifiedHistogramServicesMock } from '../../__mocks__/services';
import { getLensVisMock } from '../__mocks__/lens_vis'; import { getLensVisMock } from '../../__mocks__/lens_vis';
import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
import { Subject, of } from 'rxjs'; import { Subject, of } from 'rxjs';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
import { dataViewMock } from '../__mocks__/data_view'; import { dataViewMock } from '../../__mocks__/data_view';
import { BreakdownFieldSelector } from './breakdown_field_selector'; import { BreakdownFieldSelector } from './breakdown_field_selector';
import { checkChartAvailability } from './check_chart_availability'; import { checkChartAvailability } from './utils/check_chart_availability';
import { allSuggestionsMock } from '../__mocks__/suggestions'; import { allSuggestionsMock } from '../../__mocks__/suggestions';
let mockUseEditVisualization: jest.Mock | undefined = jest.fn(); let mockUseEditVisualization: jest.Mock | undefined = jest.fn();
@ -38,7 +38,6 @@ async function mountComponent({
noHits, noHits,
noBreakdown, noBreakdown,
chartHidden = false, chartHidden = false,
appendHistogram,
dataView = dataViewWithTimefieldMock, dataView = dataViewWithTimefieldMock,
allSuggestions, allSuggestions,
isPlainRecord, isPlainRecord,
@ -51,7 +50,6 @@ async function mountComponent({
noHits?: boolean; noHits?: boolean;
noBreakdown?: boolean; noBreakdown?: boolean;
chartHidden?: boolean; chartHidden?: boolean;
appendHistogram?: ReactElement;
dataView?: DataView; dataView?: DataView;
allSuggestions?: Suggestion[]; allSuggestions?: Suggestion[];
isPlainRecord?: boolean; isPlainRecord?: boolean;
@ -114,7 +112,7 @@ async function mountComponent({
}) })
).lensService; ).lensService;
const props: ChartProps = { const props: UnifiedHistogramChartProps = {
lensVisService, lensVisService,
dataView, dataView,
requestParams, requestParams,
@ -129,7 +127,6 @@ async function mountComponent({
breakdown: noBreakdown ? undefined : { field: undefined }, breakdown: noBreakdown ? undefined : { field: undefined },
isChartLoading: Boolean(isChartLoading), isChartLoading: Boolean(isChartLoading),
isPlainRecord, isPlainRecord,
appendHistogram,
onChartHiddenChange: jest.fn(), onChartHiddenChange: jest.fn(),
onTimeIntervalChange: jest.fn(), onTimeIntervalChange: jest.fn(),
withDefaultActions: undefined, withDefaultActions: undefined,
@ -140,7 +137,7 @@ async function mountComponent({
let instance: ReactWrapper = {} as ReactWrapper; let instance: ReactWrapper = {} as ReactWrapper;
await act(async () => { await act(async () => {
instance = mountWithIntl(<Chart {...props} />); instance = mountWithIntl(<UnifiedHistogramChart {...props} />);
// wait for initial async loading to complete // wait for initial async loading to complete
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
props.input$?.next({ type: 'fetch' }); props.input$?.next({ type: 'fetch' });
@ -339,12 +336,6 @@ describe('Chart', () => {
expect(mockUseEditVisualization).toHaveBeenCalled(); expect(mockUseEditVisualization).toHaveBeenCalled();
}); });
it('should render the element passed to appendHistogram', async () => {
const appendHistogram = <div data-test-subj="appendHistogram" />;
const component = await mountComponent({ appendHistogram });
expect(component.find('[data-test-subj="appendHistogram"]').exists()).toBeTruthy();
});
it('should not render chart if data view is not time based', async () => { it('should not render chart if data view is not time based', async () => {
const component = await mountComponent({ dataView: dataViewMock }); const component = await mountComponent({ dataView: dataViewMock });
expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeFalsy(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeFalsy();

View file

@ -11,7 +11,7 @@ import React, { memo, ReactElement, useCallback, useEffect, useMemo, useState }
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import useObservable from 'react-use/lib/useObservable'; import useObservable from 'react-use/lib/useObservable';
import { IconButtonGroup, type IconButtonGroupProps } from '@kbn/shared-ux-button-toolbar'; import { IconButtonGroup, type IconButtonGroupProps } from '@kbn/shared-ux-button-toolbar';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiDelayRender } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiDelayRender, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { import type {
EmbeddableComponentProps, EmbeddableComponentProps,
@ -26,7 +26,7 @@ import type {
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import type { TimeRange } from '@kbn/es-query'; import type { TimeRange } from '@kbn/es-query';
import { PublishingSubject } from '@kbn/presentation-publishing'; import { PublishingSubject } from '@kbn/presentation-publishing';
import { RequestStatus } from '@kbn/inspector-plugin/public'; import type { RequestStatus } from '@kbn/inspector-plugin/public';
import { IKibanaSearchResponse } from '@kbn/search-types'; import { IKibanaSearchResponse } from '@kbn/search-types';
import type { estypes } from '@elastic/elasticsearch'; import type { estypes } from '@elastic/elasticsearch';
import { Histogram } from './histogram'; import { Histogram } from './histogram';
@ -42,8 +42,8 @@ import {
UnifiedHistogramRequestContext, UnifiedHistogramRequestContext,
UnifiedHistogramServices, UnifiedHistogramServices,
UnifiedHistogramBucketInterval, UnifiedHistogramBucketInterval,
} from '../types'; } from '../../types';
import { UnifiedHistogramSuggestionType } from '../types'; import { UnifiedHistogramSuggestionType } from '../../types';
import { BreakdownFieldSelector } from './breakdown_field_selector'; import { BreakdownFieldSelector } from './breakdown_field_selector';
import { TimeIntervalSelector } from './time_interval_selector'; import { TimeIntervalSelector } from './time_interval_selector';
import { useTotalHits } from './hooks/use_total_hits'; import { useTotalHits } from './hooks/use_total_hits';
@ -52,18 +52,17 @@ import { useChartActions } from './hooks/use_chart_actions';
import { ChartConfigPanel } from './chart_config_panel'; import { ChartConfigPanel } from './chart_config_panel';
import { useFetch } from './hooks/use_fetch'; import { useFetch } from './hooks/use_fetch';
import { useEditVisualization } from './hooks/use_edit_visualization'; import { useEditVisualization } from './hooks/use_edit_visualization';
import { LensVisService } from '../services/lens_vis_service'; import { LensVisService } from '../../services/lens_vis_service';
import type { UseRequestParamsResult } from '../hooks/use_request_params'; import type { UseRequestParamsResult } from '../../hooks/use_request_params';
import { removeTablesFromLensAttributes } from '../utils/lens_vis_from_table'; import { removeTablesFromLensAttributes } from '../../utils/lens_vis_from_table';
import { useLensProps } from './hooks/use_lens_props'; import { useLensProps } from './hooks/use_lens_props';
import { useStableCallback } from '../hooks/use_stable_callback'; import { useStableCallback } from '../../hooks/use_stable_callback';
import { buildBucketInterval } from './utils/build_bucket_interval'; import { buildBucketInterval } from './utils/build_bucket_interval';
export interface ChartProps { export interface UnifiedHistogramChartProps {
abortController?: AbortController; abortController?: AbortController;
isChartAvailable: boolean; isChartAvailable: boolean;
hiddenPanel?: boolean; hiddenPanel?: boolean;
className?: string;
services: UnifiedHistogramServices; services: UnifiedHistogramServices;
dataView: DataView; dataView: DataView;
requestParams: UseRequestParamsResult; requestParams: UseRequestParamsResult;
@ -75,7 +74,6 @@ export interface ChartProps {
chart?: UnifiedHistogramChartContext; chart?: UnifiedHistogramChartContext;
breakdown?: UnifiedHistogramBreakdownContext; breakdown?: UnifiedHistogramBreakdownContext;
renderCustomChartToggleActions?: () => ReactElement | undefined; renderCustomChartToggleActions?: () => ReactElement | undefined;
appendHistogram?: ReactElement;
disableTriggers?: LensEmbeddableInput['disableTriggers']; disableTriggers?: LensEmbeddableInput['disableTriggers'];
disabledActions?: LensEmbeddableInput['disabledActions']; disabledActions?: LensEmbeddableInput['disabledActions'];
input$?: UnifiedHistogramInput$; input$?: UnifiedHistogramInput$;
@ -89,15 +87,15 @@ export interface ChartProps {
onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void;
onFilter?: LensEmbeddableInput['onFilter']; onFilter?: LensEmbeddableInput['onFilter'];
onBrushEnd?: LensEmbeddableInput['onBrushEnd']; onBrushEnd?: LensEmbeddableInput['onBrushEnd'];
withDefaultActions: EmbeddableComponentProps['withDefaultActions']; withDefaultActions?: EmbeddableComponentProps['withDefaultActions'];
columns?: DatatableColumn[]; columns?: DatatableColumn[];
} }
const RequestStatusError: typeof RequestStatus.ERROR = 2;
const HistogramMemoized = memo(Histogram); const HistogramMemoized = memo(Histogram);
export function Chart({ export function UnifiedHistogramChart({
isChartAvailable, isChartAvailable,
className,
services, services,
dataView, dataView,
requestParams, requestParams,
@ -109,9 +107,6 @@ export function Chart({
lensVisService, lensVisService,
isPlainRecord, isPlainRecord,
renderCustomChartToggleActions, renderCustomChartToggleActions,
appendHistogram,
disableTriggers,
disabledActions,
input$: originalInput$, input$: originalInput$,
lensAdapters, lensAdapters,
dataLoading$, dataLoading$,
@ -121,12 +116,9 @@ export function Chart({
onBreakdownFieldChange, onBreakdownFieldChange,
onTotalHitsChange, onTotalHitsChange,
onChartLoad, onChartLoad,
onFilter,
onBrushEnd,
withDefaultActions,
abortController,
columns, columns,
}: ChartProps) { ...histogramProps
}: UnifiedHistogramChartProps) {
const lensVisServiceCurrentSuggestionContext = useObservable( const lensVisServiceCurrentSuggestionContext = useObservable(
lensVisService.currentSuggestionContext$ lensVisService.currentSuggestionContext$
); );
@ -177,7 +169,7 @@ export function Chart({
dataLoadingSubject$?: PublishingSubject<boolean | undefined> dataLoadingSubject$?: PublishingSubject<boolean | undefined>
) => { ) => {
const lensRequest = adapters?.requests?.getRequests()[0]; const lensRequest = adapters?.requests?.getRequests()[0];
const requestFailed = lensRequest?.status === RequestStatus.ERROR; const requestFailed = lensRequest?.status === RequestStatusError;
const json = lensRequest?.response?.json as const json = lensRequest?.response?.json as
| IKibanaSearchResponse<estypes.SearchResponse> | IKibanaSearchResponse<estypes.SearchResponse>
| undefined; | undefined;
@ -315,7 +307,7 @@ export function Chart({
return ( return (
<EuiFlexGroup <EuiFlexGroup
{...a11yCommonProps} {...a11yCommonProps}
className={className} className="unifiedHistogram__chart"
direction="column" direction="column"
alignItems="stretch" alignItems="stretch"
gutterSize="none" gutterSize="none"
@ -413,7 +405,6 @@ export function Chart({
)} )}
{lensPropsContext && ( {lensPropsContext && (
<HistogramMemoized <HistogramMemoized
abortController={abortController}
services={services} services={services}
dataView={dataView} dataView={dataView}
chart={chart} chart={chart}
@ -421,16 +412,12 @@ export function Chart({
getTimeRange={getTimeRange} getTimeRange={getTimeRange}
visContext={visContext} visContext={visContext}
isPlainRecord={isPlainRecord} isPlainRecord={isPlainRecord}
disableTriggers={disableTriggers} {...histogramProps}
disabledActions={disabledActions}
onFilter={onFilter}
onBrushEnd={onBrushEnd}
withDefaultActions={withDefaultActions}
{...lensPropsContext} {...lensPropsContext}
/> />
)} )}
</section> </section>
{appendHistogram} <EuiSpacer size="s" />
</EuiFlexItem> </EuiFlexItem>
)} )}
{canSaveVisualization && isSaveModalVisible && visContext.attributes && ( {canSaveVisualization && isSaveModalVisible && visContext.attributes && (

View file

@ -12,13 +12,13 @@ import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { setTimeout } from 'timers/promises'; import { setTimeout } from 'timers/promises';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { unifiedHistogramServicesMock } from '../../__mocks__/services';
import { currentSuggestionMock } from '../__mocks__/suggestions'; import { currentSuggestionMock } from '../../__mocks__/suggestions';
import { lensAdaptersMock } from '../__mocks__/lens_adapters'; import { lensAdaptersMock } from '../../__mocks__/lens_adapters';
import { ChartConfigPanel } from './chart_config_panel'; import { ChartConfigPanel } from './chart_config_panel';
import type { UnifiedHistogramVisContext } from '../types'; import type { UnifiedHistogramVisContext } from '../../types';
import { UnifiedHistogramSuggestionType } from '../types'; import { UnifiedHistogramSuggestionType } from '../../types';
describe('ChartConfigPanel', () => { describe('ChartConfigPanel', () => {
it('should return a jsx element to edit the visualization', async () => { it('should return a jsx element to edit the visualization', async () => {

View file

@ -12,9 +12,9 @@ import type { AggregateQuery, Query } from '@kbn/es-query';
import { isEqual, isObject } from 'lodash'; import { isEqual, isObject } from 'lodash';
import type { LensEmbeddableOutput, Suggestion } from '@kbn/lens-plugin/public'; import type { LensEmbeddableOutput, Suggestion } from '@kbn/lens-plugin/public';
import type { Datatable } from '@kbn/expressions-plugin/common'; import type { Datatable } from '@kbn/expressions-plugin/common';
import { EditLensConfigPanelComponent } from '@kbn/lens-plugin/public/plugin'; import type { EditLensConfigPanelComponent } from '@kbn/lens-plugin/public/plugin';
import { DiscoverFlyouts, dismissAllFlyoutsExceptFor } from '@kbn/discover-utils'; import { DiscoverFlyouts, dismissAllFlyoutsExceptFor } from '@kbn/discover-utils';
import { deriveLensSuggestionFromLensAttributes } from '../utils/external_vis_context'; import { deriveLensSuggestionFromLensAttributes } from '../../utils/external_vis_context';
import { import {
UnifiedHistogramChartLoadEvent, UnifiedHistogramChartLoadEvent,
@ -22,7 +22,7 @@ import {
UnifiedHistogramSuggestionContext, UnifiedHistogramSuggestionContext,
UnifiedHistogramSuggestionType, UnifiedHistogramSuggestionType,
UnifiedHistogramVisContext, UnifiedHistogramVisContext,
} from '../types'; } from '../../types';
export function ChartConfigPanel({ export function ChartConfigPanel({
services, services,

View file

@ -11,11 +11,11 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
import { Histogram, HistogramProps } from './histogram'; import { Histogram, HistogramProps } from './histogram';
import React from 'react'; import React from 'react';
import { BehaviorSubject, Subject } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs';
import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { unifiedHistogramServicesMock } from '../../__mocks__/services';
import { getLensVisMock } from '../__mocks__/lens_vis'; import { getLensVisMock } from '../../__mocks__/lens_vis';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
import { createDefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; import { createDefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
import { UnifiedHistogramInput$ } from '../types'; import { UnifiedHistogramInput$ } from '../../types';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { RequestStatus } from '@kbn/inspector-plugin/public'; import { RequestStatus } from '@kbn/inspector-plugin/public';
import { getLensProps, useLensProps } from './hooks/use_lens_props'; import { getLensProps, useLensProps } from './hooks/use_lens_props';

View file

@ -18,7 +18,7 @@ import type {
UnifiedHistogramChartContext, UnifiedHistogramChartContext,
UnifiedHistogramServices, UnifiedHistogramServices,
UnifiedHistogramVisContext, UnifiedHistogramVisContext,
} from '../types'; } from '../../types';
import { useTimeRange } from './hooks/use_time_range'; import { useTimeRange } from './hooks/use_time_range';
import type { LensProps } from './hooks/use_lens_props'; import type { LensProps } from './hooks/use_lens_props';
@ -37,7 +37,7 @@ export interface HistogramProps {
disabledActions?: LensEmbeddableInput['disabledActions']; disabledActions?: LensEmbeddableInput['disabledActions'];
onFilter?: LensEmbeddableInput['onFilter']; onFilter?: LensEmbeddableInput['onFilter'];
onBrushEnd?: LensEmbeddableInput['onBrushEnd']; onBrushEnd?: LensEmbeddableInput['onBrushEnd'];
withDefaultActions: EmbeddableComponentProps['withDefaultActions']; withDefaultActions?: EmbeddableComponentProps['withDefaultActions'];
} }
export function Histogram({ export function Histogram({

View file

@ -8,7 +8,7 @@
*/ */
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { UnifiedHistogramChartContext } from '../../types'; import { UnifiedHistogramChartContext } from '../../../types';
import { useChartActions } from './use_chart_actions'; import { useChartActions } from './use_chart_actions';
describe('useChartActions', () => { describe('useChartActions', () => {

View file

@ -8,7 +8,7 @@
*/ */
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import type { UnifiedHistogramChartContext } from '../../types'; import type { UnifiedHistogramChartContext } from '../../../types';
export const useChartActions = ({ export const useChartActions = ({
chart, chart,

View file

@ -10,9 +10,9 @@
import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common';
import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { waitFor, renderHook } from '@testing-library/react'; import { waitFor, renderHook } from '@testing-library/react';
import { dataViewMock } from '../../__mocks__/data_view'; import { dataViewMock } from '../../../__mocks__/data_view';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import { unifiedHistogramServicesMock } from '../../__mocks__/services'; import { unifiedHistogramServicesMock } from '../../../__mocks__/services';
import { useEditVisualization } from './use_edit_visualization'; import { useEditVisualization } from './use_edit_visualization';
const getTriggerCompatibleActions = unifiedHistogramServicesMock.uiActions const getTriggerCompatibleActions = unifiedHistogramServicesMock.uiActions

View file

@ -12,7 +12,7 @@ import type { TimeRange } from '@kbn/es-query';
import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import type { VISUALIZE_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public'; import type { VISUALIZE_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import type { UnifiedHistogramServices } from '../..'; import type { UnifiedHistogramServices } from '../../..';
// Avoid taking a dependency on uiActionsPlugin just for this const // Avoid taking a dependency on uiActionsPlugin just for this const
const visualizeFieldTrigger: typeof VISUALIZE_FIELD_TRIGGER = 'VISUALIZE_FIELD_TRIGGER'; const visualizeFieldTrigger: typeof VISUALIZE_FIELD_TRIGGER = 'VISUALIZE_FIELD_TRIGGER';

View file

@ -9,7 +9,7 @@
import { useFetch } from './use_fetch'; import { useFetch } from './use_fetch';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { UnifiedHistogramInput$ } from '../../types'; import { UnifiedHistogramInput$ } from '../../../types';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
describe('useFetch', () => { describe('useFetch', () => {

View file

@ -9,7 +9,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { filter, share, tap } from 'rxjs'; import { filter, share, tap } from 'rxjs';
import { UnifiedHistogramInput$ } from '../../types'; import { UnifiedHistogramInput$ } from '../../../types';
export const useFetch = ({ export const useFetch = ({
input$, input$,

View file

@ -9,9 +9,9 @@
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import type { UnifiedHistogramInputMessage } from '../../types'; import type { UnifiedHistogramInputMessage } from '../../../types';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import { getLensVisMock } from '../../__mocks__/lens_vis'; import { getLensVisMock } from '../../../__mocks__/lens_vis';
import { getLensProps, useLensProps } from './use_lens_props'; import { getLensProps, useLensProps } from './use_lens_props';
describe('useLensProps', () => { describe('useLensProps', () => {

View file

@ -16,8 +16,8 @@ import type {
UnifiedHistogramInputMessage, UnifiedHistogramInputMessage,
UnifiedHistogramRequestContext, UnifiedHistogramRequestContext,
UnifiedHistogramVisContext, UnifiedHistogramVisContext,
} from '../../types'; } from '../../../types';
import { useStableCallback } from '../../hooks/use_stable_callback'; import { useStableCallback } from '../../../hooks/use_stable_callback';
export type LensProps = Pick< export type LensProps = Pick<
EmbeddableComponentProps, EmbeddableComponentProps,

View file

@ -10,7 +10,7 @@
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
import { TimeRange } from '@kbn/data-plugin/common'; import { TimeRange } from '@kbn/data-plugin/common';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { UnifiedHistogramBucketInterval } from '../../types'; import { UnifiedHistogramBucketInterval } from '../../../types';
import { useTimeRange } from './use_time_range'; import { useTimeRange } from './use_time_range';
jest.mock('@kbn/datemath', () => ({ jest.mock('@kbn/datemath', () => ({

View file

@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import dateMath from '@kbn/datemath'; import dateMath from '@kbn/datemath';
import type { TimeRange } from '@kbn/data-plugin/common'; import type { TimeRange } from '@kbn/data-plugin/common';
import type { UnifiedHistogramBucketInterval } from '../../types'; import type { UnifiedHistogramBucketInterval } from '../../../types';
export const useTimeRange = ({ export const useTimeRange = ({
uiSettings, uiSettings,

View file

@ -8,8 +8,8 @@
*/ */
import { Filter } from '@kbn/es-query'; import { Filter } from '@kbn/es-query';
import { UnifiedHistogramFetchStatus, UnifiedHistogramInput$ } from '../../types'; import { UnifiedHistogramFetchStatus, UnifiedHistogramInput$ } from '../../../types';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import { useTotalHits } from './use_total_hits'; import { useTotalHits } from './use_total_hits';
import { useEffect as mockUseEffect } from 'react'; import { useEffect as mockUseEffect } from 'react';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks';

View file

@ -19,8 +19,8 @@ import {
UnifiedHistogramInputMessage, UnifiedHistogramInputMessage,
UnifiedHistogramRequestContext, UnifiedHistogramRequestContext,
UnifiedHistogramServices, UnifiedHistogramServices,
} from '../../types'; } from '../../../types';
import { useStableCallback } from '../../hooks/use_stable_callback'; import { useStableCallback } from '../../../hooks/use_stable_callback';
export const useTotalHits = ({ export const useTotalHits = ({
services, services,

View file

@ -7,14 +7,5 @@
* 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 type { Plugin } from '@kbn/core/public'; export { UnifiedHistogramChart, type UnifiedHistogramChartProps } from './chart';
export { checkChartAvailability } from './utils/check_chart_availability';
export class UnifiedHistogramPublicPlugin implements Plugin<{}, {}, object, {}> {
public setup() {
return {};
}
public start() {
return {};
}
}

View file

@ -11,7 +11,7 @@ import React, { useCallback } from 'react';
import { EuiSelectableOption } from '@elastic/eui'; import { EuiSelectableOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { search } from '@kbn/data-plugin/public'; import { search } from '@kbn/data-plugin/public';
import type { UnifiedHistogramChartContext } from '../types'; import type { UnifiedHistogramChartContext } from '../../types';
import { ToolbarSelector, ToolbarSelectorProps, SelectableEntry } from './toolbar_selector'; import { ToolbarSelector, ToolbarSelectorProps, SelectableEntry } from './toolbar_selector';
export interface TimeIntervalSelectorProps { export interface TimeIntervalSelectorProps {

View file

@ -8,7 +8,7 @@
*/ */
import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import { calculateBounds } from '@kbn/data-plugin/public'; import { calculateBounds } from '@kbn/data-plugin/public';
import { buildBucketInterval } from './build_bucket_interval'; import { buildBucketInterval } from './build_bucket_interval';

View file

@ -11,7 +11,7 @@ import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import { DataPublicPluginStart, search, tabifyAggResponse } from '@kbn/data-plugin/public'; import { DataPublicPluginStart, search, tabifyAggResponse } from '@kbn/data-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common';
import type { TimeRange } from '@kbn/es-query'; import type { TimeRange } from '@kbn/es-query';
import type { UnifiedHistogramBucketInterval } from '../../types'; import type { UnifiedHistogramBucketInterval } from '../../../types';
import { getChartAggConfigs } from './get_chart_agg_configs'; import { getChartAggConfigs } from './get_chart_agg_configs';
/** /**

View file

@ -8,7 +8,7 @@
*/ */
import { type DataView, DataViewType } from '@kbn/data-views-plugin/common'; import { type DataView, DataViewType } from '@kbn/data-views-plugin/common';
import { UnifiedHistogramChartContext } from '../types'; import { UnifiedHistogramChartContext } from '../../../types';
export function checkChartAvailability({ export function checkChartAvailability({
chart, chart,

View file

@ -7,7 +7,7 @@
* 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 { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { getChartAggConfigs } from './get_chart_agg_configs'; import { getChartAggConfigs } from './get_chart_agg_configs';

View file

@ -7,5 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
export { Chart } from './chart'; export { UnifiedHistogramLayout, type UnifiedHistogramLayoutProps } from './layout';
export { checkChartAvailability } from './check_chart_availability';

View file

@ -12,16 +12,18 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { ReactWrapper } from 'enzyme'; import type { ReactWrapper } from 'enzyme';
import React from 'react'; import React from 'react';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { Chart } from '../chart'; import { UnifiedHistogramChart } from '../chart';
import { import {
UnifiedHistogramChartContext, UnifiedHistogramChartContext,
UnifiedHistogramFetchStatus, UnifiedHistogramFetchStatus,
UnifiedHistogramHitsContext, UnifiedHistogramHitsContext,
} from '../types'; } from '../../types';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { unifiedHistogramServicesMock } from '../../__mocks__/services';
import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from './layout'; import { UnifiedHistogramLayout } from './layout';
import { ResizableLayout, ResizableLayoutMode } from '@kbn/resizable-layout'; import { ResizableLayout, ResizableLayoutMode } from '@kbn/resizable-layout';
import { UseUnifiedHistogramProps, useUnifiedHistogram } from '../../hooks/use_unified_histogram';
import { act } from 'react-dom/test-utils';
let mockBreakpoint = 'l'; let mockBreakpoint = 'l';
@ -36,54 +38,65 @@ jest.mock('@elastic/eui', () => {
}); });
describe('Layout', () => { describe('Layout', () => {
const createHits = (): UnifiedHistogramHitsContext => ({
status: UnifiedHistogramFetchStatus.complete,
total: 10,
});
const createChart = (): UnifiedHistogramChartContext => ({
hidden: false,
timeInterval: 'auto',
});
const mountComponent = async ({ const mountComponent = async ({
services = unifiedHistogramServicesMock, services = unifiedHistogramServicesMock,
hits = createHits(), hits,
chart = createChart(), chart,
container = null, topPanelHeight,
...rest ...rest
}: Partial<Omit<UnifiedHistogramLayoutProps, 'hits' | 'chart'>> & { }: Partial<UseUnifiedHistogramProps> & {
hits?: UnifiedHistogramHitsContext | null; hits?: UnifiedHistogramHitsContext | null;
chart?: UnifiedHistogramChartContext | null; chart?: UnifiedHistogramChartContext | null;
topPanelHeight?: number | null;
} = {}) => { } = {}) => {
(searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation(
jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } }))
); );
const Wrapper = () => {
const component = mountWithIntl( const unifiedHistogram = useUnifiedHistogram({
<UnifiedHistogramLayout services,
services={services} initialState: {
hits={hits ?? undefined} totalHitsStatus: hits?.status ?? UnifiedHistogramFetchStatus.complete,
chart={chart ?? undefined} totalHitsResult: hits?.total ?? 10,
container={container} chartHidden: chart?.hidden ?? false,
dataView={dataViewWithTimefieldMock} timeInterval: chart?.timeInterval ?? 'auto',
query={{ },
dataView: dataViewWithTimefieldMock,
query: {
language: 'kuery', language: 'kuery',
query: '', query: '',
}} },
filters={[]} filters: [],
timeRange={{ timeRange: {
from: '2020-05-14T11:05:13.590', from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590', to: '2020-05-14T11:20:13.590',
}} },
lensSuggestionsApi={jest.fn()} isChartLoading: false,
onSuggestionContextChange={jest.fn()} ...rest,
isChartLoading={false} });
{...rest}
/>
);
return component; if (!unifiedHistogram.isInitialized) {
return null;
}
return (
<UnifiedHistogramLayout
container={null}
unifiedHistogramChart={<UnifiedHistogramChart {...unifiedHistogram.chartProps} />}
{...unifiedHistogram.layoutProps}
hits={hits === undefined ? unifiedHistogram.layoutProps.hits : hits ?? undefined}
chart={chart === undefined ? unifiedHistogram.layoutProps.chart : chart ?? undefined}
topPanelHeight={
topPanelHeight === undefined
? unifiedHistogram.layoutProps.topPanelHeight
: topPanelHeight ?? undefined
}
/>
);
};
const component = mountWithIntl(<Wrapper />);
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
return component.update();
}; };
const setBreakpoint = (component: ReactWrapper, breakpoint: string) => { const setBreakpoint = (component: ReactWrapper, breakpoint: string) => {
@ -109,12 +122,7 @@ describe('Layout', () => {
}); });
it('should set the layout mode to ResizableLayoutMode.Static if chart.hidden is true', async () => { it('should set the layout mode to ResizableLayoutMode.Static if chart.hidden is true', async () => {
const component = await mountComponent({ const component = await mountComponent({ chart: { timeInterval: 'auto', hidden: true } });
chart: {
...createChart(),
hidden: true,
},
});
expect(component.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Static); expect(component.find(ResizableLayout).prop('mode')).toBe(ResizableLayoutMode.Static);
}); });
@ -132,16 +140,20 @@ describe('Layout', () => {
const component = await mountComponent(); const component = await mountComponent();
setBreakpoint(component, 's'); setBreakpoint(component, 's');
const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize'); const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize');
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).toHaveStyle({ expect(
component.find(UnifiedHistogramChart).find('div.euiFlexGroup').first().getDOMNode()
).toHaveStyle({
height: `${expectedHeight}px`, height: `${expectedHeight}px`,
}); });
}); });
it('should not set a fixed height for Chart when layout mode is ResizableLayoutMode.Static and chart.hidden is true', async () => { it('should not set a fixed height for Chart when layout mode is ResizableLayoutMode.Static and chart.hidden is true', async () => {
const component = await mountComponent({ chart: { ...createChart(), hidden: true } }); const component = await mountComponent({ chart: { timeInterval: 'auto', hidden: true } });
setBreakpoint(component, 's'); setBreakpoint(component, 's');
const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize'); const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize');
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).not.toHaveStyle({ expect(
component.find(UnifiedHistogramChart).find('div.euiFlexGroup').first().getDOMNode()
).not.toHaveStyle({
height: `${expectedHeight}px`, height: `${expectedHeight}px`,
}); });
}); });
@ -150,7 +162,9 @@ describe('Layout', () => {
const component = await mountComponent({ chart: null }); const component = await mountComponent({ chart: null });
setBreakpoint(component, 's'); setBreakpoint(component, 's');
const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize'); const expectedHeight = component.find(ResizableLayout).prop('fixedPanelSize');
expect(component.find(Chart).find('div.euiFlexGroup').first().getDOMNode()).not.toHaveStyle({ expect(
component.find(UnifiedHistogramChart).find('div.euiFlexGroup').first().getDOMNode()
).not.toHaveStyle({
height: `${expectedHeight}px`, height: `${expectedHeight}px`,
}); });
}); });
@ -158,7 +172,7 @@ describe('Layout', () => {
describe('topPanelHeight', () => { describe('topPanelHeight', () => {
it('should pass a default fixedPanelSize to ResizableLayout when the topPanelHeight prop is undefined', async () => { it('should pass a default fixedPanelSize to ResizableLayout when the topPanelHeight prop is undefined', async () => {
const component = await mountComponent({ topPanelHeight: undefined }); const component = await mountComponent({ topPanelHeight: null });
expect(component.find(ResizableLayout).prop('fixedPanelSize')).toBeGreaterThan(0); expect(component.find(ResizableLayout).prop('fixedPanelSize')).toBeGreaterThan(0);
}); });
}); });

View file

@ -0,0 +1,116 @@
/*
* 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 { euiFullHeight, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui';
import React, { PropsWithChildren, ReactNode, useState } from 'react';
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import {
ResizableLayout,
ResizableLayoutDirection,
ResizableLayoutMode,
} from '@kbn/resizable-layout';
import { css } from '@emotion/react';
import { UnifiedHistogramChartContext, UnifiedHistogramHitsContext } from '../../types';
export type UnifiedHistogramLayoutProps = PropsWithChildren<{
/**
* The parent container element, used to calculate the layout size
*/
container: HTMLElement | null;
/**
* The rendered UnifiedHistogramChart component
*/
unifiedHistogramChart: ReactNode;
/**
* Context object for the chart -- leave undefined to hide the chart
*/
chart?: UnifiedHistogramChartContext;
/**
* Flag to indicate if the chart is available for rendering
*/
isChartAvailable?: boolean;
/**
* Context object for the hits count -- leave undefined to hide the hits count
*/
hits?: UnifiedHistogramHitsContext;
/**
* Current top panel height -- leave undefined to use the default
*/
topPanelHeight?: number;
/**
* Callback to update the topPanelHeight prop when a resize is triggered
*/
onTopPanelHeightChange?: (topPanelHeight: number | undefined) => void;
}>;
export const UnifiedHistogramLayout = ({
container,
unifiedHistogramChart,
chart,
isChartAvailable,
hits,
topPanelHeight,
onTopPanelHeightChange,
children,
}: UnifiedHistogramLayoutProps) => {
const [mainPanelNode] = useState(() =>
createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } })
);
const isMobile = useIsWithinBreakpoints(['xs', 's']);
const showFixedPanels = isMobile || !chart || chart.hidden;
const { euiTheme } = useEuiTheme();
const defaultTopPanelHeight = euiTheme.base * 12;
const minMainPanelHeight = euiTheme.base * 10;
const chartCss =
isMobile && chart && !chart.hidden
? css`
.unifiedHistogram__chart {
height: ${defaultTopPanelHeight}px;
}
`
: css`
.unifiedHistogram__chart {
${euiFullHeight()}
}
`;
const panelsMode =
chart || hits
? showFixedPanels
? ResizableLayoutMode.Static
: ResizableLayoutMode.Resizable
: ResizableLayoutMode.Single;
const currentTopPanelHeight = topPanelHeight ?? defaultTopPanelHeight;
return (
<>
<InPortal node={mainPanelNode}>
{React.isValidElement<{ isChartAvailable?: boolean }>(children)
? React.cloneElement(children, { isChartAvailable })
: children}
</InPortal>
<ResizableLayout
mode={panelsMode}
direction={ResizableLayoutDirection.Vertical}
container={container}
fixedPanelSize={currentTopPanelHeight}
minFixedPanelSize={defaultTopPanelHeight}
minFlexPanelSize={minMainPanelHeight}
fixedPanel={unifiedHistogramChart}
flexPanel={<OutPortal node={mainPanelNode} />}
data-test-subj="unifiedHistogram"
css={chartCss}
onFixedPanelSizeChange={onTopPanelHeightChange}
/>
</>
);
};

View file

@ -12,11 +12,11 @@ import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { waitFor, renderHook, act } from '@testing-library/react'; import { waitFor, renderHook, act } from '@testing-library/react';
import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils'; import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
import { UnifiedHistogramFetchStatus, UnifiedHistogramSuggestionContext } from '../../types'; import { UnifiedHistogramFetchStatus, UnifiedHistogramSuggestionContext } from '../types';
import { dataViewMock } from '../../__mocks__/data_view'; import { dataViewMock } from '../__mocks__/data_view';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
import { lensAdaptersMock } from '../../__mocks__/lens_adapters'; import { lensAdaptersMock } from '../__mocks__/lens_adapters';
import { unifiedHistogramServicesMock } from '../../__mocks__/services'; import { unifiedHistogramServicesMock } from '../__mocks__/services';
import { import {
createStateService, createStateService,
UnifiedHistogramState, UnifiedHistogramState,
@ -64,6 +64,7 @@ describe('useStateProps', () => {
columns: undefined, columns: undefined,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
expect(result.current).toMatchInlineSnapshot(` expect(result.current).toMatchInlineSnapshot(`
@ -123,6 +124,7 @@ describe('useStateProps', () => {
"onTimeIntervalChange": [Function], "onTimeIntervalChange": [Function],
"onTopPanelHeightChange": [Function], "onTopPanelHeightChange": [Function],
"onTotalHitsChange": [Function], "onTotalHitsChange": [Function],
"onVisContextChanged": undefined,
"request": Object { "request": Object {
"adapter": RequestAdapter { "adapter": RequestAdapter {
"_events": Object {}, "_events": Object {},
@ -135,6 +137,7 @@ describe('useStateProps', () => {
}, },
"searchSessionId": "123", "searchSessionId": "123",
}, },
"topPanelHeight": 100,
} }
`); `);
}); });
@ -153,6 +156,7 @@ describe('useStateProps', () => {
columns: undefined, columns: undefined,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
expect(result.current).toMatchInlineSnapshot(` expect(result.current).toMatchInlineSnapshot(`
@ -212,6 +216,7 @@ describe('useStateProps', () => {
"onTimeIntervalChange": [Function], "onTimeIntervalChange": [Function],
"onTopPanelHeightChange": [Function], "onTopPanelHeightChange": [Function],
"onTotalHitsChange": [Function], "onTotalHitsChange": [Function],
"onVisContextChanged": undefined,
"request": Object { "request": Object {
"adapter": RequestAdapter { "adapter": RequestAdapter {
"_events": Object {}, "_events": Object {},
@ -224,6 +229,7 @@ describe('useStateProps', () => {
}, },
"searchSessionId": "123", "searchSessionId": "123",
}, },
"topPanelHeight": 100,
} }
`); `);
@ -251,6 +257,7 @@ describe('useStateProps', () => {
columns: undefined, columns: undefined,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
expect(result.current.chart).toStrictEqual({ hidden: false, timeInterval: 'auto' }); expect(result.current.chart).toStrictEqual({ hidden: false, timeInterval: 'auto' });
@ -290,6 +297,7 @@ describe('useStateProps', () => {
columns: esqlColumns, columns: esqlColumns,
breakdownField, breakdownField,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
@ -332,6 +340,7 @@ describe('useStateProps', () => {
columns: esqlColumns, columns: esqlColumns,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
const { onBreakdownFieldChange } = result.current; const { onBreakdownFieldChange } = result.current;
@ -357,6 +366,7 @@ describe('useStateProps', () => {
columns: undefined, columns: undefined,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
expect(result.current).toMatchInlineSnapshot(` expect(result.current).toMatchInlineSnapshot(`
@ -411,6 +421,7 @@ describe('useStateProps', () => {
"onTimeIntervalChange": [Function], "onTimeIntervalChange": [Function],
"onTopPanelHeightChange": [Function], "onTopPanelHeightChange": [Function],
"onTotalHitsChange": [Function], "onTotalHitsChange": [Function],
"onVisContextChanged": undefined,
"request": Object { "request": Object {
"adapter": RequestAdapter { "adapter": RequestAdapter {
"_events": Object {}, "_events": Object {},
@ -423,6 +434,7 @@ describe('useStateProps', () => {
}, },
"searchSessionId": "123", "searchSessionId": "123",
}, },
"topPanelHeight": 100,
} }
`); `);
}); });
@ -441,6 +453,7 @@ describe('useStateProps', () => {
columns: undefined, columns: undefined,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
expect(result.current).toMatchInlineSnapshot(` expect(result.current).toMatchInlineSnapshot(`
@ -495,6 +508,7 @@ describe('useStateProps', () => {
"onTimeIntervalChange": [Function], "onTimeIntervalChange": [Function],
"onTopPanelHeightChange": [Function], "onTopPanelHeightChange": [Function],
"onTotalHitsChange": [Function], "onTotalHitsChange": [Function],
"onVisContextChanged": undefined,
"request": Object { "request": Object {
"adapter": RequestAdapter { "adapter": RequestAdapter {
"_events": Object {}, "_events": Object {},
@ -507,6 +521,7 @@ describe('useStateProps', () => {
}, },
"searchSessionId": "123", "searchSessionId": "123",
}, },
"topPanelHeight": 100,
} }
`); `);
}); });
@ -525,6 +540,7 @@ describe('useStateProps', () => {
columns: undefined, columns: undefined,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
@ -602,6 +618,7 @@ describe('useStateProps', () => {
columns: undefined, columns: undefined,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}) })
); );
(stateService.setLensRequestAdapter as jest.Mock).mockClear(); (stateService.setLensRequestAdapter as jest.Mock).mockClear();
@ -626,6 +643,7 @@ describe('useStateProps', () => {
columns: undefined, columns: undefined,
breakdownField: undefined, breakdownField: undefined,
onBreakdownFieldChange: undefined, onBreakdownFieldChange: undefined,
onVisContextChanged: undefined,
}; };
const hook = renderHook((props: Parameters<typeof useStateProps>[0]) => useStateProps(props), { const hook = renderHook((props: Parameters<typeof useStateProps>[0]) => useStateProps(props), {
initialProps, initialProps,

View file

@ -16,10 +16,12 @@ import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils'
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { import {
UnifiedHistogramChartLoadEvent, UnifiedHistogramChartLoadEvent,
UnifiedHistogramExternalVisContextStatus,
UnifiedHistogramFetchStatus, UnifiedHistogramFetchStatus,
UnifiedHistogramServices, UnifiedHistogramServices,
UnifiedHistogramSuggestionContext, UnifiedHistogramSuggestionContext,
} from '../../types'; UnifiedHistogramVisContext,
} from '../types';
import type { UnifiedHistogramStateService } from '../services/state_service'; import type { UnifiedHistogramStateService } from '../services/state_service';
import { import {
chartHiddenSelector, chartHiddenSelector,
@ -28,9 +30,12 @@ import {
totalHitsStatusSelector, totalHitsStatusSelector,
lensAdaptersSelector, lensAdaptersSelector,
lensDataLoadingSelector$, lensDataLoadingSelector$,
topPanelHeightSelector,
} from '../utils/state_selectors'; } from '../utils/state_selectors';
import { useStateSelector } from '../utils/use_state_selector'; import { useStateSelector } from './use_state_selector';
import { setBreakdownField } from '../utils/local_storage_utils'; import { setBreakdownField } from '../utils/local_storage_utils';
import { exportVisContext } from '../utils/external_vis_context';
import { UseUnifiedHistogramProps } from './use_unified_histogram';
export const useStateProps = ({ export const useStateProps = ({
services, services,
@ -43,6 +48,7 @@ export const useStateProps = ({
columns, columns,
breakdownField, breakdownField,
onBreakdownFieldChange: originalOnBreakdownFieldChange, onBreakdownFieldChange: originalOnBreakdownFieldChange,
onVisContextChanged: originalOnVisContextChanged,
}: { }: {
services: UnifiedHistogramServices; services: UnifiedHistogramServices;
localStorageKeyPrefix: string | undefined; localStorageKeyPrefix: string | undefined;
@ -54,13 +60,21 @@ export const useStateProps = ({
columns: DatatableColumn[] | undefined; columns: DatatableColumn[] | undefined;
breakdownField: string | undefined; breakdownField: string | undefined;
onBreakdownFieldChange: ((breakdownField: string | undefined) => void) | undefined; onBreakdownFieldChange: ((breakdownField: string | undefined) => void) | undefined;
onVisContextChanged:
| ((
nextVisContext: UnifiedHistogramVisContext | undefined,
externalVisContextStatus: UnifiedHistogramExternalVisContextStatus
) => void)
| undefined;
}) => { }) => {
const topPanelHeight = useStateSelector(stateService?.state$, topPanelHeightSelector);
const chartHidden = useStateSelector(stateService?.state$, chartHiddenSelector); const chartHidden = useStateSelector(stateService?.state$, chartHiddenSelector);
const timeInterval = useStateSelector(stateService?.state$, timeIntervalSelector); const timeInterval = useStateSelector(stateService?.state$, timeIntervalSelector);
const totalHitsResult = useStateSelector(stateService?.state$, totalHitsResultSelector); const totalHitsResult = useStateSelector(stateService?.state$, totalHitsResultSelector);
const totalHitsStatus = useStateSelector(stateService?.state$, totalHitsStatusSelector); const totalHitsStatus = useStateSelector(stateService?.state$, totalHitsStatusSelector);
const lensAdapters = useStateSelector(stateService?.state$, lensAdaptersSelector); const lensAdapters = useStateSelector(stateService?.state$, lensAdaptersSelector);
const lensDataLoading$ = useStateSelector(stateService?.state$, lensDataLoadingSelector$); const lensDataLoading$ = useStateSelector(stateService?.state$, lensDataLoadingSelector$);
/** /**
* Contexts * Contexts
*/ */
@ -132,8 +146,8 @@ export const useStateProps = ({
*/ */
const onTopPanelHeightChange = useCallback( const onTopPanelHeightChange = useCallback(
(topPanelHeight: number | undefined) => { (newTopPanelHeight: number | undefined) => {
stateService?.setTopPanelHeight(topPanelHeight); stateService?.setTopPanelHeight(newTopPanelHeight);
}, },
[stateService] [stateService]
); );
@ -186,6 +200,18 @@ export const useStateProps = ({
[stateService] [stateService]
); );
const onVisContextChanged: UseUnifiedHistogramProps['onVisContextChanged'] = useMemo(() => {
if (!originalOnVisContextChanged || !isPlainRecord) {
return undefined;
}
return (visContext, externalVisContextStatus) => {
const minifiedVisContext = exportVisContext(visContext);
originalOnVisContextChanged(minifiedVisContext, externalVisContextStatus);
};
}, [isPlainRecord, originalOnVisContextChanged]);
/** /**
* Effects * Effects
*/ */
@ -205,6 +231,7 @@ export const useStateProps = ({
}, [chart, chartHidden, stateService]); }, [chart, chartHidden, stateService]);
return { return {
topPanelHeight,
hits, hits,
chart, chart,
breakdown, breakdown,
@ -219,5 +246,6 @@ export const useStateProps = ({
onChartLoad, onChartLoad,
onBreakdownFieldChange, onBreakdownFieldChange,
onSuggestionContextChange, onSuggestionContextChange,
onVisContextChanged,
}; };
}; };

View file

@ -0,0 +1,68 @@
/*
* 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 { RequestAdapter } from '@kbn/inspector-plugin/common';
import { act } from 'react-dom/test-utils';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
import { unifiedHistogramServicesMock } from '../__mocks__/services';
import { useUnifiedHistogram } from './use_unified_histogram';
import { renderHook, waitFor } from '@testing-library/react';
describe('useUnifiedHistogram', () => {
it('should initialize', async () => {
const hook = renderHook(() =>
useUnifiedHistogram({
services: unifiedHistogramServicesMock,
initialState: { timeInterval: '42s' },
dataView: dataViewWithTimefieldMock,
filters: [],
query: { language: 'kuery', query: '' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
timeRange: { from: 'now-15m', to: 'now' },
})
);
expect(hook.result.current.isInitialized).toBe(false);
expect(hook.result.current.api).toBeUndefined();
expect(hook.result.current.chartProps).toBeUndefined();
expect(hook.result.current.layoutProps).toBeUndefined();
await waitFor(() => {
expect(hook.result.current.isInitialized).toBe(true);
});
expect(hook.result.current.api).toBeDefined();
expect(hook.result.current.chartProps?.chart?.timeInterval).toBe('42s');
expect(hook.result.current.layoutProps).toBeDefined();
});
it('should trigger input$ when fetch is called', async () => {
const { result } = renderHook(() =>
useUnifiedHistogram({
services: unifiedHistogramServicesMock,
initialState: { timeInterval: '42s' },
dataView: dataViewWithTimefieldMock,
filters: [],
query: { language: 'kuery', query: '' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
timeRange: { from: 'now-15m', to: 'now' },
})
);
await waitFor(() => {
expect(result.current.isInitialized).toBe(true);
});
const input$ = result.current.chartProps?.input$;
const inputSpy = jest.fn();
input$?.subscribe(inputSpy);
act(() => {
result.current.api?.fetch();
});
expect(inputSpy).toHaveBeenCalledTimes(1);
expect(inputSpy).toHaveBeenCalledWith({ type: 'fetch' });
});
});

View file

@ -0,0 +1,324 @@
/*
* 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 { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/public';
import type { EmbeddableComponentProps, LensEmbeddableInput } from '@kbn/lens-plugin/public';
import { useEffect, useMemo, useState } from 'react';
import { Observable, Subject, of } from 'rxjs';
import useMount from 'react-use/lib/useMount';
import { pick } from 'lodash';
import type { DataView } from '@kbn/data-views-plugin/common';
import useObservable from 'react-use/lib/useObservable';
import { UnifiedHistogramChartProps } from '../components/chart/chart';
import {
UnifiedHistogramExternalVisContextStatus,
UnifiedHistogramInputMessage,
UnifiedHistogramRequestContext,
UnifiedHistogramServices,
UnifiedHistogramSuggestionContext,
UnifiedHistogramSuggestionType,
UnifiedHistogramVisContext,
} from '../types';
import {
UnifiedHistogramStateOptions,
UnifiedHistogramStateService,
createStateService,
} from '../services/state_service';
import { useStateProps } from './use_state_props';
import { useRequestParams } from './use_request_params';
import { LensVisService } from '../services/lens_vis_service';
import { checkChartAvailability } from '../components/chart';
import { UnifiedHistogramLayoutProps } from '../components/layout/layout';
import { getBreakdownField } from '../utils/local_storage_utils';
export type UseUnifiedHistogramProps = Omit<UnifiedHistogramStateOptions, 'services'> & {
/**
* Required services
*/
services: UnifiedHistogramServices;
/**
* The current search session ID
*/
searchSessionId?: UnifiedHistogramRequestContext['searchSessionId'];
/**
* The request adapter to use for the inspector
*/
requestAdapter?: UnifiedHistogramRequestContext['adapter'];
/**
* The abort controller to use for requests
*/
abortController?: AbortController;
/**
* The current data view
*/
dataView: DataView;
/**
* The current query
*/
query?: Query | AggregateQuery;
/**
* The current filters
*/
filters?: Filter[];
/**
* The current breakdown field
*/
breakdownField?: string;
/**
* The external custom Lens vis
*/
externalVisContext?: UnifiedHistogramVisContext;
/**
* The current time range
*/
timeRange?: TimeRange;
/**
* The relative time range, used when timeRange is an absolute range (e.g. for edit visualization button)
*/
relativeTimeRange?: TimeRange;
/**
* The current columns
*/
columns?: DatatableColumn[];
/**
* Preloaded data table sometimes used for rendering the chart in ES|QL mode
*/
table?: Datatable;
/**
* Flag indicating that the chart is currently loading
*/
isChartLoading?: boolean;
/**
* Allows users to enable/disable default actions
*/
withDefaultActions?: EmbeddableComponentProps['withDefaultActions'];
/**
* Disabled action IDs for the Lens embeddable
*/
disabledActions?: LensEmbeddableInput['disabledActions'];
/**
* Callback to pass to the Lens embeddable to handle filter changes
*/
onFilter?: LensEmbeddableInput['onFilter'];
/**
* Callback to pass to the Lens embeddable to handle brush events
*/
onBrushEnd?: LensEmbeddableInput['onBrushEnd'];
/**
* Callback to update the breakdown field -- should set {@link UnifiedHistogramBreakdownContext.field} to breakdownField
*/
onBreakdownFieldChange?: (breakdownField: string | undefined) => void;
/**
* Callback to notify about the change in Lens attributes
*/
onVisContextChanged?: (
nextVisContext: UnifiedHistogramVisContext | undefined,
externalVisContextStatus: UnifiedHistogramExternalVisContextStatus
) => void;
};
export type UnifiedHistogramApi = {
/**
* Trigger a fetch of the data
*/
fetch: () => void;
} & Pick<
UnifiedHistogramStateService,
'state$' | 'setChartHidden' | 'setTopPanelHeight' | 'setTimeInterval' | 'setTotalHits'
>;
export type UnifiedHistogramPartialLayoutProps = Omit<
UnifiedHistogramLayoutProps,
'container' | 'unifiedHistogramChart'
>;
export type UseUnifiedHistogramResult =
| { isInitialized: false; api?: undefined; chartProps?: undefined; layoutProps?: undefined }
| {
isInitialized: true;
api: UnifiedHistogramApi;
chartProps: UnifiedHistogramChartProps;
layoutProps: UnifiedHistogramPartialLayoutProps;
};
const EMPTY_SUGGESTION_CONTEXT: Observable<UnifiedHistogramSuggestionContext> = of({
suggestion: undefined,
type: UnifiedHistogramSuggestionType.unsupported,
});
export const useUnifiedHistogram = (props: UseUnifiedHistogramProps): UseUnifiedHistogramResult => {
const [stateService] = useState(() => {
const { services, initialState, localStorageKeyPrefix } = props;
return createStateService({ services, initialState, localStorageKeyPrefix });
});
const [lensVisService, setLensVisService] = useState<LensVisService>();
const [input$] = useState(() => new Subject<UnifiedHistogramInputMessage>());
const [api, setApi] = useState<UnifiedHistogramApi>();
// Load async services and initialize API
useMount(async () => {
const apiHelper = await services.lens.stateHelperApi();
setLensVisService(new LensVisService({ services, lensSuggestionsApi: apiHelper.suggestions }));
setApi({
fetch: () => {
input$.next({ type: 'fetch' });
},
...pick(
stateService,
'state$',
'setChartHidden',
'setTopPanelHeight',
'setTimeInterval',
'setTotalHits'
),
});
});
const {
services,
dataView,
query,
columns,
searchSessionId,
requestAdapter,
isChartLoading,
localStorageKeyPrefix,
filters,
timeRange,
table,
externalVisContext,
} = props;
const initialBreakdownField = useMemo(
() =>
localStorageKeyPrefix
? getBreakdownField(services.storage, localStorageKeyPrefix)
: undefined,
[localStorageKeyPrefix, services.storage]
);
const stateProps = useStateProps({
services,
localStorageKeyPrefix,
stateService,
dataView,
query,
searchSessionId,
requestAdapter,
columns,
breakdownField: 'breakdownField' in props ? props.breakdownField : initialBreakdownField,
onBreakdownFieldChange: props.onBreakdownFieldChange,
onVisContextChanged: props.onVisContextChanged,
});
const columnsMap = useMemo(() => {
return columns?.reduce<Record<string, DatatableColumn>>((acc, column) => {
acc[column.id] = column;
return acc;
}, {});
}, [columns]);
const requestParams = useRequestParams({
services,
query,
filters,
timeRange,
});
const lensVisServiceCurrentSuggestionContext = useObservable(
lensVisService?.currentSuggestionContext$ ?? EMPTY_SUGGESTION_CONTEXT
);
useEffect(() => {
if (isChartLoading || !lensVisService) {
return;
}
lensVisService.update({
externalVisContext,
queryParams: {
dataView,
query: requestParams.query,
filters: requestParams.filters,
timeRange,
isPlainRecord: stateProps.isPlainRecord,
columns,
columnsMap,
},
timeInterval: stateProps.chart?.timeInterval,
breakdownField: stateProps.breakdown?.field,
table,
onSuggestionContextChange: stateProps.onSuggestionContextChange,
onVisContextChanged: stateProps.onVisContextChanged,
});
}, [
columns,
columnsMap,
dataView,
externalVisContext,
isChartLoading,
lensVisService,
requestParams.filters,
requestParams.query,
stateProps.breakdown?.field,
stateProps.chart?.timeInterval,
stateProps.isPlainRecord,
stateProps.onSuggestionContextChange,
stateProps.onVisContextChanged,
table,
timeRange,
]);
const chart =
!lensVisServiceCurrentSuggestionContext?.type ||
lensVisServiceCurrentSuggestionContext.type === UnifiedHistogramSuggestionType.unsupported
? undefined
: stateProps.chart;
const isChartAvailable = checkChartAvailability({
chart,
dataView,
isPlainRecord: stateProps.isPlainRecord,
});
const chartProps = useMemo<UnifiedHistogramChartProps | undefined>(() => {
return lensVisService
? {
...props,
...stateProps,
input$,
chart,
isChartAvailable,
requestParams,
lensVisService,
}
: undefined;
}, [chart, input$, isChartAvailable, lensVisService, props, requestParams, stateProps]);
const layoutProps = useMemo<UnifiedHistogramPartialLayoutProps>(
() => ({
chart,
isChartAvailable,
hits: stateProps.hits,
topPanelHeight: stateProps.topPanelHeight,
onTopPanelHeightChange: stateProps.onTopPanelHeightChange,
}),
[
chart,
isChartAvailable,
stateProps.hits,
stateProps.onTopPanelHeightChange,
stateProps.topPanelHeight,
]
);
if (!api || !chartProps) {
return { isInitialized: false };
}
return {
isInitialized: true,
api,
chartProps,
layoutProps,
};
};

View file

@ -7,27 +7,6 @@
* 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 { UnifiedHistogramPublicPlugin } from './plugin';
export type { BreakdownFieldSelectorProps } from './chart/lazy';
export { UnifiedBreakdownFieldSelector } from './chart/lazy';
export type {
UnifiedHistogramApi,
UnifiedHistogramContainerProps,
UnifiedHistogramCreationOptions,
UnifiedHistogramState,
UnifiedHistogramStateOptions,
} from './container';
export {
UnifiedHistogramContainer,
getChartHidden,
getTopPanelHeight,
getBreakdownField,
setChartHidden,
setTopPanelHeight,
setBreakdownField,
} from './container';
export type { export type {
UnifiedHistogramServices, UnifiedHistogramServices,
UnifiedHistogramChartLoadEvent, UnifiedHistogramChartLoadEvent,
@ -35,6 +14,29 @@ export type {
UnifiedHistogramVisContext, UnifiedHistogramVisContext,
} from './types'; } from './types';
export { UnifiedHistogramFetchStatus, UnifiedHistogramExternalVisContextStatus } from './types'; export { UnifiedHistogramFetchStatus, UnifiedHistogramExternalVisContextStatus } from './types';
export { canImportVisContext } from './utils/external_vis_context';
export const plugin = () => new UnifiedHistogramPublicPlugin(); export {
UnifiedBreakdownFieldSelector,
type BreakdownFieldSelectorProps,
} from './components/chart/lazy';
export { UnifiedHistogramChart, type UnifiedHistogramChartProps } from './components/chart';
export { UnifiedHistogramLayout, type UnifiedHistogramLayoutProps } from './components/layout';
export {
useUnifiedHistogram,
type UseUnifiedHistogramProps,
type UnifiedHistogramApi,
type UnifiedHistogramPartialLayoutProps,
} from './hooks/use_unified_histogram';
export type { UnifiedHistogramState } from './services/state_service';
export {
getChartHidden,
getTopPanelHeight,
getBreakdownField,
setChartHidden,
setTopPanelHeight,
setBreakdownField,
} from './utils/local_storage_utils';
export { canImportVisContext } from './utils/external_vis_context';

View file

@ -10,11 +10,5 @@
module.exports = { module.exports = {
preset: '@kbn/test', preset: '@kbn/test',
rootDir: '../../../../..', rootDir: '../../../../..',
roots: ['<rootDir>/src/platform/plugins/shared/unified_histogram'], roots: ['<rootDir>/src/platform/packages/shared/kbn-unified-histogram'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/src/platform/plugins/shared/unified_histogram',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/src/platform/plugins/shared/unified_histogram/{common,public,server}/**/*.{ts,tsx}',
],
}; };

View file

@ -0,0 +1,8 @@
{
"type": "shared-browser",
"id": "@kbn/unified-histogram",
"owner": "@elastic/kibana-data-discovery",
"group": "platform",
"visibility": "shared",
"description": "Components for the Discover histogram chart"
}

View file

@ -8,7 +8,7 @@
*/ */
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import type { UnifiedHistogramApi } from './container'; import { UnifiedHistogramApi } from './hooks/use_unified_histogram';
export const createMockUnifiedHistogramApi = () => { export const createMockUnifiedHistogramApi = () => {
const api: UnifiedHistogramApi = { const api: UnifiedHistogramApi = {

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/unified-histogram",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}

View file

@ -34,8 +34,8 @@ import {
mapVisToChartType, mapVisToChartType,
computeInterval, computeInterval,
} from '@kbn/visualization-utils'; } from '@kbn/visualization-utils';
import { LegendSize } from '@kbn/visualizations-plugin/public'; import type { LegendSize } from '@kbn/visualizations-plugin/public';
import { XYConfiguration } from '@kbn/visualizations-plugin/common'; import type { XYConfiguration } from '@kbn/visualizations-plugin/common';
import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common'; import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { fieldSupportsBreakdown } from '@kbn/field-utils'; import { fieldSupportsBreakdown } from '@kbn/field-utils';
@ -413,6 +413,7 @@ export class LensVisService {
}, },
}; };
const legendSize: `${LegendSize.EXTRA_LARGE}` = 'xlarge';
const visualizationState = { const visualizationState = {
layers: [ layers: [
{ {
@ -435,7 +436,7 @@ export class LensVisService {
legend: { legend: {
isVisible: true, isVisible: true,
position: 'right', position: 'right',
legendSize: LegendSize.EXTRA_LARGE, legendSize,
shouldTruncate: false, shouldTruncate: false,
}, },
preferredSeriesType: 'bar_stacked', preferredSeriesType: 'bar_stacked',

View file

@ -8,9 +8,9 @@
*/ */
import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { UnifiedHistogramFetchStatus } from '../..'; import { UnifiedHistogramFetchStatus } from '..';
import { unifiedHistogramServicesMock } from '../../__mocks__/services'; import { unifiedHistogramServicesMock } from '../__mocks__/services';
import { lensAdaptersMock } from '../../__mocks__/lens_adapters'; import { lensAdaptersMock } from '../__mocks__/lens_adapters';
import { import {
getChartHidden, getChartHidden,
getTopPanelHeight, getTopPanelHeight,

View file

@ -10,15 +10,15 @@
import type { RequestAdapter } from '@kbn/inspector-plugin/common'; import type { RequestAdapter } from '@kbn/inspector-plugin/common';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { PublishingSubject } from '@kbn/presentation-publishing'; import { PublishingSubject } from '@kbn/presentation-publishing';
import { UnifiedHistogramFetchStatus } from '../..'; import { UnifiedHistogramFetchStatus } from '..';
import type { UnifiedHistogramServices, UnifiedHistogramChartLoadEvent } from '../../types'; import type { UnifiedHistogramServices, UnifiedHistogramChartLoadEvent } from '../types';
import { import {
getChartHidden, getChartHidden,
getTopPanelHeight, getTopPanelHeight,
setChartHidden, setChartHidden,
setTopPanelHeight, setTopPanelHeight,
} from '../utils/local_storage_utils'; } from '../utils/local_storage_utils';
import type { UnifiedHistogramSuggestionContext } from '../../types'; import type { UnifiedHistogramSuggestionContext } from '../types';
/** /**
* The current state of the container * The current state of the container

View file

@ -2,40 +2,39 @@
"extends": "../../../../../tsconfig.base.json", "extends": "../../../../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "target/types", "outDir": "target/types",
"types": ["jest", "node", "react", "@emotion/react/types/css-prop"]
}, },
"include": [ "../../../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*"], "include": ["**/*.ts", "**/*.tsx"],
"exclude": ["target/**/*"],
"kbn_references": [ "kbn_references": [
"@kbn/core",
"@kbn/data-plugin",
"@kbn/data-views-plugin", "@kbn/data-views-plugin",
"@kbn/lens-plugin",
"@kbn/field-formats-plugin",
"@kbn/inspector-plugin",
"@kbn/expressions-plugin", "@kbn/expressions-plugin",
"@kbn/test-jest-helpers", "@kbn/lens-plugin",
"@kbn/i18n", "@kbn/data-plugin",
"@kbn/es-query", "@kbn/field-formats-plugin",
"@kbn/core-ui-settings-browser", "@kbn/data-view-utils",
"@kbn/datemath",
"@kbn/core-ui-settings-browser-mocks",
"@kbn/shared-ux-utility",
"@kbn/ui-actions-plugin",
"@kbn/kibana-utils-plugin",
"@kbn/visualizations-plugin",
"@kbn/resizable-layout",
"@kbn/shared-ux-button-toolbar",
"@kbn/calculate-width-from-char-count",
"@kbn/lens-embeddable-utils",
"@kbn/i18n-react",
"@kbn/field-utils", "@kbn/field-utils",
"@kbn/esql-utils", "@kbn/esql-utils",
"@kbn/discover-utils", "@kbn/i18n",
"@kbn/visualization-utils", "@kbn/test-jest-helpers",
"@kbn/search-types", "@kbn/core",
"@kbn/shared-ux-button-toolbar",
"@kbn/es-query",
"@kbn/presentation-publishing", "@kbn/presentation-publishing",
"@kbn/data-view-utils", "@kbn/inspector-plugin",
], "@kbn/search-types",
"exclude": [ "@kbn/discover-utils",
"target/**/*", "@kbn/ui-actions-plugin",
"@kbn/core-ui-settings-browser-mocks",
"@kbn/core-ui-settings-browser",
"@kbn/datemath",
"@kbn/shared-ux-utility",
"@kbn/i18n-react",
"@kbn/calculate-width-from-char-count",
"@kbn/resizable-layout",
"@kbn/visualization-utils",
"@kbn/visualizations-plugin",
"@kbn/kibana-utils-plugin",
"@kbn/lens-embeddable-utils"
] ]
} }

View file

@ -7,7 +7,7 @@
* 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 { Storage } from '@kbn/kibana-utils-plugin/public'; import type { Storage } from '@kbn/kibana-utils-plugin/public';
export const CHART_HIDDEN_KEY = 'chartHidden'; export const CHART_HIDDEN_KEY = 'chartHidden';
export const HISTOGRAM_HEIGHT_KEY = 'histogramHeight'; export const HISTOGRAM_HEIGHT_KEY = 'histogramHeight';

View file

@ -30,7 +30,6 @@
"expressions", "expressions",
"unifiedDocViewer", "unifiedDocViewer",
"unifiedSearch", "unifiedSearch",
"unifiedHistogram",
"contentManagement", "contentManagement",
"discoverShared" "discoverShared"
], ],

View file

@ -17,12 +17,14 @@ import type { RuntimeStateManager } from '../application/main/state_management/r
import { import {
createInternalStateStore, createInternalStateStore,
createRuntimeStateManager, createRuntimeStateManager,
selectTabRuntimeState,
} from '../application/main/state_management/redux'; } from '../application/main/state_management/redux';
import type { DiscoverServices, HistoryLocationState } from '../build_services'; import type { DiscoverServices, HistoryLocationState } from '../build_services';
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import type { History } from 'history'; import type { History } from 'history';
import type { DiscoverCustomizationContext } from '../customizations'; import type { DiscoverCustomizationContext } from '../customizations';
import { createCustomizationService } from '../customizations/customization_service';
export function getDiscoverStateMock({ export function getDiscoverStateMock({
isTimeBased = true, isTimeBased = true,
@ -71,6 +73,15 @@ export function getDiscoverStateMock({
internalState, internalState,
runtimeStateManager, runtimeStateManager,
}); });
const tabRuntimeState = selectTabRuntimeState(
runtimeStateManager,
internalState.getState().tabs.unsafeCurrentId
);
tabRuntimeState.customizationService$.next({
...createCustomizationService(),
cleanup: async () => {},
});
tabRuntimeState.stateContainer$.next(container);
if (savedSearch !== false) { if (savedSearch !== false) {
container.savedSearchState.set( container.savedSearchState.set(
savedSearch ? savedSearch : isTimeBased ? savedSearchMockWithTimeField : savedSearchMock savedSearch ? savedSearch : isTimeBased ? savedSearchMockWithTimeField : savedSearchMock

View file

@ -63,6 +63,9 @@ export function createDiscoverServicesMock(): DiscoverServices {
dataPlugin.query.timefilter.timefilter.getTime = jest.fn(() => { dataPlugin.query.timefilter.timefilter.getTime = jest.fn(() => {
return { from: 'now-15m', to: 'now' }; return { from: 'now-15m', to: 'now' };
}); });
dataPlugin.query.timefilter.timefilter.getTimeDefaults = jest.fn(() => {
return { from: 'now-15m', to: 'now' };
});
dataPlugin.query.timefilter.timefilter.getRefreshInterval = jest.fn(() => { dataPlugin.query.timefilter.timefilter.getRefreshInterval = jest.fn(() => {
return { pause: true, value: 1000 }; return { pause: true, value: 1000 };
}); });

View file

@ -0,0 +1,169 @@
/*
* 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, { type PropsWithChildren, useCallback, useEffect, useRef } from 'react';
import { type HtmlPortalNode, InPortal, createHtmlPortalNode } from 'react-reverse-portal';
import { UnifiedHistogramChart, useUnifiedHistogram } from '@kbn/unified-histogram';
import { DiscoverCustomizationProvider } from '../../../../customizations';
import {
useInternalStateSelector,
type RuntimeStateManager,
selectTabRuntimeState,
useRuntimeState,
CurrentTabProvider,
RuntimeStateProvider,
useCurrentTabSelector,
} from '../../state_management/redux';
import type { DiscoverMainContentProps } from '../layout/discover_main_content';
import { DiscoverMainProvider } from '../../state_management/discover_state_provider';
import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { useDiscoverHistogram } from './use_discover_histogram';
export type ChartPortalNode = HtmlPortalNode;
export type ChartPortalNodes = Record<string, ChartPortalNode>;
export const ChartPortalsRenderer = ({
runtimeStateManager,
children,
}: PropsWithChildren<{
runtimeStateManager: RuntimeStateManager;
}>) => {
const allTabIds = useInternalStateSelector((state) => state.tabs.allIds);
const currentTabId = useInternalStateSelector((state) => state.tabs.unsafeCurrentId);
const chartPortalNodes = useRef<ChartPortalNodes>({});
chartPortalNodes.current = updatePortals(chartPortalNodes.current, allTabIds);
return (
<>
{Object.keys(chartPortalNodes.current).map((tabId) => {
return (
<InPortal key={tabId} node={chartPortalNodes.current[tabId]}>
<UnifiedHistogramGuard tabId={tabId} runtimeStateManager={runtimeStateManager} />
</InPortal>
);
})}
<CurrentTabProvider
currentTabId={currentTabId}
currentChartPortalNode={chartPortalNodes.current[currentTabId]}
>
{children}
</CurrentTabProvider>
</>
);
};
const updatePortals = (portals: ChartPortalNodes, tabsIds: string[]) =>
tabsIds.reduce<ChartPortalNodes>(
(acc, tabId) => ({
...acc,
[tabId]: portals[tabId] || createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
}),
{}
);
interface UnifiedHistogramGuardProps {
tabId: string;
runtimeStateManager: RuntimeStateManager;
panelsToggle?: DiscoverMainContentProps['panelsToggle'];
}
const UnifiedHistogramGuard = ({
tabId,
runtimeStateManager,
panelsToggle,
}: UnifiedHistogramGuardProps) => {
const isSelected = useInternalStateSelector((state) => state.tabs.unsafeCurrentId === tabId);
const currentTabRuntimeState = selectTabRuntimeState(runtimeStateManager, tabId);
const currentCustomizationService = useRuntimeState(currentTabRuntimeState.customizationService$);
const currentStateContainer = useRuntimeState(currentTabRuntimeState.stateContainer$);
const currentDataView = useRuntimeState(currentTabRuntimeState.currentDataView$);
const adHocDataViews = useRuntimeState(runtimeStateManager.adHocDataViews$);
const isInitialized = useRef(false);
if (
(!isSelected && !isInitialized.current) ||
!currentCustomizationService ||
!currentStateContainer ||
!currentDataView ||
!currentTabRuntimeState
) {
return null;
}
isInitialized.current = true;
return (
<CurrentTabProvider currentTabId={tabId}>
<DiscoverCustomizationProvider value={currentCustomizationService}>
<DiscoverMainProvider value={currentStateContainer}>
<RuntimeStateProvider currentDataView={currentDataView} adHocDataViews={adHocDataViews}>
<UnifiedHistogramChartWrapper
stateContainer={currentStateContainer}
panelsToggle={panelsToggle}
/>
</RuntimeStateProvider>
</DiscoverMainProvider>
</DiscoverCustomizationProvider>
</CurrentTabProvider>
);
};
type UnifiedHistogramChartProps = Pick<UnifiedHistogramGuardProps, 'panelsToggle'> & {
stateContainer: DiscoverStateContainer;
};
const UnifiedHistogramChartWrapper = ({
stateContainer,
panelsToggle,
}: UnifiedHistogramChartProps) => {
const { setUnifiedHistogramApi, ...unifiedHistogramProps } = useDiscoverHistogram(stateContainer);
const unifiedHistogram = useUnifiedHistogram(unifiedHistogramProps);
useEffect(() => {
if (unifiedHistogram.isInitialized) {
setUnifiedHistogramApi(unifiedHistogram.api);
}
}, [setUnifiedHistogramApi, unifiedHistogram.api, unifiedHistogram.isInitialized]);
const currentTabId = useCurrentTabSelector((tab) => tab.id);
useEffect(() => {
if (unifiedHistogram.layoutProps) {
const currentTabRuntimeState = selectTabRuntimeState(
stateContainer.runtimeStateManager,
currentTabId
);
currentTabRuntimeState.unifiedHistogramLayoutProps$.next(unifiedHistogram.layoutProps);
}
}, [currentTabId, stateContainer.runtimeStateManager, unifiedHistogram.layoutProps]);
const isEsqlMode = useIsEsqlMode();
const renderCustomChartToggleActions = useCallback(
() =>
React.isValidElement(panelsToggle)
? React.cloneElement(panelsToggle, { renderedFor: 'histogram' })
: panelsToggle,
[panelsToggle]
);
// Initialized when the first search has been requested or
// when in ES|QL mode since search sessions are not supported
if (!unifiedHistogram.isInitialized || (!unifiedHistogramProps.searchSessionId && !isEsqlMode)) {
return null;
}
return (
<UnifiedHistogramChart
{...unifiedHistogram.chartProps}
renderCustomChartToggleActions={renderCustomChartToggleActions}
/>
);
};

View file

@ -7,5 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
export type { UnifiedHistogramLayoutProps } from './layout'; export { type ChartPortalNode, ChartPortalsRenderer } from './chart_portals_renderer';
export { UnifiedHistogramLayout } from './layout';

View file

@ -7,7 +7,6 @@
* 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 type { ReactElement } from 'react';
import React from 'react'; import React from 'react';
import type { AggregateQuery, Query } from '@kbn/es-query'; import type { AggregateQuery, Query } from '@kbn/es-query';
import { renderHook, act } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react';
@ -15,17 +14,15 @@ import { BehaviorSubject, Subject } from 'rxjs';
import { FetchStatus } from '../../../types'; import { FetchStatus } from '../../../types';
import type { DiscoverStateContainer } from '../../state_management/discover_state'; import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import type { UseDiscoverHistogramProps } from './use_discover_histogram';
import { useDiscoverHistogram } from './use_discover_histogram'; import { useDiscoverHistogram } from './use_discover_histogram';
import { setTimeout } from 'timers/promises'; import { setTimeout } from 'timers/promises';
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
import { DiscoverMainProvider } from '../../state_management/discover_state_provider'; import { DiscoverMainProvider } from '../../state_management/discover_state_provider';
import { RequestAdapter } from '@kbn/inspector-plugin/public'; import { RequestAdapter } from '@kbn/inspector-plugin/public';
import type { UnifiedHistogramState } from '@kbn/unified-histogram-plugin/public'; import type { UnifiedHistogramState } from '@kbn/unified-histogram';
import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram';
import { createMockUnifiedHistogramApi } from '@kbn/unified-histogram-plugin/public/mocks'; import { createMockUnifiedHistogramApi } from '@kbn/unified-histogram/mocks';
import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages';
import type { InspectorAdapters } from '../../hooks/use_inspector';
import type { UnifiedHistogramCustomization } from '../../../../customizations/customization_types/histogram_customization'; import type { UnifiedHistogramCustomization } from '../../../../customizations/customization_types/histogram_customization';
import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverCustomization } from '../../../../customizations';
import type { DiscoverCustomizationId } from '../../../../customizations/customization_service'; import type { DiscoverCustomizationId } from '../../../../customizations/customization_service';
@ -111,44 +108,26 @@ describe('useDiscoverHistogram', () => {
return stateContainer; return stateContainer;
}; };
const renderUseDiscoverHistogram = async ({ const renderUseDiscoverHistogram = async (
stateContainer = getStateContainer(), stateContainer: DiscoverStateContainer = getStateContainer()
inspectorAdapters = { requests: new RequestAdapter() }, ) => {
hideChart = false,
}: {
stateContainer?: DiscoverStateContainer;
inspectorAdapters?: InspectorAdapters;
hideChart?: boolean;
} = {}) => {
const initialProps = {
stateContainer,
inspectorAdapters,
hideChart,
};
const Wrapper = ({ children }: React.PropsWithChildren<unknown>) => ( const Wrapper = ({ children }: React.PropsWithChildren<unknown>) => (
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}> <CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}>
<DiscoverMainProvider value={stateContainer}> <DiscoverMainProvider value={stateContainer}>
<RuntimeStateProvider currentDataView={dataViewMockWithTimeField} adHocDataViews={[]}> <RuntimeStateProvider currentDataView={dataViewMockWithTimeField} adHocDataViews={[]}>
{children as ReactElement} {children}
</RuntimeStateProvider> </RuntimeStateProvider>
</DiscoverMainProvider> </DiscoverMainProvider>
</CurrentTabProvider> </CurrentTabProvider>
); );
const hook = renderHook( const hook = renderHook(() => useDiscoverHistogram(stateContainer), {
(props: UseDiscoverHistogramProps) => { wrapper: Wrapper,
return useDiscoverHistogram(props); });
},
{
wrapper: Wrapper,
initialProps,
}
);
await act(() => setTimeout(0)); await act(() => setTimeout(0));
return { hook, initialProps }; return { hook };
}; };
beforeEach(() => { beforeEach(() => {
@ -169,9 +148,9 @@ describe('useDiscoverHistogram', () => {
}); });
describe('initialization', () => { describe('initialization', () => {
it('should return the expected parameters from getCreationOptions', async () => { it('should return the expected parameters', async () => {
const { hook } = await renderUseDiscoverHistogram(); const { hook } = await renderUseDiscoverHistogram();
const params = hook.result.current.getCreationOptions(); const params = hook.result.current;
expect(params?.localStorageKeyPrefix).toBe('discover'); expect(params?.localStorageKeyPrefix).toBe('discover');
expect(Object.keys(params?.initialState ?? {})).toEqual([ expect(Object.keys(params?.initialState ?? {})).toEqual([
'chartHidden', 'chartHidden',
@ -200,7 +179,7 @@ describe('useDiscoverHistogram', () => {
const api = createMockUnifiedHistogramApi(); const api = createMockUnifiedHistogramApi();
jest.spyOn(api.state$, 'subscribe'); jest.spyOn(api.state$, 'subscribe');
act(() => { act(() => {
hook.result.current.ref(api); hook.result.current.setUnifiedHistogramApi(api);
}); });
expect(api.state$.subscribe).toHaveBeenCalledTimes(2); expect(api.state$.subscribe).toHaveBeenCalledTimes(2);
}); });
@ -208,7 +187,8 @@ describe('useDiscoverHistogram', () => {
it('should sync Unified Histogram state with the state container', async () => { it('should sync Unified Histogram state with the state container', async () => {
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
const inspectorAdapters = { requests: new RequestAdapter(), lensRequests: undefined }; const inspectorAdapters = { requests: new RequestAdapter(), lensRequests: undefined };
const { hook } = await renderUseDiscoverHistogram({ stateContainer, inspectorAdapters }); stateContainer.dataState.inspectorAdapters = inspectorAdapters;
const { hook } = await renderUseDiscoverHistogram(stateContainer);
const lensRequestAdapter = new RequestAdapter(); const lensRequestAdapter = new RequestAdapter();
const state = { const state = {
timeInterval: '1m', timeInterval: '1m',
@ -219,7 +199,7 @@ describe('useDiscoverHistogram', () => {
const api = createMockUnifiedHistogramApi(); const api = createMockUnifiedHistogramApi();
api.state$ = new BehaviorSubject({ ...state, lensRequestAdapter }); api.state$ = new BehaviorSubject({ ...state, lensRequestAdapter });
act(() => { act(() => {
hook.result.current.ref(api); hook.result.current.setUnifiedHistogramApi(api);
}); });
expect(inspectorAdapters.lensRequests).toBe(lensRequestAdapter); expect(inspectorAdapters.lensRequests).toBe(lensRequestAdapter);
expect(stateContainer.appState.update).toHaveBeenCalledWith({ expect(stateContainer.appState.update).toHaveBeenCalledWith({
@ -230,7 +210,7 @@ describe('useDiscoverHistogram', () => {
it('should not sync Unified Histogram state with the state container if there are no changes', async () => { it('should not sync Unified Histogram state with the state container if there are no changes', async () => {
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
const containerState = stateContainer.appState.getState(); const containerState = stateContainer.appState.getState();
const state = { const state = {
timeInterval: containerState.interval, timeInterval: containerState.interval,
@ -241,14 +221,14 @@ describe('useDiscoverHistogram', () => {
const api = createMockUnifiedHistogramApi(); const api = createMockUnifiedHistogramApi();
api.state$ = new BehaviorSubject(state); api.state$ = new BehaviorSubject(state);
act(() => { act(() => {
hook.result.current.ref(api); hook.result.current.setUnifiedHistogramApi(api);
}); });
expect(stateContainer.appState.update).not.toHaveBeenCalled(); expect(stateContainer.appState.update).not.toHaveBeenCalled();
}); });
it('should sync the state container state with Unified Histogram', async () => { it('should sync the state container state with Unified Histogram', async () => {
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
const api = createMockUnifiedHistogramApi(); const api = createMockUnifiedHistogramApi();
let params: Partial<UnifiedHistogramState> = {}; let params: Partial<UnifiedHistogramState> = {};
api.setTotalHits = jest.fn((p) => { api.setTotalHits = jest.fn((p) => {
@ -261,7 +241,7 @@ describe('useDiscoverHistogram', () => {
params = { ...params, timeInterval }; params = { ...params, timeInterval };
}); });
act(() => { act(() => {
hook.result.current.ref(api); hook.result.current.setUnifiedHistogramApi(api);
}); });
stateContainer.appState.update({ hideChart: true, interval: '1m' }); stateContainer.appState.update({ hideChart: true, interval: '1m' });
expect(api.setTotalHits).not.toHaveBeenCalled(); expect(api.setTotalHits).not.toHaveBeenCalled();
@ -272,7 +252,7 @@ describe('useDiscoverHistogram', () => {
it('should exclude totalHitsStatus and totalHitsResult from Unified Histogram state updates', async () => { it('should exclude totalHitsStatus and totalHitsResult from Unified Histogram state updates', async () => {
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
const containerState = stateContainer.appState.getState(); const containerState = stateContainer.appState.getState();
const state = { const state = {
timeInterval: containerState.interval, timeInterval: containerState.interval,
@ -288,7 +268,7 @@ describe('useDiscoverHistogram', () => {
const subject$ = new BehaviorSubject(state); const subject$ = new BehaviorSubject(state);
api.state$ = subject$; api.state$ = subject$;
act(() => { act(() => {
hook.result.current.ref(api); hook.result.current.setUnifiedHistogramApi(api);
}); });
stateContainer.appState.update({ hideChart: true }); stateContainer.appState.update({ hideChart: true });
expect(Object.keys(params ?? {})).toEqual(['chartHidden']); expect(Object.keys(params ?? {})).toEqual(['chartHidden']);
@ -306,7 +286,7 @@ describe('useDiscoverHistogram', () => {
it('should update total hits when the total hits state changes', async () => { it('should update total hits when the total hits state changes', async () => {
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
const containerState = stateContainer.appState.getState(); const containerState = stateContainer.appState.getState();
const state = { const state = {
timeInterval: containerState.interval, timeInterval: containerState.interval,
@ -325,7 +305,7 @@ describe('useDiscoverHistogram', () => {
result: 100, result: 100,
}); });
act(() => { act(() => {
hook.result.current.ref(api); hook.result.current.setUnifiedHistogramApi(api);
}); });
expect(stateContainer.dataState.data$.totalHits$.value).toEqual({ expect(stateContainer.dataState.data$.totalHits$.value).toEqual({
fetchStatus: FetchStatus.COMPLETE, fetchStatus: FetchStatus.COMPLETE,
@ -349,7 +329,7 @@ describe('useDiscoverHistogram', () => {
mockData.query.getState = () => mockQueryState; mockData.query.getState = () => mockQueryState;
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
const containerState = stateContainer.appState.getState(); const containerState = stateContainer.appState.getState();
const error = new Error('test'); const error = new Error('test');
const state = { const state = {
@ -369,7 +349,7 @@ describe('useDiscoverHistogram', () => {
error, error,
}); });
act(() => { act(() => {
hook.result.current.ref(api); hook.result.current.setUnifiedHistogramApi(api);
}); });
expect(sendErrorTo).toHaveBeenCalledWith(stateContainer.dataState.data$.totalHits$); expect(sendErrorTo).toHaveBeenCalledWith(stateContainer.dataState.data$.totalHits$);
expect(stateContainer.dataState.data$.totalHits$.value).toEqual({ expect(stateContainer.dataState.data$.totalHits$.value).toEqual({
@ -384,7 +364,7 @@ describe('useDiscoverHistogram', () => {
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
stateContainer.appState.update({ query: { esql: 'from *' } }); stateContainer.appState.update({ query: { esql: 'from *' } });
stateContainer.dataState.fetchChart$ = fetch$; stateContainer.dataState.fetchChart$ = fetch$;
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
act(() => { act(() => {
fetch$.next(); fetch$.next();
}); });
@ -404,7 +384,7 @@ describe('useDiscoverHistogram', () => {
}, },
}) })
); );
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
act(() => { act(() => {
fetch$.next(); fetch$.next();
}); });
@ -418,10 +398,10 @@ describe('useDiscoverHistogram', () => {
const savedSearchFetch$ = new Subject<void>(); const savedSearchFetch$ = new Subject<void>();
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
stateContainer.dataState.fetchChart$ = savedSearchFetch$; stateContainer.dataState.fetchChart$ = savedSearchFetch$;
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
const api = createMockUnifiedHistogramApi(); const api = createMockUnifiedHistogramApi();
act(() => { act(() => {
hook.result.current.ref(api); hook.result.current.setUnifiedHistogramApi(api);
}); });
expect(api.fetch).not.toHaveBeenCalled(); expect(api.fetch).not.toHaveBeenCalled();
act(() => { act(() => {
@ -435,7 +415,7 @@ describe('useDiscoverHistogram', () => {
test('should use custom values provided by customization fwk ', async () => { test('should use custom values provided by customization fwk ', async () => {
mockUseCustomizations = true; mockUseCustomizations = true;
const stateContainer = getStateContainer(); const stateContainer = getStateContainer();
const { hook } = await renderUseDiscoverHistogram({ stateContainer }); const { hook } = await renderUseDiscoverHistogram(stateContainer);
expect(hook.result.current.onFilter).toEqual(mockHistogramCustomization.onFilter); expect(hook.result.current.onFilter).toEqual(mockHistogramCustomization.onFilter);
expect(hook.result.current.onBrushEnd).toEqual(mockHistogramCustomization.onBrushEnd); expect(hook.result.current.onBrushEnd).toEqual(mockHistogramCustomization.onBrushEnd);

View file

@ -10,16 +10,15 @@
import { useQuerySubscriber } from '@kbn/unified-field-list/src/hooks/use_query_subscriber'; import { useQuerySubscriber } from '@kbn/unified-field-list/src/hooks/use_query_subscriber';
import type { import type {
UnifiedHistogramApi, UnifiedHistogramApi,
UnifiedHistogramContainerProps,
UnifiedHistogramCreationOptions,
UnifiedHistogramState, UnifiedHistogramState,
UnifiedHistogramVisContext, UnifiedHistogramVisContext,
} from '@kbn/unified-histogram-plugin/public'; UseUnifiedHistogramProps,
} from '@kbn/unified-histogram';
import { import {
canImportVisContext, canImportVisContext,
UnifiedHistogramExternalVisContextStatus, UnifiedHistogramExternalVisContextStatus,
UnifiedHistogramFetchStatus, UnifiedHistogramFetchStatus,
} from '@kbn/unified-histogram-plugin/public'; } from '@kbn/unified-histogram';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
@ -43,7 +42,6 @@ import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common';
import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverCustomization } from '../../../../customizations';
import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { FetchStatus } from '../../../types'; import { FetchStatus } from '../../../types';
import type { InspectorAdapters } from '../../hooks/use_inspector';
import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages';
import type { DiscoverStateContainer } from '../../state_management/discover_state'; import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { addLog } from '../../../../utils/add_log'; import { addLog } from '../../../../utils/add_log';
@ -65,25 +63,15 @@ import {
const EMPTY_ESQL_COLUMNS: DatatableColumn[] = []; const EMPTY_ESQL_COLUMNS: DatatableColumn[] = [];
const EMPTY_FILTERS: Filter[] = []; const EMPTY_FILTERS: Filter[] = [];
export interface UseDiscoverHistogramProps { export const useDiscoverHistogram = (
stateContainer: DiscoverStateContainer; stateContainer: DiscoverStateContainer
inspectorAdapters: InspectorAdapters; ): UseUnifiedHistogramProps & { setUnifiedHistogramApi: (api: UnifiedHistogramApi) => void } => {
hideChart: boolean | undefined;
}
export const useDiscoverHistogram = ({
stateContainer,
inspectorAdapters,
hideChart,
}: UseDiscoverHistogramProps): Omit<
UnifiedHistogramContainerProps,
'container' | 'getCreationOptions'
> & {
ref: (api: UnifiedHistogramApi | null) => void;
getCreationOptions: () => UnifiedHistogramCreationOptions;
} => {
const services = useDiscoverServices(); const services = useDiscoverServices();
const { main$, documents$, totalHits$ } = stateContainer.dataState.data$; const {
data$: { main$, documents$, totalHits$ },
inspectorAdapters,
getAbortController,
} = stateContainer.dataState;
const savedSearchState = useSavedSearch(); const savedSearchState = useSavedSearch();
const isEsqlMode = useIsEsqlMode(); const isEsqlMode = useIsEsqlMode();
@ -91,53 +79,38 @@ export const useDiscoverHistogram = ({
* API initialization * API initialization
*/ */
const [unifiedHistogram, ref] = useState<UnifiedHistogramApi | null>(); const [unifiedHistogramApi, setUnifiedHistogramApi] = useState<UnifiedHistogramApi>();
const [isSuggestionLoading, setIsSuggestionLoading] = useState(false); const [isSuggestionLoading, setIsSuggestionLoading] = useState(false);
const getCreationOptions = useCallback(() => {
const { hideChart: chartHidden, interval: timeInterval } = stateContainer.appState.getState();
return {
localStorageKeyPrefix: 'discover',
disableAutoFetching: true,
initialState: {
chartHidden,
timeInterval,
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
totalHitsResult: undefined,
},
};
}, [stateContainer.appState]);
/** /**
* Sync Unified Histogram state with Discover state * Sync Unified Histogram state with Discover state
*/ */
useEffect(() => { useEffect(() => {
const subscription = createUnifiedHistogramStateObservable(unifiedHistogram?.state$)?.subscribe( const subscription = createUnifiedHistogramStateObservable(
(changes) => { unifiedHistogramApi?.state$
const { lensRequestAdapter, ...stateChanges } = changes; )?.subscribe((changes) => {
const appState = stateContainer.appState.getState(); const { lensRequestAdapter, ...stateChanges } = changes;
const oldState = { const appState = stateContainer.appState.getState();
hideChart: appState.hideChart, const oldState = {
interval: appState.interval, hideChart: appState.hideChart,
}; interval: appState.interval,
const newState = { ...oldState, ...stateChanges }; };
const newState = { ...oldState, ...stateChanges };
if ('lensRequestAdapter' in changes) { if ('lensRequestAdapter' in changes) {
inspectorAdapters.lensRequests = lensRequestAdapter; inspectorAdapters.lensRequests = lensRequestAdapter;
}
if (!isEqual(oldState, newState)) {
stateContainer.appState.update(newState);
}
} }
);
if (!isEqual(oldState, newState)) {
stateContainer.appState.update(newState);
}
});
return () => { return () => {
subscription?.unsubscribe(); subscription?.unsubscribe();
}; };
}, [inspectorAdapters, stateContainer.appState, unifiedHistogram?.state$]); }, [inspectorAdapters, stateContainer.appState, unifiedHistogramApi?.state$]);
/** /**
* Sync URL query params with Unified Histogram * Sync URL query params with Unified Histogram
@ -147,11 +120,11 @@ export const useDiscoverHistogram = ({
const subscription = createAppStateObservable(stateContainer.appState.state$).subscribe( const subscription = createAppStateObservable(stateContainer.appState.state$).subscribe(
(changes) => { (changes) => {
if ('timeInterval' in changes && changes.timeInterval) { if ('timeInterval' in changes && changes.timeInterval) {
unifiedHistogram?.setTimeInterval(changes.timeInterval); unifiedHistogramApi?.setTimeInterval(changes.timeInterval);
} }
if ('chartHidden' in changes && typeof changes.chartHidden === 'boolean') { if ('chartHidden' in changes && typeof changes.chartHidden === 'boolean') {
unifiedHistogram?.setChartHidden(changes.chartHidden); unifiedHistogramApi?.setChartHidden(changes.chartHidden);
} }
} }
); );
@ -159,7 +132,7 @@ export const useDiscoverHistogram = ({
return () => { return () => {
subscription?.unsubscribe(); subscription?.unsubscribe();
}; };
}, [stateContainer.appState.state$, unifiedHistogram]); }, [stateContainer.appState.state$, unifiedHistogramApi]);
/** /**
* Total hits * Total hits
@ -168,7 +141,7 @@ export const useDiscoverHistogram = ({
const setTotalHitsError = useMemo(() => sendErrorTo(totalHits$), [totalHits$]); const setTotalHitsError = useMemo(() => sendErrorTo(totalHits$), [totalHits$]);
useEffect(() => { useEffect(() => {
const subscription = createTotalHitsObservable(unifiedHistogram?.state$)?.subscribe( const subscription = createTotalHitsObservable(unifiedHistogramApi?.state$)?.subscribe(
({ status, result }) => { ({ status, result }) => {
if (isEsqlMode) { if (isEsqlMode) {
// ignore histogram's total hits updates for ES|QL as Discover manages them during docs fetching // ignore histogram's total hits updates for ES|QL as Discover manages them during docs fetching
@ -221,7 +194,7 @@ export const useDiscoverHistogram = ({
totalHits$, totalHits$,
setTotalHitsError, setTotalHitsError,
stateContainer.appState, stateContainer.appState,
unifiedHistogram?.state$, unifiedHistogramApi?.state$,
]); ]);
/** /**
@ -282,7 +255,7 @@ export const useDiscoverHistogram = ({
// Handle unified histogram refetching // Handle unified histogram refetching
useEffect(() => { useEffect(() => {
if (!unifiedHistogram) { if (!unifiedHistogramApi) {
return; return;
} }
@ -297,7 +270,7 @@ export const useDiscoverHistogram = ({
// a refetch anyway and result in multiple unnecessary fetches. // a refetch anyway and result in multiple unnecessary fetches.
if (isEsqlMode) { if (isEsqlMode) {
fetchChart$ = merge( fetchChart$ = merge(
createCurrentSuggestionObservable(unifiedHistogram.state$).pipe(map(() => 'lens')), createCurrentSuggestionObservable(unifiedHistogramApi.state$).pipe(map(() => 'lens')),
esqlFetchComplete$.pipe(map(() => 'discover')) esqlFetchComplete$.pipe(map(() => 'discover'))
).pipe(debounceTime(50)); ).pipe(debounceTime(50));
} else { } else {
@ -307,13 +280,13 @@ export const useDiscoverHistogram = ({
const subscription = fetchChart$.subscribe((source) => { const subscription = fetchChart$.subscribe((source) => {
if (source === 'discover') addLog('Unified Histogram - Discover refetch'); if (source === 'discover') addLog('Unified Histogram - Discover refetch');
if (source === 'lens') addLog('Unified Histogram - Lens suggestion refetch'); if (source === 'lens') addLog('Unified Histogram - Lens suggestion refetch');
unifiedHistogram.fetch(); unifiedHistogramApi.fetch();
}); });
return () => { return () => {
subscription.unsubscribe(); subscription.unsubscribe();
}; };
}, [isEsqlMode, stateContainer.dataState.fetchChart$, esqlFetchComplete$, unifiedHistogram]); }, [isEsqlMode, stateContainer.dataState.fetchChart$, esqlFetchComplete$, unifiedHistogramApi]);
const dataView = useCurrentDataView(); const dataView = useCurrentDataView();
@ -380,10 +353,12 @@ export const useDiscoverHistogram = ({
[dispatch, setOverriddenVisContextAfterInvalidation, stateContainer.savedSearchState] [dispatch, setOverriddenVisContextAfterInvalidation, stateContainer.savedSearchState]
); );
const chartHidden = useAppStateSelector((state) => state.hideChart);
const timeInterval = useAppStateSelector((state) => state.interval);
const breakdownField = useAppStateSelector((state) => state.breakdownField); const breakdownField = useAppStateSelector((state) => state.breakdownField);
const onBreakdownFieldChange = useCallback< const onBreakdownFieldChange = useCallback<
NonNullable<UnifiedHistogramContainerProps['onBreakdownFieldChange']> NonNullable<UseUnifiedHistogramProps['onBreakdownFieldChange']>
>( >(
(nextBreakdownField) => { (nextBreakdownField) => {
if (nextBreakdownField !== breakdownField) { if (nextBreakdownField !== breakdownField) {
@ -394,9 +369,17 @@ export const useDiscoverHistogram = ({
); );
return { return {
ref, setUnifiedHistogramApi,
getCreationOptions,
services, services,
localStorageKeyPrefix: 'discover',
requestAdapter: inspectorAdapters.requests,
abortController: getAbortController(),
initialState: {
chartHidden,
timeInterval,
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
totalHitsResult: undefined,
},
dataView: isEsqlMode ? esqlDataView : dataView, dataView: isEsqlMode ? esqlDataView : dataView,
query: isEsqlMode ? esqlQuery : query, query: isEsqlMode ? esqlQuery : query,
filters: filtersMemoized, filters: filtersMemoized,

View file

@ -36,10 +36,17 @@ import { act } from 'react-dom/test-utils';
import { PanelsToggle } from '../../../../components/panels_toggle'; import { PanelsToggle } from '../../../../components/panels_toggle';
import { createDataViewDataSource } from '../../../../../common/data_sources'; import { createDataViewDataSource } from '../../../../../common/data_sources';
import { import {
CurrentTabProvider, InternalStateProvider,
RuntimeStateProvider, RuntimeStateProvider,
internalStateActions, internalStateActions,
} from '../../state_management/redux'; } from '../../state_management/redux';
import { ChartPortalsRenderer } from '../chart';
import { UnifiedHistogramChart } from '@kbn/unified-histogram';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
useResizeObserver: jest.fn(() => ({ width: 1000, height: 1000 })),
}));
function getStateContainer({ function getStateContainer({
savedSearch, savedSearch,
@ -155,13 +162,15 @@ const mountComponent = async ({
const component = mountWithIntl( const component = mountWithIntl(
<KibanaRenderContextProvider {...services.core}> <KibanaRenderContextProvider {...services.core}>
<KibanaContextProvider services={services}> <KibanaContextProvider services={services}>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}> <InternalStateProvider store={stateContainer.internalState}>
<DiscoverMainProvider value={stateContainer}> <ChartPortalsRenderer runtimeStateManager={stateContainer.runtimeStateManager}>
<RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}> <DiscoverMainProvider value={stateContainer}>
<DiscoverHistogramLayout {...props} /> <RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}>
</RuntimeStateProvider> <DiscoverHistogramLayout {...props} />
</DiscoverMainProvider> </RuntimeStateProvider>
</CurrentTabProvider> </DiscoverMainProvider>
</ChartPortalsRenderer>
</InternalStateProvider>
</KibanaContextProvider> </KibanaContextProvider>
</KibanaRenderContextProvider> </KibanaRenderContextProvider>
); );
@ -177,19 +186,19 @@ const mountComponent = async ({
describe('Discover histogram layout component', () => { describe('Discover histogram layout component', () => {
describe('render', () => { describe('render', () => {
it('should render null if there is no search session', async () => { it('should not render chart if there is no search session', async () => {
const { component } = await mountComponent({ searchSessionId: null }); const { component } = await mountComponent({ searchSessionId: null });
expect(component.isEmptyRender()).toBe(true); expect(component.exists(UnifiedHistogramChart)).toBe(false);
}); });
it('should not render null if there is a search session', async () => { it('should render chart if there is a search session', async () => {
const { component } = await mountComponent(); const { component } = await mountComponent();
expect(component.isEmptyRender()).toBe(false); expect(component.exists(UnifiedHistogramChart)).toBe(true);
}, 10000); }, 10000);
it('should not render null if there is no search session, but isEsqlMode is true', async () => { it('should render chart if there is no search session, but isEsqlMode is true', async () => {
const { component } = await mountComponent({ isEsqlMode: true }); const { component } = await mountComponent({ isEsqlMode: true });
expect(component.isEmptyRender()).toBe(false); expect(component.exists(UnifiedHistogramChart)).toBe(true);
}); });
it('should render PanelsToggle', async () => { it('should render PanelsToggle', async () => {

View file

@ -7,67 +7,40 @@
* 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, { useCallback } from 'react'; import React from 'react';
import { UnifiedHistogramContainer } from '@kbn/unified-histogram-plugin/public'; import { UnifiedHistogramLayout } from '@kbn/unified-histogram';
import { css } from '@emotion/react'; import { OutPortal } from 'react-reverse-portal';
import { useDiscoverHistogram } from './use_discover_histogram';
import { type DiscoverMainContentProps, DiscoverMainContent } from './discover_main_content'; import { type DiscoverMainContentProps, DiscoverMainContent } from './discover_main_content';
import { useAppStateSelector } from '../../state_management/discover_app_state_container'; import { useCurrentChartPortalNode, useCurrentTabRuntimeState } from '../../state_management/redux';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps { export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps {
container: HTMLElement | null; container: HTMLElement | null;
} }
const histogramLayoutCss = css`
height: 100%;
`;
export const DiscoverHistogramLayout = ({ export const DiscoverHistogramLayout = ({
dataView,
stateContainer,
container, container,
panelsToggle, panelsToggle,
...mainContentProps ...mainContentProps
}: DiscoverHistogramLayoutProps) => { }: DiscoverHistogramLayoutProps) => {
const { dataState } = stateContainer; const chartPortalNode = useCurrentChartPortalNode();
const hideChart = useAppStateSelector((state) => state.hideChart); const layoutProps = useCurrentTabRuntimeState(
const isEsqlMode = useIsEsqlMode(); mainContentProps.stateContainer.runtimeStateManager,
const unifiedHistogramProps = useDiscoverHistogram({ (tab) => tab.unifiedHistogramLayoutProps$
stateContainer,
inspectorAdapters: dataState.inspectorAdapters,
hideChart,
});
const renderCustomChartToggleActions = useCallback(
() =>
React.isValidElement(panelsToggle)
? React.cloneElement(panelsToggle, { renderedFor: 'histogram' })
: panelsToggle,
[panelsToggle]
); );
// Initialized when the first search has been requested or if (!layoutProps) {
// when in ES|QL mode since search sessions are not supported
if (!unifiedHistogramProps.searchSessionId && !isEsqlMode) {
return null; return null;
} }
return ( return (
<UnifiedHistogramContainer <UnifiedHistogramLayout
{...unifiedHistogramProps}
requestAdapter={dataState.inspectorAdapters.requests}
container={container} container={container}
css={histogramLayoutCss} unifiedHistogramChart={
renderCustomChartToggleActions={renderCustomChartToggleActions} chartPortalNode ? <OutPortal node={chartPortalNode} panelsToggle={panelsToggle} /> : null
abortController={stateContainer.dataState.getAbortController()} }
{...layoutProps}
> >
<DiscoverMainContent <DiscoverMainContent {...mainContentProps} panelsToggle={panelsToggle} />
{...mainContentProps} </UnifiedHistogramLayout>
stateContainer={stateContainer}
dataView={dataView}
panelsToggle={panelsToggle}
/>
</UnifiedHistogramContainer>
); );
}; };

View file

@ -40,10 +40,11 @@ import { ErrorCallout } from '../../../../components/common/error_callout';
import { PanelsToggle } from '../../../../components/panels_toggle'; import { PanelsToggle } from '../../../../components/panels_toggle';
import { createDataViewDataSource } from '../../../../../common/data_sources'; import { createDataViewDataSource } from '../../../../../common/data_sources';
import { import {
CurrentTabProvider, InternalStateProvider,
RuntimeStateProvider, RuntimeStateProvider,
internalStateActions, internalStateActions,
} from '../../state_management/redux'; } from '../../state_management/redux';
import { ChartPortalsRenderer } from '../chart';
jest.mock('@elastic/eui', () => ({ jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'), ...jest.requireActual('@elastic/eui'),
@ -137,15 +138,17 @@ async function mountComponent(
const component = mountWithIntl( const component = mountWithIntl(
<KibanaContextProvider services={services}> <KibanaContextProvider services={services}>
<CurrentTabProvider currentTabId={stateContainer.getCurrentTab().id}> <InternalStateProvider store={stateContainer.internalState}>
<DiscoverMainProvider value={stateContainer}> <ChartPortalsRenderer runtimeStateManager={stateContainer.runtimeStateManager}>
<RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}> <DiscoverMainProvider value={stateContainer}>
<EuiProvider highContrastMode={false}> <RuntimeStateProvider currentDataView={dataView} adHocDataViews={[]}>
<DiscoverLayout {...props} /> <EuiProvider highContrastMode={false}>
</EuiProvider> <DiscoverLayout {...props} />
</RuntimeStateProvider> </EuiProvider>
</DiscoverMainProvider> </RuntimeStateProvider>
</CurrentTabProvider> </DiscoverMainProvider>
</ChartPortalsRenderer>
</InternalStateProvider>
</KibanaContextProvider>, </KibanaContextProvider>,
mountOptions mountOptions
); );

View file

@ -49,7 +49,6 @@ import type { SidebarToggleState } from '../../../types';
import { FetchStatus } from '../../../types'; import { FetchStatus } from '../../../types';
import { useDataState } from '../../hooks/use_data_state'; import { useDataState } from '../../hooks/use_data_state';
import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout'; import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout';
import { DiscoverHistogramLayout } from './discover_histogram_layout';
import { ErrorCallout } from '../../../../components/common/error_callout'; import { ErrorCallout } from '../../../../components/common/error_callout';
import { addLog } from '../../../../utils/add_log'; import { addLog } from '../../../../utils/add_log';
import { DiscoverResizableLayout } from './discover_resizable_layout'; import { DiscoverResizableLayout } from './discover_resizable_layout';
@ -59,6 +58,7 @@ import { sendErrorMsg } from '../../hooks/use_saved_search_messages';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { useCurrentDataView, useCurrentTabSelector } from '../../state_management/redux'; import { useCurrentDataView, useCurrentTabSelector } from '../../state_management/redux';
import { TABS_ENABLED } from '../../../../constants'; import { TABS_ENABLED } from '../../../../constants';
import { DiscoverHistogramLayout } from './discover_histogram_layout';
const SidebarMemoized = React.memo(DiscoverSidebarResponsive); const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
const TopNavMemoized = React.memo(DiscoverTopNav); const TopNavMemoized = React.memo(DiscoverTopNav);

View file

@ -12,7 +12,6 @@ import React, { useState } from 'react';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { DiscoverSessionView, type DiscoverSessionViewProps } from '../session_view'; import { DiscoverSessionView, type DiscoverSessionViewProps } from '../session_view';
import { import {
CurrentTabProvider,
createTabItem, createTabItem,
internalStateActions, internalStateActions,
selectAllTabs, selectAllTabs,
@ -34,17 +33,10 @@ export const TabsView = (props: DiscoverSessionViewProps) => {
<UnifiedTabs <UnifiedTabs
services={services} services={services}
initialItems={initialItems} initialItems={initialItems}
onChanged={(updateState) => { onChanged={(updateState) => dispatch(internalStateActions.updateTabs(updateState))}
const updateTabsAction = internalStateActions.updateTabs(updateState);
return dispatch(updateTabsAction);
}}
createItem={() => createTabItem(allTabs)} createItem={() => createTabItem(allTabs)}
getPreviewData={getPreviewData} getPreviewData={getPreviewData}
renderContent={() => ( renderContent={() => <DiscoverSessionView key={currentTabId} {...props} />}
<CurrentTabProvider currentTabId={currentTabId}>
<DiscoverSessionView key={currentTabId} {...props} />
</CurrentTabProvider>
)}
/> />
); );
}; };

View file

@ -21,7 +21,6 @@ import {
createInternalStateStore, createInternalStateStore,
createRuntimeStateManager, createRuntimeStateManager,
internalStateActions, internalStateActions,
CurrentTabProvider,
} from './state_management/redux'; } from './state_management/redux';
import type { RootProfileState } from '../../context_awareness'; import type { RootProfileState } from '../../context_awareness';
import { useRootProfile, useDefaultAdHocDataViews } from '../../context_awareness'; import { useRootProfile, useDefaultAdHocDataViews } from '../../context_awareness';
@ -35,6 +34,7 @@ import {
import { useAsyncFunction } from './hooks/use_async_function'; import { useAsyncFunction } from './hooks/use_async_function';
import { TabsView } from './components/tabs_view'; import { TabsView } from './components/tabs_view';
import { TABS_ENABLED } from '../../constants'; import { TABS_ENABLED } from '../../constants';
import { ChartPortalsRenderer } from './components/chart';
export interface MainRouteProps { export interface MainRouteProps {
customizationContext: DiscoverCustomizationContext; customizationContext: DiscoverCustomizationContext;
@ -142,13 +142,13 @@ export const DiscoverMainRoute = ({
return ( return (
<InternalStateProvider store={internalState}> <InternalStateProvider store={internalState}>
<rootProfileState.AppWrapper> <rootProfileState.AppWrapper>
{TABS_ENABLED ? ( <ChartPortalsRenderer runtimeStateManager={sessionViewProps.runtimeStateManager}>
<TabsView {...sessionViewProps} /> {TABS_ENABLED ? (
) : ( <TabsView {...sessionViewProps} />
<CurrentTabProvider currentTabId={internalState.getState().tabs.unsafeCurrentId}> ) : (
<DiscoverSessionView {...sessionViewProps} /> <DiscoverSessionView {...sessionViewProps} />
</CurrentTabProvider> )}
)} </ChartPortalsRenderer>
</rootProfileState.AppWrapper> </rootProfileState.AppWrapper>
</InternalStateProvider> </InternalStateProvider>
); );

View file

@ -15,8 +15,8 @@ import type { FilterCompareOptions } from '@kbn/es-query';
import { COMPARE_ALL_OPTIONS, isOfAggregateQueryType, updateFilterReferences } from '@kbn/es-query'; import { COMPARE_ALL_OPTIONS, isOfAggregateQueryType, updateFilterReferences } from '@kbn/es-query';
import type { SearchSourceFields } from '@kbn/data-plugin/common'; import type { SearchSourceFields } from '@kbn/data-plugin/common';
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common'; import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public'; import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram';
import { canImportVisContext } from '@kbn/unified-histogram-plugin/public'; import { canImportVisContext } from '@kbn/unified-histogram';
import type { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public'; import type { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
import { isEqual, isFunction } from 'lodash'; import { isEqual, isFunction } from 'lodash';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';

View file

@ -68,12 +68,31 @@ export const initializeSession: InternalStateThunkActionCreator<
dispatch(disconnectTab({ tabId })); dispatch(disconnectTab({ tabId }));
dispatch(internalStateSlice.actions.resetOnSavedSearchChange({ tabId })); dispatch(internalStateSlice.actions.resetOnSavedSearchChange({ tabId }));
const discoverSessionLoadTracker =
services.ebtManager.trackPerformanceEvent('discoverLoadSavedSearch');
const { currentDataView$, stateContainer$, customizationService$ } = selectTabRuntimeState(
runtimeStateManager,
tabId
);
/**
* New tab initialization or existing tab re-initialization
*/
const wasTabInitialized = Boolean(stateContainer$.getValue());
if (wasTabInitialized) {
// Clear existing runtime state on re-initialization
// to ensure no stale state is used during loading
currentDataView$.next(undefined);
stateContainer$.next(undefined);
customizationService$.next(undefined);
}
/** /**
* "No data" checks * "No data" checks
*/ */
const discoverSessionLoadTracker =
services.ebtManager.trackPerformanceEvent('discoverLoadSavedSearch');
const urlState = cleanupUrlState( const urlState = cleanupUrlState(
defaultUrlState ?? urlStateStorage.get<AppStateUrl>(APP_STATE_URL_KEY), defaultUrlState ?? urlStateStorage.get<AppStateUrl>(APP_STATE_URL_KEY),
services.uiSettings services.uiSettings
@ -124,10 +143,6 @@ export const initializeSession: InternalStateThunkActionCreator<
setBreadcrumbs({ services, titleBreadcrumbText: persistedDiscoverSession.title }); setBreadcrumbs({ services, titleBreadcrumbText: persistedDiscoverSession.title });
} }
const { currentDataView$, stateContainer$, customizationService$ } = selectTabRuntimeState(
runtimeStateManager,
tabId
);
let dataView: DataView; let dataView: DataView;
if (isOfAggregateQueryType(initialQuery)) { if (isOfAggregateQueryType(initialQuery)) {

View file

@ -15,7 +15,8 @@ import {
createDispatchHook, createDispatchHook,
createSelectorHook, createSelectorHook,
} from 'react-redux'; } from 'react-redux';
import React, { type PropsWithChildren, useMemo, createContext } from 'react'; import type { PropsWithChildren } from 'react';
import React, { useMemo, createContext } from 'react';
import { useAdHocDataViews } from './runtime_state'; import { useAdHocDataViews } from './runtime_state';
import type { DiscoverInternalState, TabState } from './types'; import type { DiscoverInternalState, TabState } from './types';
import { import {
@ -25,6 +26,7 @@ import {
} from './internal_state'; } from './internal_state';
import { selectTab } from './selectors'; import { selectTab } from './selectors';
import { type TabActionInjector, createTabActionInjector } from './utils'; import { type TabActionInjector, createTabActionInjector } from './utils';
import type { ChartPortalNode } from '../../components/chart';
const internalStateContext = createContext<ReactReduxContextValue>( const internalStateContext = createContext<ReactReduxContextValue>(
// Recommended approach for versions of Redux prior to v9: // Recommended approach for versions of Redux prior to v9:
@ -49,6 +51,7 @@ export const useInternalStateSelector: TypedUseSelectorHook<DiscoverInternalStat
interface CurrentTabContextValue { interface CurrentTabContextValue {
currentTabId: string; currentTabId: string;
currentChartPortalNode?: ChartPortalNode;
injectCurrentTab: TabActionInjector; injectCurrentTab: TabActionInjector;
} }
@ -56,11 +59,16 @@ const currentTabContext = createContext<CurrentTabContextValue | undefined>(unde
export const CurrentTabProvider = ({ export const CurrentTabProvider = ({
currentTabId, currentTabId,
currentChartPortalNode,
children, children,
}: PropsWithChildren<{ currentTabId: string }>) => { }: PropsWithChildren<{ currentTabId: string; currentChartPortalNode?: ChartPortalNode }>) => {
const contextValue = useMemo<CurrentTabContextValue>( const contextValue = useMemo<CurrentTabContextValue>(
() => ({ currentTabId, injectCurrentTab: createTabActionInjector(currentTabId) }), () => ({
[currentTabId] currentTabId,
currentChartPortalNode,
injectCurrentTab: createTabActionInjector(currentTabId),
}),
[currentChartPortalNode, currentTabId]
); );
return <currentTabContext.Provider value={contextValue}>{children}</currentTabContext.Provider>; return <currentTabContext.Provider value={contextValue}>{children}</currentTabContext.Provider>;
@ -88,6 +96,8 @@ export const useCurrentTabAction = <TPayload extends TabActionPayload, TReturn>(
return useMemo(() => injectCurrentTab(actionCreator), [actionCreator, injectCurrentTab]); return useMemo(() => injectCurrentTab(actionCreator), [actionCreator, injectCurrentTab]);
}; };
export const useCurrentChartPortalNode = () => useCurrentTabContext().currentChartPortalNode;
export const useDataViewsForPicker = () => { export const useDataViewsForPicker = () => {
const originalAdHocDataViews = useAdHocDataViews(); const originalAdHocDataViews = useAdHocDataViews();
const savedDataViews = useInternalStateSelector((state) => state.savedDataViews); const savedDataViews = useInternalStateSelector((state) => state.savedDataViews);

View file

@ -52,6 +52,7 @@ export {
CurrentTabProvider, CurrentTabProvider,
useCurrentTabSelector, useCurrentTabSelector,
useCurrentTabAction, useCurrentTabAction,
useCurrentChartPortalNode,
useDataViewsForPicker, useDataViewsForPicker,
} from './hooks'; } from './hooks';

View file

@ -193,11 +193,16 @@ export interface InternalStateThunkDependencies {
urlStateStorage: IKbnUrlStateStorage; urlStateStorage: IKbnUrlStateStorage;
} }
const IS_JEST_ENVIRONMENT = typeof jest !== 'undefined';
export const createInternalStateStore = (options: InternalStateThunkDependencies) => { export const createInternalStateStore = (options: InternalStateThunkDependencies) => {
const store = configureStore({ const store = configureStore({
reducer: internalStateSlice.reducer, reducer: internalStateSlice.reducer,
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ thunk: { extraArgument: options } }), getDefaultMiddleware({
thunk: { extraArgument: options },
serializableCheck: !IS_JEST_ENVIRONMENT,
}),
}); });
// TEMPORARY: Create initial default tab // TEMPORARY: Create initial default tab

View file

@ -11,6 +11,7 @@ import type { DataView } from '@kbn/data-views-plugin/common';
import React, { type PropsWithChildren, createContext, useContext, useMemo } from 'react'; import React, { type PropsWithChildren, createContext, useContext, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable'; import useObservable from 'react-use/lib/useObservable';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import type { UnifiedHistogramPartialLayoutProps } from '@kbn/unified-histogram';
import { useCurrentTabContext } from './hooks'; import { useCurrentTabContext } from './hooks';
import type { DiscoverStateContainer } from '../discover_state'; import type { DiscoverStateContainer } from '../discover_state';
import type { ConnectedCustomizationService } from '../../../../customizations'; import type { ConnectedCustomizationService } from '../../../../customizations';
@ -22,6 +23,7 @@ interface DiscoverRuntimeState {
interface TabRuntimeState { interface TabRuntimeState {
stateContainer?: DiscoverStateContainer; stateContainer?: DiscoverStateContainer;
customizationService?: ConnectedCustomizationService; customizationService?: ConnectedCustomizationService;
unifiedHistogramLayoutProps?: UnifiedHistogramPartialLayoutProps;
currentDataView: DataView; currentDataView: DataView;
} }
@ -45,6 +47,9 @@ export const createRuntimeStateManager = (): RuntimeStateManager => ({
export const createTabRuntimeState = (): ReactiveTabRuntimeState => ({ export const createTabRuntimeState = (): ReactiveTabRuntimeState => ({
stateContainer$: new BehaviorSubject<DiscoverStateContainer | undefined>(undefined), stateContainer$: new BehaviorSubject<DiscoverStateContainer | undefined>(undefined),
customizationService$: new BehaviorSubject<ConnectedCustomizationService | undefined>(undefined), customizationService$: new BehaviorSubject<ConnectedCustomizationService | undefined>(undefined),
unifiedHistogramLayoutProps$: new BehaviorSubject<UnifiedHistogramPartialLayoutProps | undefined>(
undefined
),
currentDataView$: new BehaviorSubject<DataView | undefined>(undefined), currentDataView$: new BehaviorSubject<DataView | undefined>(undefined),
}); });

View file

@ -7,9 +7,15 @@
* 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 { createSelector } from '@reduxjs/toolkit';
import type { DiscoverInternalState } from './types'; import type { DiscoverInternalState } from './types';
export const selectTab = (state: DiscoverInternalState, tabId: string) => state.tabs.byId[tabId]; export const selectTab = (state: DiscoverInternalState, tabId: string) => state.tabs.byId[tabId];
export const selectAllTabs = (state: DiscoverInternalState) => export const selectAllTabs = createSelector(
state.tabs.allIds.map((id) => selectTab(state, id)); [
(state: DiscoverInternalState) => state.tabs.allIds,
(state: DiscoverInternalState) => state.tabs.byId,
],
(allIds, byId) => allIds.map((id) => byId[id])
);

View file

@ -11,7 +11,7 @@ import type { RefreshInterval } from '@kbn/data-plugin/common';
import type { DataViewListItem } from '@kbn/data-views-plugin/public'; import type { DataViewListItem } from '@kbn/data-views-plugin/public';
import type { DataTableRecord } from '@kbn/discover-utils'; import type { DataTableRecord } from '@kbn/discover-utils';
import type { Filter, TimeRange } from '@kbn/es-query'; import type { Filter, TimeRange } from '@kbn/es-query';
import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram-plugin/public'; import type { UnifiedHistogramVisContext } from '@kbn/unified-histogram';
import type { TabItem } from '@kbn/unified-tabs'; import type { TabItem } from '@kbn/unified-tabs';
export enum LoadingStatus { export enum LoadingStatus {

Some files were not shown because too many files have changed in this diff Show more