From fe306b0e44018f064e96e5a0c27617c51882f5cf Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 12 Oct 2022 23:06:49 -0300 Subject: [PATCH 01/84] [Discover] Start updating unified histogram to use Lens --- src/plugins/discover/kibana.json | 3 +- .../layout/discover_main_content.tsx | 1 + src/plugins/discover/public/build_services.ts | 3 + src/plugins/discover/public/plugin.tsx | 2 + src/plugins/unified_histogram/kibana.json | 2 +- .../unified_histogram/public/chart/chart.tsx | 26 +- .../public/chart/histogram.tsx | 352 ++++++------------ .../public/layout/layout.tsx | 4 + src/plugins/unified_histogram/public/types.ts | 2 + src/plugins/unified_histogram/tsconfig.json | 3 +- x-pack/plugins/lens/kibana.json | 3 +- .../public/embeddable/embeddable.test.tsx | 30 +- .../lens/public/embeddable/embeddable.tsx | 15 +- 13 files changed, 174 insertions(+), 272 deletions(-) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index e9d78d844a6e0..238de02475307 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -29,7 +29,8 @@ "usageCollection", "spaces", "triggersActionsUi", - "savedObjectsTaggingOss" + "savedObjectsTaggingOss", + "lens" ], "requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedSearch", "savedSearch"], "extraPublicDirs": ["common"], diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 100412c8f7930..29b2295bd7708 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -106,6 +106,7 @@ export const DiscoverMainContent = ({ ({ @@ -85,17 +86,6 @@ export function Chart({ onChartHiddenChange?.(chartHidden); }, [chart?.hidden, onChartHiddenChange]); - const timefilterUpdateHandler = useCallback( - (ranges: { from: number; to: number }) => { - data.query.timefilter.timefilter.setTime({ - from: moment(ranges.from).toISOString(), - to: moment(ranges.to).toISOString(), - mode: 'absolute', - }); - }, - [data] - ); - const panels = useChartPanels({ chart, toggleHideChart, @@ -106,8 +96,8 @@ export function Chart({ const { euiTheme } = useEuiTheme(); const resultCountCss = css` - padding: ${euiTheme.size.s}; - min-height: ${euiTheme.base * 3}px; + padding: ${euiTheme.size.s} ${euiTheme.size.s} 0 ${euiTheme.size.s}; + min-height: ${euiTheme.base * 2.5}px; `; const resultCountTitleCss = css` ${useEuiBreakpoint(['xs', 's'])} { @@ -214,11 +204,7 @@ export function Chart({ })} css={timechartCss} > - + {appendHistogram} diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index a201258e49bf2..17ddd7c60231e 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -6,96 +6,119 @@ * Side Public License, v 1. */ -import moment, { unitOfTime } from 'moment-timezone'; -import React, { useCallback, useMemo } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiIconTip, - EuiLoadingChart, - EuiSpacer, - EuiText, - useEuiTheme, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiText, useEuiTheme } from '@elastic/eui'; import dateMath from '@kbn/datemath'; -import type { - BrushEndListener, - ElementClickListener, - XYBrushEvent, - XYChartElementEvent, -} from '@elastic/charts'; -import { - Axis, - Chart, - HistogramBarSeries, - Position, - ScaleType, - Settings, - TooltipType, -} from '@elastic/charts'; -import type { IUiSettingsClient } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { - CurrentTime, - Endzones, - getAdjustedInterval, - renderEndzoneTooltip, -} from '@kbn/charts-plugin/public'; -import { LEGACY_TIME_AXIS, MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; import { css } from '@emotion/react'; +import React, { useCallback, useMemo } from 'react'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import type { UnifiedHistogramChartContext, UnifiedHistogramServices } from '../types'; export interface HistogramProps { services: UnifiedHistogramServices; + dataView: DataView; chart: UnifiedHistogramChartContext; - timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; -} - -function getTimezone(uiSettings: IUiSettingsClient) { - if (uiSettings.isDefault('dateFormat:tz')) { - const detectedTimezone = moment.tz.guess(); - if (detectedTimezone) return detectedTimezone; - else return moment().format('Z'); - } else { - return uiSettings.get('dateFormat:tz', 'Browser'); - } } export function Histogram({ - services: { data, theme, uiSettings, fieldFormats }, - chart: { status, timeInterval, bucketInterval, data: chartData, error }, - timefilterUpdateHandler, + services: { data, lens, uiSettings }, + dataView, + chart: { timeInterval, bucketInterval, data: chartData }, }: HistogramProps) { - const chartTheme = theme.useChartsTheme(); - const chartBaseTheme = theme.useChartsBaseTheme(); - const timeZone = getTimezone(uiSettings); - - const onBrushEnd = useCallback( - ({ x }: XYBrushEvent) => { - if (!x) { - return; - } - const [from, to] = x; - timefilterUpdateHandler({ from, to }); - }, - [timefilterUpdateHandler] - ); - - const onElementClick = useCallback( - (xInterval: number): ElementClickListener => - ([elementData]) => { - const startRange = (elementData as XYChartElementEvent)[0].x; - - const range = { - from: startRange, - to: startRange + xInterval, - }; - - timefilterUpdateHandler(range); + const attributes = useMemo( + () => ({ + title: '', + references: [ + { + id: dataView.id ?? '', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: dataView.id ?? '', + name: 'indexpattern-datasource-layer-layer1', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer1: { + columnOrder: ['col1', 'col2'], + columns: { + col2: { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: '___records___', + }, + col1: { + dataType: 'date', + isBucketed: true, + label: dataView.timeFieldName ?? '', + operationType: 'date_histogram', + interval: timeInterval ?? 'auto', + params: {}, + scale: 'interval', + sourceField: dataView.timeFieldName, + }, + }, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['col2'], + layerId: 'layer1', + layerType: 'data', + seriesType: 'bar_stacked', + xAccessor: 'col1', + yConfig: [ + { + forAccessor: 'col2', + }, + ], + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_stacked', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', + }, }, - [timefilterUpdateHandler] + visualizationType: 'lnsXY', + }), + [dataView.id, dataView.timeFieldName, timeInterval] ); const { timefilter } = data.query.timefilter; @@ -138,120 +161,17 @@ export function Histogram({ const { euiTheme } = useEuiTheme(); const chartCss = css` flex-grow: 1; - padding: 0 ${euiTheme.size.s} ${euiTheme.size.s} ${euiTheme.size.s}; - `; + padding: 0; - if (!chartData && status === 'loading') { - const chartLoadingCss = css` - display: flex; - flex-direction: column; - justify-content: center; - flex: 1 0 100%; - text-align: center; + & > div { height: 100%; - width: 100%; - `; - - return ( -
-
- - - - - -
-
- ); - } - - if (status === 'error' && error) { - const chartErrorContainerCss = css` - padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; - `; - const chartErrorIconCss = css` - padding-top: 0.5 * ${euiTheme.size.xs}; - `; - const chartErrorCss = css` - margin-left: ${euiTheme.size.xs} !important; - `; - const chartErrorTextCss = css` - margin-top: ${euiTheme.size.s}; - `; - - return ( -
- - - - - - - - - - - - {error.message} - -
- ); - } + } + `; - if (!chartData) { + if (!chartData || !dataView.id || !dataView.isTimeBased()) { return null; } - const formatXValue = (val: string) => { - const xAxisFormat = chartData.xAxisFormat.params!.pattern; - return moment(val).format(xAxisFormat); - }; - - const isDarkMode = uiSettings.get('theme:darkMode'); - - /* - * Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval]. - * see https://github.com/elastic/kibana/issues/27410 - * TODO: Once the Discover query has been update, we should change the below to use the new field - */ - const { intervalESValue, intervalESUnit, interval } = chartData.ordered; - const xInterval = interval.asMilliseconds(); - - const xValues = chartData.xAxisOrderedValues; - const lastXValue = xValues[xValues.length - 1]; - - const domain = chartData.ordered; - const domainStart = domain.min.valueOf(); - const domainEnd = domain.max.valueOf(); - - const domainMin = Math.min(chartData.values[0]?.x, domainStart); - const domainMax = Math.max(domainEnd - xInterval, lastXValue); - - const xDomain = { - min: domainMin, - max: domainMax, - minInterval: getAdjustedInterval( - xValues, - intervalESValue, - intervalESUnit as unitOfTime.Base, - timeZone - ), - }; - const tooltipProps = { - headerFormatter: renderEndzoneTooltip(xInterval, domainStart, domainEnd, formatXValue), - type: TooltipType.VerticalCursor, - }; - - const xAxisFormatter = fieldFormats.deserialize(chartData.yAxisFormat); - - const useLegacyTimeAxis = uiSettings.get(LEGACY_TIME_AXIS, false); - const toolTipTitle = i18n.translate('unifiedHistogram.timeIntervalWithValueWarning', { defaultMessage: 'Warning', }); @@ -275,15 +195,18 @@ export function Histogram({ const timeRangeCss = css` padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; `; + let timeRange = ( {timeRangeText} ); + if (bucketInterval?.scaled) { const timeRangeWrapperCss = css` flex-grow: 0; `; + timeRange = ( + <>
- - - xAxisFormatter.convert(value)} - /> - - - - - +
{timeRange} - + ); } diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index 229d8a922e465..fb9149b5bcc95 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -11,6 +11,7 @@ import type { PropsWithChildren, ReactElement, RefObject } from 'react'; import React, { useMemo } from 'react'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { css } from '@emotion/css'; +import type { DataView } from '@kbn/data-views-plugin/public'; import { Chart } from '../chart'; import { Panels, PANELS_MODE } from '../panels'; import type { @@ -22,6 +23,7 @@ import type { export interface UnifiedHistogramLayoutProps extends PropsWithChildren { className?: string; services: UnifiedHistogramServices; + dataView: DataView; /** * Context object for the hits count -- leave undefined to hide the hits count */ @@ -63,6 +65,7 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren export const UnifiedHistogramLayout = ({ className, services, + dataView, hits, chart, resizeRef, @@ -119,6 +122,7 @@ export const UnifiedHistogramLayout = ({ { visualizationType: 'testVis', }; - const createEmbeddable = (noPadding?: boolean) => { + const createEmbeddable = (displayOptions?: { noPadding: boolean }, noPadding?: boolean) => { return new Embeddable( { timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, @@ -1451,9 +1451,7 @@ describe('embeddable', () => { theme: themeServiceMock.createStartContract(), visualizationMap: { [visDocument.visualizationType as string]: { - getDisplayOptions: () => ({ - noPadding: false, - }), + getDisplayOptions: displayOptions ? () => displayOptions : undefined, } as unknown as Visualization, }, datasourceMap: {}, @@ -1481,6 +1479,7 @@ describe('embeddable', () => { ); }; + // no display options and no override let embeddable = createEmbeddable(); embeddable.render(mountpoint); @@ -1490,7 +1489,8 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(1); expect(expressionRenderer.mock.calls[0][0]!.padding).toBe('s'); - embeddable = createEmbeddable(true); + // display options and no override + embeddable = createEmbeddable({ noPadding: true }); embeddable.render(mountpoint); // wait one tick to give embeddable time to initialize @@ -1498,5 +1498,25 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(2); expect(expressionRenderer.mock.calls[1][0]!.padding).toBe(undefined); + + // no display options and override + embeddable = createEmbeddable(undefined, true); + embeddable.render(mountpoint); + + // wait one tick to give embeddable time to initialize + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(expressionRenderer).toHaveBeenCalledTimes(3); + expect(expressionRenderer.mock.calls[1][0]!.padding).toBe(undefined); + + // display options and override + embeddable = createEmbeddable({ noPadding: false }, true); + embeddable.render(mountpoint); + + // wait one tick to give embeddable time to initialize + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(expressionRenderer).toHaveBeenCalledTimes(4); + expect(expressionRenderer.mock.calls[1][0]!.padding).toBe(undefined); }); }); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 65c91b115ce70..b14b59bca3e86 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -700,7 +700,7 @@ export class Embeddable onRuntimeError={() => { this.logError('runtime'); }} - noPadding={this.visDisplayOptions?.noPadding} + noPadding={this.visDisplayOptions.noPadding} />
Date: Wed, 12 Oct 2022 23:31:41 -0300 Subject: [PATCH 02/84] [Discover] Add filters and query support to unified histogram --- .../unified_histogram/public/chart/histogram.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 17ddd7c60231e..f15de21270187 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -27,6 +27,8 @@ export function Histogram({ dataView, chart: { timeInterval, bucketInterval, data: chartData }, }: HistogramProps) { + const filters = data.query.filterManager.getFilters(); + const query = data.query.queryString.getQuery(); const attributes = useMemo( () => ({ title: '', @@ -72,11 +74,8 @@ export function Histogram({ }, }, }, - filters: [], - query: { - language: 'kuery', - query: '', - }, + filters, + query: 'language' in query ? query : { language: 'kuery', query: '' }, visualization: { axisTitlesVisibilitySettings: { x: false, @@ -118,7 +117,7 @@ export function Histogram({ }, visualizationType: 'lnsXY', }), - [dataView.id, dataView.timeFieldName, timeInterval] + [dataView.id, dataView.timeFieldName, filters, query, timeInterval] ); const { timefilter } = data.query.timefilter; From 5847970e24f667e43bb4ecf4474a1c6c8d546f55 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 14 Oct 2022 15:55:17 -0300 Subject: [PATCH 03/84] [Discover] Update indexpattern to formBased in Lens config --- src/plugins/unified_histogram/public/chart/histogram.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index f15de21270187..79077ae522c87 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -46,7 +46,7 @@ export function Histogram({ ], state: { datasourceStates: { - indexpattern: { + formBased: { layers: { layer1: { columnOrder: ['col1', 'col2'], From ea02b13f7b10a925878ad88690fe2f2607b069c2 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 14 Oct 2022 23:35:30 -0300 Subject: [PATCH 04/84] [Discover] Fix histogram time interval --- src/plugins/unified_histogram/public/chart/histogram.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 79077ae522c87..4328d77aa1841 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -64,8 +64,9 @@ export function Histogram({ isBucketed: true, label: dataView.timeFieldName ?? '', operationType: 'date_histogram', - interval: timeInterval ?? 'auto', - params: {}, + params: { + interval: timeInterval ?? 'auto', + }, scale: 'interval', sourceField: dataView.timeFieldName, }, From 54fc5e16c546d8c2c8c6c134c0a8a75ed500faad Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 17 Oct 2022 11:37:42 -0300 Subject: [PATCH 05/84] [Discover] Continue adding support for histogram breakdown --- .../layout/discover_main_content.tsx | 1 + .../unified_histogram/public/chart/chart.tsx | 37 +++- .../public/chart/get_lens_attributes.ts | 158 ++++++++++++++++++ .../public/chart/histogram.tsx | 105 ++---------- .../public/layout/layout.tsx | 7 + src/plugins/unified_histogram/public/types.ts | 11 ++ 6 files changed, 224 insertions(+), 95 deletions(-) create mode 100644 src/plugins/unified_histogram/public/chart/get_lens_attributes.ts diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 29b2295bd7708..3eabea48b22ca 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -109,6 +109,7 @@ export const DiscoverMainContent = ({ dataView={dataView} hits={hits} chart={chart} + breakdown={{ field: dataView.fields.getByName('DestWeather') }} resizeRef={resizeRef} topPanelHeight={topPanelHeight} appendHitsCounter={ diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 06f2b9742493a..d818e6426e67f 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -10,6 +10,8 @@ import type { ReactElement } from 'react'; import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import { EuiButtonIcon, + EuiComboBox, + EuiComboBoxOptionOption, EuiContextMenu, EuiFlexGroup, EuiFlexItem, @@ -25,6 +27,7 @@ import { HitsCounter } from '../hits_counter'; import { Histogram } from './histogram'; import { useChartPanels } from './use_chart_panels'; import type { + UnifiedHistogramBreakdownContext, UnifiedHistogramChartContext, UnifiedHistogramHitsContext, UnifiedHistogramServices, @@ -36,6 +39,7 @@ export interface ChartProps { dataView: DataView; hits?: UnifiedHistogramHitsContext; chart?: UnifiedHistogramChartContext; + breakdown?: UnifiedHistogramBreakdownContext; appendHitsCounter?: ReactElement; appendHistogram?: ReactElement; onEditVisualization?: () => void; @@ -52,6 +56,7 @@ export function Chart({ dataView, hits, chart, + breakdown, appendHitsCounter, appendHistogram, onEditVisualization, @@ -122,6 +127,16 @@ export function Chart({ } `; + const options = dataView.fields + .filter((field) => field.aggregatable) + .map((field) => ({ label: field.name })); + + const [selectedOptions, setSelectedOptions] = useState(); + + const onChange = (newOptions: EuiComboBoxOptionOption[]) => { + setSelectedOptions(newOptions); + }; + return ( + + + {onEditVisualization && ( - + field.name === selectedOptions?.[0]?.label), + }} + /> {appendHistogram} diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts new file mode 100644 index 0000000000000..870448408d7b1 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -0,0 +1,158 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { + CountIndexPatternColumn, + DateHistogramIndexPatternColumn, + GenericIndexPatternColumn, + TermsIndexPatternColumn, + TypedLensByValueInput, +} from '@kbn/lens-plugin/public'; + +export const getLensAttributes = ({ + data, + dataView, + timeInterval, + breakdownField, +}: { + data: DataPublicPluginStart; + dataView: DataView; + timeInterval: string | undefined; + breakdownField: DataViewField | undefined; +}) => { + const filters = data.query.filterManager.getFilters(); + const query = data.query.queryString.getQuery(); + const showBreakdown = breakdownField?.aggregatable; + + let columnOrder = ['date_column', 'count_column']; + + if (showBreakdown) { + columnOrder = ['breakdown_column', ...columnOrder]; + } + + let columns: Record = { + date_column: { + dataType: 'date', + isBucketed: true, + label: dataView.timeFieldName ?? '', + operationType: 'date_histogram', + scale: 'interval', + sourceField: dataView.timeFieldName, + params: { + interval: timeInterval ?? 'auto', + }, + } as DateHistogramIndexPatternColumn, + count_column: { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: '___records___', + } as CountIndexPatternColumn, + }; + + if (showBreakdown) { + columns = { + ...columns, + breakdown_column: { + dataType: 'string', + isBucketed: true, + label: `Top 3 values of ${breakdownField.name}`, + operationType: 'terms', + scale: 'ordinal', + sourceField: breakdownField.name, + params: { + size: 3, + orderBy: { + type: 'column', + columnId: 'count_column', + }, + orderDirection: 'desc', + otherBucket: false, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + }, + } as TermsIndexPatternColumn, + }; + } + + return { + title: '', + references: [ + { + id: dataView.id ?? '', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: dataView.id ?? '', + name: 'indexpattern-datasource-layer-layer1', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + formBased: { + layers: { + layer1: { columnOrder, columns }, + }, + }, + }, + filters, + query: 'language' in query ? query : { language: 'kuery', query: '' }, + visualization: { + layers: [ + { + accessors: ['count_column'], + layerId: 'layer1', + layerType: 'data', + seriesType: 'bar_stacked', + xAccessor: 'date_column', + ...(showBreakdown + ? { splitAccessor: 'breakdown_column' } + : { + yConfig: [ + { + forAccessor: 'count_column', + }, + ], + }), + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_stacked', + valueLabels: 'hide', + fittingFunction: 'None', + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + }, + }, + visualizationType: 'lnsXY', + } as TypedLensByValueInput['attributes']; +}; diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 4328d77aa1841..32c322aa4a336 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -13,112 +13,29 @@ import { css } from '@emotion/react'; import React, { useCallback, useMemo } from 'react'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; -import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; -import type { UnifiedHistogramChartContext, UnifiedHistogramServices } from '../types'; +import type { + UnifiedHistogramBreakdownContext, + UnifiedHistogramChartContext, + UnifiedHistogramServices, +} from '../types'; +import { getLensAttributes } from './get_lens_attributes'; export interface HistogramProps { services: UnifiedHistogramServices; dataView: DataView; chart: UnifiedHistogramChartContext; + breakdown?: UnifiedHistogramBreakdownContext; } export function Histogram({ services: { data, lens, uiSettings }, dataView, chart: { timeInterval, bucketInterval, data: chartData }, + breakdown: { field: breakdownField } = {}, }: HistogramProps) { - const filters = data.query.filterManager.getFilters(); - const query = data.query.queryString.getQuery(); - const attributes = useMemo( - () => ({ - title: '', - references: [ - { - id: dataView.id ?? '', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: dataView.id ?? '', - name: 'indexpattern-datasource-layer-layer1', - type: 'index-pattern', - }, - ], - state: { - datasourceStates: { - formBased: { - layers: { - layer1: { - columnOrder: ['col1', 'col2'], - columns: { - col2: { - dataType: 'number', - isBucketed: false, - label: 'Count of records', - operationType: 'count', - scale: 'ratio', - sourceField: '___records___', - }, - col1: { - dataType: 'date', - isBucketed: true, - label: dataView.timeFieldName ?? '', - operationType: 'date_histogram', - params: { - interval: timeInterval ?? 'auto', - }, - scale: 'interval', - sourceField: dataView.timeFieldName, - }, - }, - }, - }, - }, - }, - filters, - query: 'language' in query ? query : { language: 'kuery', query: '' }, - visualization: { - axisTitlesVisibilitySettings: { - x: false, - yLeft: false, - yRight: true, - }, - fittingFunction: 'None', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - layers: [ - { - accessors: ['col2'], - layerId: 'layer1', - layerType: 'data', - seriesType: 'bar_stacked', - xAccessor: 'col1', - yConfig: [ - { - forAccessor: 'col2', - }, - ], - }, - ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'bar_stacked', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', - }, - }, - visualizationType: 'lnsXY', - }), - [dataView.id, dataView.timeFieldName, filters, query, timeInterval] + const attributes = useMemo( + () => getLensAttributes({ data, dataView, timeInterval, breakdownField }), + [breakdownField, data, dataView, timeInterval] ); const { timefilter } = data.query.timefilter; diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index fb9149b5bcc95..ef973f8d30fdb 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -18,6 +18,7 @@ import type { UnifiedHistogramChartContext, UnifiedHistogramServices, UnifiedHistogramHitsContext, + UnifiedHistogramBreakdownContext, } from '../types'; export interface UnifiedHistogramLayoutProps extends PropsWithChildren { @@ -32,6 +33,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * Context object for the chart -- leave undefined to hide the chart */ chart?: UnifiedHistogramChartContext; + /** + * Context object for the breakdown -- leave undefined to hide the breakdown + */ + breakdown?: UnifiedHistogramBreakdownContext; /** * Ref to the element wrapping the layout which will be used for resize calculations */ @@ -68,6 +73,7 @@ export const UnifiedHistogramLayout = ({ dataView, hits, chart, + breakdown, resizeRef, topPanelHeight, appendHitsCounter, @@ -125,6 +131,7 @@ export const UnifiedHistogramLayout = ({ dataView={dataView} hits={hits} chart={chart} + breakdown={breakdown} appendHitsCounter={appendHitsCounter} appendHistogram={showFixedPanels ? : } onEditVisualization={onEditVisualization} diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index 0934c10b2e78e..9c0e4e342ca4e 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -14,6 +14,7 @@ import type { Duration, Moment } from 'moment'; import type { Unit } from '@kbn/datemath'; import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; +import type { DataViewField } from '@kbn/data-views-plugin/public'; /** * The fetch status of a unified histogram request @@ -154,3 +155,13 @@ export interface UnifiedHistogramChartContext { */ error?: Error; } + +/** + * Context object for the histogram breakdown + */ +export interface UnifiedHistogramBreakdownContext { + /** + * The field used for the breakdown + */ + field?: DataViewField; +} From 16d282496b71554c85208de1e91ab9262f422055 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 17 Oct 2022 17:30:14 -0300 Subject: [PATCH 06/84] [Discover] Removing Discover dependency from Lens --- src/plugins/dashboard/tsconfig.json | 1 - x-pack/plugins/lens/public/plugin.ts | 35 +++++++++++-------- .../open_in_discover_action.ts | 10 +++--- .../open_in_discover_drilldown.tsx | 12 +++---- .../open_in_discover_helpers.ts | 28 ++++++--------- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 96a2757909c12..390fc6f6a0a5e 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -26,7 +26,6 @@ { "path": "../screenshot_mode/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../charts/tsconfig.json" }, - { "path": "../discover/tsconfig.json" }, { "path": "../visualizations/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } ] diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index c4d4cf9bfab9f..b178e9e776000 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -46,10 +46,10 @@ import { AGG_BASED_VISUALIZATION_TRIGGER, } from '@kbn/visualizations-plugin/public'; import { createStartServicesGetter } from '@kbn/kibana-utils-plugin/public'; -import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { AdvancedUiActionsSetup } from '@kbn/ui-actions-enhanced-plugin/public'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service'; import type { FormBasedDatasource as FormBasedDatasourceType, @@ -116,8 +116,8 @@ export interface LensPluginSetupDependencies { eventAnnotation: EventAnnotationPluginSetup; globalSearch?: GlobalSearchPluginSetup; usageCollection?: UsageCollectionSetup; - discover?: DiscoverSetup; uiActionsEnhanced: AdvancedUiActionsSetup; + share?: SharePluginSetup; } export interface LensPluginStartDependencies { @@ -140,8 +140,8 @@ export interface LensPluginStartDependencies { inspector: InspectorStartContract; spaces: SpacesPluginStart; usageCollection?: UsageCollectionStart; - discover?: DiscoverStart; docLinks: DocLinksStart; + share?: SharePluginStart; } export interface LensPublicSetup { @@ -257,7 +257,7 @@ export class LensPlugin { globalSearch, usageCollection, uiActionsEnhanced, - discover, + share, }: LensPluginSetupDependencies ) { const startServices = createStartServicesGetter(core.getStartServices); @@ -316,11 +316,14 @@ export class LensPlugin { } visualizations.registerAlias(getLensAliasConfig()); - if (discover) { + + const discoverLocator = share?.url.locators.get('discover'); + + if (discoverLocator) { uiActionsEnhanced.registerDrilldown( new OpenInDiscoverDrilldown({ - discover, dataViews: () => this.dataViewsService!, + locator: () => discoverLocator, hasDiscoverAccess: () => this.hasDiscoverAccess, application: () => startServices().core.application, }) @@ -496,14 +499,18 @@ export class LensPlugin { visualizeAggBasedVisAction(core.application) ); - startDependencies.uiActions.addTriggerAction( - CONTEXT_MENU_TRIGGER, - createOpenInDiscoverAction( - startDependencies.discover!, - startDependencies.dataViews!, - this.hasDiscoverAccess - ) - ); + const discoverLocator = startDependencies.share?.url.locators.get('discover'); + + if (discoverLocator) { + startDependencies.uiActions.addTriggerAction( + CONTEXT_MENU_TRIGGER, + createOpenInDiscoverAction( + discoverLocator, + startDependencies.dataViews, + this.hasDiscoverAccess + ) + ); + } return { EmbeddableComponent: getEmbeddableComponent(core, startDependencies), diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts index 45494c5e14b68..61845a76bd3a3 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { createAction } from '@kbn/ui-actions-plugin/public'; -import type { DiscoverStart } from '@kbn/discover-plugin/public'; +import type { DiscoverAppLocator } from '@kbn/discover-plugin/public'; import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import type { DataViewsService } from '@kbn/data-views-plugin/public'; @@ -20,7 +20,7 @@ interface Context { export const getDiscoverHelpersAsync = async () => await import('../async_services'); export const createOpenInDiscoverAction = ( - discover: Pick, + locator: DiscoverAppLocator, dataViews: Pick, hasDiscoverAccess: boolean ) => @@ -36,7 +36,7 @@ export const createOpenInDiscoverAction = ( getHref: async (context: Context) => { const { getHref } = await getDiscoverHelpersAsync(); return getHref({ - discover, + locator, dataViews, hasDiscoverAccess, ...context, @@ -46,13 +46,13 @@ export const createOpenInDiscoverAction = ( const { isCompatible } = await getDiscoverHelpersAsync(); return isCompatible({ hasDiscoverAccess, - discover, + locator, dataViews, embeddable: context.embeddable, }); }, execute: async (context: Context) => { const { execute } = await getDiscoverHelpersAsync(); - return execute({ ...context, discover, dataViews, hasDiscoverAccess }); + return execute({ ...context, locator, dataViews, hasDiscoverAccess }); }, }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx index 2022e97fb9b45..8da471d27c90e 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx @@ -13,12 +13,12 @@ import type { ApplicationStart } from '@kbn/core/public'; import type { SerializableRecord } from '@kbn/utility-types'; import type { CollectConfigProps as CollectConfigPropsBase } from '@kbn/kibana-utils-plugin/public'; import { reactToUiComponent } from '@kbn/kibana-react-plugin/public'; -import { +import type { UiActionsEnhancedDrilldownDefinition as Drilldown, UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, } from '@kbn/ui-actions-enhanced-plugin/public'; import { EuiFormRow, EuiSwitch } from '@elastic/eui'; -import type { DiscoverSetup } from '@kbn/discover-plugin/public'; +import type { DiscoverAppLocator } from '@kbn/discover-plugin/public'; import type { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; import { i18n } from '@kbn/i18n'; import type { DataViewsService } from '@kbn/data-views-plugin/public'; @@ -36,7 +36,7 @@ export const getDiscoverHelpersAsync = async () => await import('../async_servic export type EmbeddableWithQueryInput = IEmbeddable; interface UrlDrilldownDeps { - discover: Pick; + locator: () => DiscoverAppLocator | undefined; dataViews: () => Pick; hasDiscoverAccess: () => boolean; application: () => ApplicationStart; @@ -106,7 +106,7 @@ export class OpenInDiscoverDrilldown const { isCompatible } = await getDiscoverHelpersAsync(); return isCompatible({ - discover: this.deps.discover, + locator: this.deps.locator(), dataViews: this.deps.dataViews(), hasDiscoverAccess: this.deps.hasDiscoverAccess(), ...context, @@ -122,7 +122,7 @@ export class OpenInDiscoverDrilldown const { getHref } = await getDiscoverHelpersAsync(); return getHref({ - discover: this.deps.discover, + locator: this.deps.locator(), dataViews: this.deps.dataViews(), hasDiscoverAccess: this.deps.hasDiscoverAccess(), ...context, @@ -137,7 +137,7 @@ export class OpenInDiscoverDrilldown const { getLocation } = await getDiscoverHelpersAsync(); const { app, path, state } = await getLocation({ - discover: this.deps.discover, + locator: this.deps.locator(), dataViews: this.deps.dataViews(), hasDiscoverAccess: this.deps.hasDiscoverAccess(), ...context, diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts index 677a7a4734b2e..4a3248498ea52 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts @@ -5,10 +5,10 @@ * 2.0. */ -import type { DiscoverSetup } from '@kbn/discover-plugin/public'; -import { Filter } from '@kbn/es-query'; -import { IEmbeddable } from '@kbn/embeddable-plugin/public'; -import { DataViewsService } from '@kbn/data-views-plugin/public'; +import type { DiscoverAppLocator } from '@kbn/discover-plugin/public'; +import type { Filter } from '@kbn/es-query'; +import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; +import type { DataViewsService } from '@kbn/data-views-plugin/public'; import type { Embeddable } from '../embeddable'; import { DOC_TYPE } from '../../common'; @@ -18,7 +18,7 @@ interface Context { openInSameTab?: boolean; hasDiscoverAccess: boolean; dataViews: Pick; - discover: Pick; + locator?: DiscoverAppLocator; timeFieldName?: string; } @@ -73,13 +73,7 @@ async function getDiscoverLocationParams({ }; } -export async function getHref({ - embeddable, - discover, - filters, - dataViews, - timeFieldName, -}: Context) { +export async function getHref({ embeddable, locator, filters, dataViews, timeFieldName }: Context) { const params = await getDiscoverLocationParams({ embeddable, filters, @@ -87,14 +81,14 @@ export async function getHref({ timeFieldName, }); - const discoverUrl = discover.locator?.getRedirectUrl(params); + const discoverUrl = locator?.getRedirectUrl(params); return discoverUrl; } export async function getLocation({ embeddable, - discover, + locator, filters, dataViews, timeFieldName, @@ -106,7 +100,7 @@ export async function getLocation({ timeFieldName, }); - const discoverLocation = discover.locator?.getLocation(params); + const discoverLocation = locator?.getLocation(params); if (!discoverLocation) { throw new Error('Discover location not found'); @@ -117,7 +111,7 @@ export async function getLocation({ export async function execute({ embeddable, - discover, + locator, filters, openInSameTab, dataViews, @@ -126,7 +120,7 @@ export async function execute({ }: Context) { const discoverUrl = await getHref({ embeddable, - discover, + locator, filters, dataViews, timeFieldName, From 6ea0adea7ed1fad8511ea2b09a31d36498e0f14b Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 17 Oct 2022 22:00:28 -0300 Subject: [PATCH 07/84] [Discover] Finish removing Discover dependencies from Lens --- .../lens/public/app_plugin/lens_top_nav.tsx | 14 ++++++----- .../lens/public/app_plugin/mounter.tsx | 4 ++-- .../plugins/lens/public/app_plugin/types.ts | 4 ++-- .../form_based/field_item.test.tsx | 13 ++++++---- .../datasources/form_based/field_item.tsx | 5 ++-- .../datasources/form_based/form_based.tsx | 8 +++---- .../public/datasources/form_based/index.ts | 8 +++---- .../open_in_discover_action.test.ts | 24 +++++++++---------- .../open_in_discover_action.ts | 2 +- .../open_in_discover_drilldown.test.tsx | 5 ++-- .../open_in_discover_drilldown.tsx | 2 +- .../open_in_discover_helpers.ts | 15 ++++++++++-- 12 files changed, 59 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 2e1b021e21470..adac52ae8f0ba 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -233,7 +233,7 @@ export const LensTopNavMenu = ({ uiSettings, application, attributeService, - discover, + share, dashboardFeatureFlag, dataViewFieldEditor, dataViewEditor, @@ -424,8 +424,10 @@ export const LensTopNavMenu = ({ currentDoc, ]); + const discoverLocator = share?.url.locators.get('discover'); + const layerMetaInfo = useMemo(() => { - if (!activeDatasourceId || !discover) { + if (!activeDatasourceId || !discoverLocator) { return; } return getLayerMetaInfo( @@ -438,7 +440,7 @@ export const LensTopNavMenu = ({ ); }, [ activeDatasourceId, - discover, + discoverLocator, datasourceMap, datasourceStates, activeData, @@ -559,7 +561,7 @@ export const LensTopNavMenu = ({ const { error, meta } = layerMetaInfo; // If Discover is not available, return // If there's no data, return - if (error || !discover || !meta) { + if (error || !discoverLocator || !meta) { return; } const { filters: newFilters, query: newQuery } = combineQueryAndFilters( @@ -570,7 +572,7 @@ export const LensTopNavMenu = ({ getEsQueryConfig(uiSettings) ); - return discover.locator!.getRedirectUrl({ + return discoverLocator.getRedirectUrl({ dataViewSpec: dataViews.indexPatterns[meta.id]?.spec, timeRange: data.query.timefilter.timefilter.getTime(), filters: newFilters, @@ -611,7 +613,7 @@ export const LensTopNavMenu = ({ setIsSaveModalVisible, goBackToOriginatingApp, redirectToOrigin, - discover, + discoverLocator, query, filters, indexPatterns, diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index ef1933dbd5ca1..82873673271c6 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -67,7 +67,7 @@ export async function getLensServices( usageCollection, fieldFormats, spaces, - discover, + share, unifiedSearch, } = startDependencies; @@ -108,7 +108,7 @@ export async function getLensServices( // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag: startDependencies.dashboard.dashboardFeatureFlagConfig, spaces, - discover, + share, unifiedSearch, docLinks: coreStart.docLinks, }; diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index b2466c60e6e4e..831b7ce54da39 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -7,7 +7,6 @@ import type { History } from 'history'; import type { OnSaveProps } from '@kbn/saved-objects-plugin/public'; -import { DiscoverStart } from '@kbn/discover-plugin/public'; import { Observable } from 'rxjs'; import { SpacesApi } from '@kbn/spaces-plugin/public'; import type { @@ -44,6 +43,7 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { DatasourceMap, EditorFrameInstance, @@ -153,7 +153,7 @@ export interface LensAppServices { presentationUtil: PresentationUtilPluginStart; spaces: SpacesApi; charts: ChartsPluginSetup; - discover?: DiscoverStart; + share?: SharePluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; docLinks: DocLinksStart; // Temporarily required until the 'by value' paradigm is default. diff --git a/x-pack/plugins/lens/public/datasources/form_based/field_item.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/field_item.test.tsx index a0514dafd61b0..4d7064647eca3 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/field_item.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/field_item.test.tsx @@ -9,7 +9,6 @@ import React, { ReactElement } from 'react'; import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; -import type { DiscoverStart } from '@kbn/discover-plugin/public'; import { InnerFieldItem, FieldItemProps } from './field_item'; import { coreMock } from '@kbn/core/public/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; @@ -49,11 +48,15 @@ const mockedServices = { fieldFormats: fieldFormatsServiceMock.createStartContract(), charts: chartPluginMock.createSetupContract(), uiSettings: coreMock.createStart().uiSettings, - discover: { - locator: { - getRedirectUrl: jest.fn(() => 'discover_url'), + share: { + url: { + locators: { + get: jest.fn().mockReturnValue({ + getRedirectUrl: jest.fn(() => 'discover_url'), + }), + }, }, - } as unknown as DiscoverStart, + }, application: { capabilities: { discover: { save: true, saveQuery: true, show: true }, diff --git a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx index e2ee0559b3808..36727780944ac 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx @@ -309,10 +309,11 @@ function FieldItemPopoverContents( [indexPattern], getEsQueryConfig(services.uiSettings) ); - if (!services.discover || !services.application.capabilities.discover.show) { + const discoverLocator = services.share?.url.locators.get('discover'); + if (!discoverLocator || !services.application.capabilities.discover.show) { return; } - return services.discover.locator!.getRedirectUrl({ + return discoverLocator.getRedirectUrl({ dataViewSpec: indexPattern?.spec, timeRange: services.data.query.timefilter.timefilter.getTime(), filters: newFilters, diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index 95a13d46cdc53..52be5d72108d5 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -11,7 +11,6 @@ import { I18nProvider } from '@kbn/i18n-react'; import type { CoreStart, SavedObjectReference } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { TimeRange } from '@kbn/es-query'; -import type { DiscoverStart } from '@kbn/discover-plugin/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { flatten, isEqual } from 'lodash'; @@ -25,6 +24,7 @@ import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { EuiCallOut, EuiLink } from '@elastic/eui'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { DatasourceDimensionEditorProps, DatasourceDimensionTriggerProps, @@ -141,7 +141,7 @@ export function getFormBasedDatasource({ storage, data, unifiedSearch, - discover, + share, dataViews, fieldFormats, charts, @@ -152,7 +152,7 @@ export function getFormBasedDatasource({ storage: IStorageWrapper; data: DataPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; - discover?: DiscoverStart; + share?: SharePluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; charts: ChartsPluginSetup; @@ -458,7 +458,7 @@ export function getFormBasedDatasource({ fieldFormats, charts, unifiedSearch, - discover, + share, }} > { describe('compatibility check', () => { @@ -19,7 +19,7 @@ describe('open in discover action', () => { const embeddable = { type: 'NOT_LENS' } as IEmbeddable; const isCompatible = await createOpenInDiscoverAction( - {} as DiscoverStart, + {} as DiscoverAppLocator, {} as DataViewsService, true ).isCompatible({ @@ -37,7 +37,7 @@ describe('open in discover action', () => { // make sure it would work if we had access to Discover expect( await createOpenInDiscoverAction( - {} as DiscoverStart, + {} as DiscoverAppLocator, {} as DataViewsService, hasDiscoverAccess ).isCompatible({ @@ -49,7 +49,7 @@ describe('open in discover action', () => { hasDiscoverAccess = false; expect( await createOpenInDiscoverAction( - {} as DiscoverStart, + {} as DiscoverAppLocator, {} as DataViewsService, hasDiscoverAccess ).isCompatible({ @@ -65,7 +65,7 @@ describe('open in discover action', () => { embeddable.canViewUnderlyingData = jest.fn(() => Promise.resolve(false)); expect( await createOpenInDiscoverAction( - {} as DiscoverStart, + {} as DiscoverAppLocator, {} as DataViewsService, true ).isCompatible({ @@ -79,7 +79,7 @@ describe('open in discover action', () => { embeddable.canViewUnderlyingData = jest.fn(() => Promise.resolve(true)); expect( await createOpenInDiscoverAction( - {} as DiscoverStart, + {} as DiscoverAppLocator, {} as DataViewsService, true ).isCompatible({ @@ -106,16 +106,14 @@ describe('open in discover action', () => { }; const discoverUrl = 'https://discover-redirect-url'; - const discover = { - locator: { - getRedirectUrl: jest.fn(() => discoverUrl), - }, - } as unknown as DiscoverStart; + const locator = { + getRedirectUrl: jest.fn(() => discoverUrl), + } as unknown as DiscoverAppLocator; globalThis.open = jest.fn(); await createOpenInDiscoverAction( - discover, + locator, { get: () => ({ isTimeBased: () => true }) } as unknown as DataViewsService, true ).execute({ @@ -125,7 +123,7 @@ describe('open in discover action', () => { }>); expect(embeddable.getViewUnderlyingDataArgs).toHaveBeenCalled(); - expect(discover.locator!.getRedirectUrl).toHaveBeenCalledWith(viewUnderlyingDataArgs); + expect(locator.getRedirectUrl).toHaveBeenCalledWith(viewUnderlyingDataArgs); expect(globalThis.open).toHaveBeenCalledWith(discoverUrl, '_blank'); }); }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts index 61845a76bd3a3..7fc04d84f5e15 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import { createAction } from '@kbn/ui-actions-plugin/public'; -import type { DiscoverAppLocator } from '@kbn/discover-plugin/public'; import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import type { DataViewsService } from '@kbn/data-views-plugin/public'; +import type { DiscoverAppLocator } from './open_in_discover_helpers'; const ACTION_OPEN_IN_DISCOVER = 'ACTION_OPEN_IN_DISCOVER'; diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx index c4e8fe050df23..bfa0036c14550 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx @@ -7,9 +7,8 @@ import React, { FormEvent } from 'react'; import { IEmbeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; -import { DiscoverSetup } from '@kbn/discover-plugin/public'; import type { ApplicationStart } from '@kbn/core/public'; -import { getHref, isCompatible } from './open_in_discover_helpers'; +import { DiscoverAppLocator, getHref, isCompatible } from './open_in_discover_helpers'; import { mount } from 'enzyme'; import { Filter } from '@kbn/es-query'; import { @@ -35,7 +34,7 @@ describe('open in discover drilldown', () => { beforeEach(() => { drilldown = new OpenInDiscoverDrilldown({ - discover: {} as DiscoverSetup, + locator: () => ({} as DiscoverAppLocator), dataViews: () => ({} as DataViewsService), hasDiscoverAccess: () => true, application: () => ({} as ApplicationStart), diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx index 8da471d27c90e..d9d9a1f4ed955 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx @@ -18,11 +18,11 @@ import type { UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, } from '@kbn/ui-actions-enhanced-plugin/public'; import { EuiFormRow, EuiSwitch } from '@elastic/eui'; -import type { DiscoverAppLocator } from '@kbn/discover-plugin/public'; import type { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; import { i18n } from '@kbn/i18n'; import type { DataViewsService } from '@kbn/data-views-plugin/public'; import { DOC_TYPE } from '../../common/constants'; +import type { DiscoverAppLocator } from './open_in_discover_helpers'; interface EmbeddableQueryInput extends EmbeddableInput { query?: Query; diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts index 4a3248498ea52..7342b82109be9 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts @@ -5,13 +5,24 @@ * 2.0. */ -import type { DiscoverAppLocator } from '@kbn/discover-plugin/public'; -import type { Filter } from '@kbn/es-query'; +import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import type { DataViewsService } from '@kbn/data-views-plugin/public'; +import type { LocatorPublic } from '@kbn/share-plugin/public'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { Embeddable } from '../embeddable'; import { DOC_TYPE } from '../../common'; +interface DiscoverAppLocatorParams extends SerializableRecord { + timeRange?: TimeRange; + filters?: Filter[]; + indexPatternId?: string; + query?: Query | AggregateQuery | undefined; + columns?: string[]; +} + +export type DiscoverAppLocator = LocatorPublic; + interface Context { embeddable: IEmbeddable; filters?: Filter[]; From 954ba1c2b22780681d26a93217d0909d0f5f29ea Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Sun, 23 Oct 2022 23:32:09 -0300 Subject: [PATCH 08/84] [Discover] Add support for breakdown field selector to unifiedHistogram --- .../layout/discover_main_content.tsx | 12 ++- .../layout/use_discover_histogram.ts | 27 ++++++- .../breakdown/breakdown_field_selector.tsx | 59 ++++++++++++++ .../unified_histogram/public/chart/chart.tsx | 76 ++++++++++--------- .../public/chart/histogram.tsx | 5 +- .../public/layout/layout.tsx | 10 ++- 6 files changed, 148 insertions(+), 41 deletions(-) create mode 100644 src/plugins/unified_histogram/public/breakdown/breakdown_field_selector.tsx diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 3eabea48b22ca..4551f3d6d903e 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -14,6 +14,7 @@ import { METRIC_TYPE } from '@kbn/analytics'; import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DataTableRecord } from '../../../../types'; import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; @@ -88,10 +89,12 @@ export const DiscoverMainContent = ({ topPanelHeight, hits, chart, + breakdown, onEditVisualization, onTopPanelHeightChange, onChartHiddenChange, onTimeIntervalChange, + onBreakdownFieldChange, } = useDiscoverHistogram({ stateContainer, state, @@ -102,6 +105,10 @@ export const DiscoverMainContent = ({ isPlainRecord, }); + const resetSearchButtonWrapper = css` + overflow: hidden; + `; + return ( + { + const fieldName = storage.get(HISTOGRAM_BREAKDOWN_FIELD_KEY); + return dataView.getFieldByName(fieldName); + }); + + const onBreakdownFieldChange = useCallback( + (breakdownField: DataViewField | undefined) => { + storage.set(HISTOGRAM_BREAKDOWN_FIELD_KEY, breakdownField?.name); + setField(breakdownField); + }, + [storage] + ); + + const breakdown = useMemo( + () => (isPlainRecord || !isTimeBased ? undefined : { field }), + [field, isPlainRecord, isTimeBased] + ); + return { topPanelHeight, hits, chart, + breakdown, onEditVisualization: canVisualize ? onEditVisualization : undefined, onTopPanelHeightChange, onChartHiddenChange, onTimeIntervalChange, + onBreakdownFieldChange, }; }; diff --git a/src/plugins/unified_histogram/public/breakdown/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/breakdown/breakdown_field_selector.tsx new file mode 100644 index 0000000000000..1369ffce513d4 --- /dev/null +++ b/src/plugins/unified_histogram/public/breakdown/breakdown_field_selector.tsx @@ -0,0 +1,59 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { EuiComboBox, EuiComboBoxOptionOption, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import React, { useCallback } from 'react'; +import { UnifiedHistogramBreakdownContext } from '../types'; + +export interface BreakdownFieldSelectorProps { + dataView: DataView; + breakdown?: UnifiedHistogramBreakdownContext; + onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; +} + +export const BreakdownFieldSelector = ({ + dataView, + breakdown, + onBreakdownFieldChange, +}: BreakdownFieldSelectorProps) => { + const fieldOptions = dataView.fields + .filter((field) => field.aggregatable) + .map((field) => ({ label: field.name })); + + const selectedFields = breakdown?.field ? [{ label: breakdown.field.name }] : []; + + const onFieldChange = useCallback( + (newOptions: EuiComboBoxOptionOption[]) => { + const field = newOptions.length + ? dataView.fields.find((currentField) => currentField.name === newOptions[0].label) + : undefined; + + onBreakdownFieldChange?.(field); + }, + [dataView.fields, onBreakdownFieldChange] + ); + + const { euiTheme } = useEuiTheme(); + const breakdownCss = css` + max-width: ${euiTheme.base * 22}px; + `; + + return ( + + ); +}; diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index d818e6426e67f..7db78b67b153d 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -10,8 +10,6 @@ import type { ReactElement } from 'react'; import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import { EuiButtonIcon, - EuiComboBox, - EuiComboBoxOptionOption, EuiContextMenu, EuiFlexGroup, EuiFlexItem, @@ -22,7 +20,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { HitsCounter } from '../hits_counter'; import { Histogram } from './histogram'; import { useChartPanels } from './use_chart_panels'; @@ -32,6 +30,7 @@ import type { UnifiedHistogramHitsContext, UnifiedHistogramServices, } from '../types'; +import { BreakdownFieldSelector } from '../breakdown/breakdown_field_selector'; export interface ChartProps { className?: string; @@ -46,6 +45,7 @@ export interface ChartProps { onResetChartHeight?: () => void; onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; + onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; } const HistogramMemoized = memo(Histogram); @@ -63,6 +63,7 @@ export function Chart({ onResetChartHeight, onChartHiddenChange, onTimeIntervalChange, + onBreakdownFieldChange, }: ChartProps) { const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); @@ -99,9 +100,12 @@ export function Chart({ onResetChartHeight, }); + const chartVisible = chart && !chart.hidden; + const { euiTheme } = useEuiTheme(); const resultCountCss = css` - padding: ${euiTheme.size.s} ${euiTheme.size.s} 0 ${euiTheme.size.s}; + padding: ${euiTheme.size.s} ${euiTheme.size.s} ${chartVisible ? 0 : euiTheme.size.s} + ${euiTheme.size.s}; min-height: ${euiTheme.base * 2.5}px; `; const resultCountTitleCss = css` @@ -126,16 +130,18 @@ export function Chart({ stroke-width: 1; } `; - - const options = dataView.fields - .filter((field) => field.aggregatable) - .map((field) => ({ label: field.name })); - - const [selectedOptions, setSelectedOptions] = useState(); - - const onChange = (newOptions: EuiComboBoxOptionOption[]) => { - setSelectedOptions(newOptions); - }; + const breakdownFieldSelectorGroupCss = css` + width: 100%; + `; + const breakdownFieldSelectorItemCss = css` + align-items: flex-end; + padding-left: ${euiTheme.size.s}; + `; + const chartToolButtonCss = css` + display: flex; + justify-content: center; + padding-left: ${euiTheme.size.s}; + `; return ( } {chart && ( - - - - - + + + {chartVisible && ( + + + + )} {onEditVisualization && ( - + )} - + - {chart && !chart.hidden && ( + {chartVisible && (
(chartRef.current.element = element)} @@ -236,9 +244,7 @@ export function Chart({ services={services} dataView={dataView} chart={chart} - breakdown={{ - field: dataView.fields.find((field) => field.name === selectedOptions?.[0]?.label), - }} + breakdown={breakdown} />
{appendHistogram} diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 32c322aa4a336..190cad517d8b3 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -78,11 +78,14 @@ export function Histogram({ const { euiTheme } = useEuiTheme(); const chartCss = css` flex-grow: 1; - padding: 0; & > div { height: 100%; } + + & .echLegend .echLegendList { + padding-right: ${euiTheme.size.s}; + } `; if (!chartData || !dataView.id || !dataView.isTimeBased()) { diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index ef973f8d30fdb..b085ebcb84b34 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -11,7 +11,7 @@ import type { PropsWithChildren, ReactElement, RefObject } from 'react'; import React, { useMemo } from 'react'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { css } from '@emotion/css'; -import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { Chart } from '../chart'; import { Panels, PANELS_MODE } from '../panels'; import type { @@ -65,6 +65,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * Callback to update the time interval -- should set {@link UnifiedHistogramChartContext.timeInterval} to timeInterval */ onTimeIntervalChange?: (timeInterval: string) => void; + /** + * Callback to update the breakdown field -- should set {@link UnifiedHistogramBreakdownContext.field} to breakdownField + */ + onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; } export const UnifiedHistogramLayout = ({ @@ -81,6 +85,7 @@ export const UnifiedHistogramLayout = ({ onEditVisualization, onChartHiddenChange, onTimeIntervalChange, + onBreakdownFieldChange, children, }: UnifiedHistogramLayoutProps) => { const topPanelNode = useMemo( @@ -97,7 +102,7 @@ export const UnifiedHistogramLayout = ({ const showFixedPanels = isMobile || !chart || chart.hidden; const { euiTheme } = useEuiTheme(); const defaultTopPanelHeight = euiTheme.base * 12; - const minTopPanelHeight = euiTheme.base * 8; + const minTopPanelHeight = euiTheme.base * 11; const minMainPanelHeight = euiTheme.base * 10; const chartClassName = @@ -138,6 +143,7 @@ export const UnifiedHistogramLayout = ({ onResetChartHeight={onResetChartHeight} onChartHiddenChange={onChartHiddenChange} onTimeIntervalChange={onTimeIntervalChange} + onBreakdownFieldChange={onBreakdownFieldChange} /> {children} From 90848449ad4fe432c6522094820bc095d6ffcdec Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 24 Oct 2022 16:36:55 -0300 Subject: [PATCH 09/84] [Discover] Fix Lens histogram loading indicator position --- src/plugins/unified_histogram/public/chart/histogram.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 190cad517d8b3..04c0ac80fd9e1 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -77,6 +77,7 @@ export function Histogram({ const { euiTheme } = useEuiTheme(); const chartCss = css` + position: relative; flex-grow: 1; & > div { @@ -86,6 +87,13 @@ export function Histogram({ & .echLegend .echLegendList { padding-right: ${euiTheme.size.s}; } + + & > .euiLoadingChart { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } `; if (!chartData || !dataView.id || !dataView.isTimeBased()) { From 5578336fccce3a095abffd9b0a749b198ccef4a9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 24 Oct 2022 16:40:51 -0300 Subject: [PATCH 10/84] [Discover] Fix Lens histogram not reacting to query/filter change --- .../public/chart/get_lens_attributes.ts | 10 +++++----- .../unified_histogram/public/chart/histogram.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts index 870448408d7b1..25300de632db0 100644 --- a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; import type { CountIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -17,18 +17,18 @@ import type { } from '@kbn/lens-plugin/public'; export const getLensAttributes = ({ - data, + filters, + query, dataView, timeInterval, breakdownField, }: { - data: DataPublicPluginStart; + filters: Filter[]; + query: Query | AggregateQuery; dataView: DataView; timeInterval: string | undefined; breakdownField: DataViewField | undefined; }) => { - const filters = data.query.filterManager.getFilters(); - const query = data.query.queryString.getQuery(); const showBreakdown = breakdownField?.aggregatable; let columnOrder = ['date_column', 'count_column']; diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 04c0ac80fd9e1..dc542ea4fbf1d 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -33,9 +33,11 @@ export function Histogram({ chart: { timeInterval, bucketInterval, data: chartData }, breakdown: { field: breakdownField } = {}, }: HistogramProps) { + const filters = data.query.filterManager.getFilters(); + const query = data.query.queryString.getQuery(); const attributes = useMemo( - () => getLensAttributes({ data, dataView, timeInterval, breakdownField }), - [breakdownField, data, dataView, timeInterval] + () => getLensAttributes({ filters, query, dataView, timeInterval, breakdownField }), + [breakdownField, dataView, filters, query, timeInterval] ); const { timefilter } = data.query.timefilter; From 9cccac2134a03b6977da8e908d9f310950be066c Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 24 Oct 2022 23:28:18 -0300 Subject: [PATCH 11/84] [Discover] Add support for total hits --- .../main/hooks/use_saved_search.ts | 2 -- .../application/main/utils/fetch_all.ts | 6 ------ .../unified_histogram/public/chart/chart.tsx | 9 ++++++++- .../public/chart/get_lens_attributes.ts | 6 +++--- .../public/chart/histogram.tsx | 15 +++++++++++++++ .../public/hits_counter/hits_counter.tsx | 19 +++++++++++-------- 6 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 2f097daac982d..72396c0578db0 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -78,8 +78,6 @@ export interface DataDocumentsMsg extends DataMsg { } export interface DataTotalHitsMsg extends DataMsg { - fetchStatus: FetchStatus; - error?: Error; result?: number; } diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index d530da1492fac..213bdca335e1a 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -164,12 +164,6 @@ export function fetchAll( charts ?.then((chart) => { - dataSubjects.totalHits$.next({ - fetchStatus: FetchStatus.COMPLETE, - result: chart.totalHits, - recordRawType, - }); - dataSubjects.charts$.next({ fetchStatus: FetchStatus.COMPLETE, response: chart.response, diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 7db78b67b153d..342603dde99a5 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -100,6 +100,12 @@ export function Chart({ onResetChartHeight, }); + const [totalHits, setTotalHits] = useState(); + + const onTotalHitsChange = useCallback((newTotalHits: number) => { + setTotalHits(newTotalHits); + }, []); + const chartVisible = chart && !chart.hidden; const { euiTheme } = useEuiTheme(); @@ -158,7 +164,7 @@ export function Chart({ className="eui-textTruncate eui-textNoWrap" css={resultCountTitleCss} > - {hits && } + {hits && }
{chart && ( @@ -245,6 +251,7 @@ export function Chart({ dataView={dataView} chart={chart} breakdown={breakdown} + onTotalHitsChange={onTotalHitsChange} /> {appendHistogram} diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts index 25300de632db0..a630633012012 100644 --- a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -96,7 +96,7 @@ export const getLensAttributes = ({ }, { id: dataView.id ?? '', - name: 'indexpattern-datasource-layer-layer1', + name: 'indexpattern-datasource-layer-unifiedHistogram', type: 'index-pattern', }, ], @@ -104,7 +104,7 @@ export const getLensAttributes = ({ datasourceStates: { formBased: { layers: { - layer1: { columnOrder, columns }, + unifiedHistogram: { columnOrder, columns }, }, }, }, @@ -114,7 +114,7 @@ export const getLensAttributes = ({ layers: [ { accessors: ['count_column'], - layerId: 'layer1', + layerId: 'unifiedHistogram', layerType: 'data', seriesType: 'bar_stacked', xAccessor: 'date_column', diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index dc542ea4fbf1d..a172ad4e455f1 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -13,6 +13,7 @@ import { css } from '@emotion/react'; import React, { useCallback, useMemo } from 'react'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; import type { UnifiedHistogramBreakdownContext, UnifiedHistogramChartContext, @@ -25,6 +26,7 @@ export interface HistogramProps { dataView: DataView; chart: UnifiedHistogramChartContext; breakdown?: UnifiedHistogramBreakdownContext; + onTotalHitsChange: (totalHits: number) => void; } export function Histogram({ @@ -32,6 +34,7 @@ export function Histogram({ dataView, chart: { timeInterval, bucketInterval, data: chartData }, breakdown: { field: breakdownField } = {}, + onTotalHitsChange, }: HistogramProps) { const filters = data.query.filterManager.getFilters(); const query = data.query.queryString.getQuery(); @@ -77,6 +80,17 @@ export function Histogram({ return `${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${intervalText}`; }, [from, to, timeInterval, bucketInterval?.description, toMoment]); + const onLoad = useCallback( + (_, adapters: Partial | undefined) => { + const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount; + + if (totalHits) { + onTotalHitsChange(totalHits); + } + }, + [onTotalHitsChange] + ); + const { euiTheme } = useEuiTheme(); const chartCss = css` position: relative; @@ -164,6 +178,7 @@ export function Histogram({ timeRange={{ from, to }} attributes={attributes} noPadding + onLoad={onLoad} />
{timeRange} diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx index 39df40650557c..e6296429576a8 100644 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx @@ -16,21 +16,24 @@ import type { UnifiedHistogramHitsContext } from '../types'; export interface HitsCounterProps { hits: UnifiedHistogramHitsContext; + totalHits?: number; append?: ReactElement; } -export function HitsCounter({ hits, append }: HitsCounterProps) { - if (!hits.total && hits.status === 'loading') { +export function HitsCounter({ hits, totalHits, append }: HitsCounterProps) { + if (!hits.total && hits.status === 'loading' && !totalHits) { return null; } const formattedHits = ( - + ); @@ -48,23 +51,23 @@ export function HitsCounter({ hits, append }: HitsCounterProps) { > - {hits.status === 'partial' && ( + {hits.status === 'partial' && !totalHits && ( )} - {hits.status !== 'partial' && ( + {(hits.status !== 'partial' || totalHits) && ( )} - {hits.status === 'partial' && ( + {hits.status === 'partial' && !totalHits && ( Date: Tue, 25 Oct 2022 23:02:25 -0300 Subject: [PATCH 12/84] [Discover] Removed chart data fetching from Discover and Unified Histogram --- .../layout/use_discover_histogram.ts | 31 +--- .../main/hooks/use_saved_search.ts | 1 - .../main/hooks/use_saved_search_messages.ts | 5 - .../application/main/utils/fetch_all.test.ts | 28 --- .../application/main/utils/fetch_all.ts | 18 +- .../main/utils/fetch_chart.test.ts | 134 -------------- .../application/main/utils/fetch_chart.ts | 75 -------- src/plugins/unified_histogram/kibana.json | 2 +- .../breakdown_field_selector.tsx | 0 ....test.ts => build_bucket_interval.test.ts} | 10 +- ...chart_data.ts => build_bucket_interval.ts} | 17 +- .../chart/build_point_series_data.test.ts | 120 ------------- .../public/chart/build_point_series_data.ts | 45 ----- .../unified_histogram/public/chart/chart.tsx | 11 +- .../public/chart/get_dimensions.test.ts | 58 ------ .../public/chart/get_dimensions.ts | 56 ------ .../public/chart/histogram.tsx | 166 +++++++----------- .../unified_histogram/public/chart/index.ts | 2 - .../public/chart/use_time_range.tsx | 126 +++++++++++++ src/plugins/unified_histogram/public/index.ts | 4 +- src/plugins/unified_histogram/public/types.ts | 86 +-------- 21 files changed, 211 insertions(+), 784 deletions(-) delete mode 100644 src/plugins/discover/public/application/main/utils/fetch_chart.test.ts delete mode 100644 src/plugins/discover/public/application/main/utils/fetch_chart.ts rename src/plugins/unified_histogram/public/{breakdown => chart}/breakdown_field_selector.tsx (100%) rename src/plugins/unified_histogram/public/chart/{build_chart_data.test.ts => build_bucket_interval.test.ts} (94%) rename src/plugins/unified_histogram/public/chart/{build_chart_data.ts => build_bucket_interval.ts} (74%) delete mode 100644 src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts delete mode 100644 src/plugins/unified_histogram/public/chart/build_point_series_data.ts delete mode 100644 src/plugins/unified_histogram/public/chart/get_dimensions.test.ts delete mode 100644 src/plugins/unified_histogram/public/chart/get_dimensions.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_time_range.tsx diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 45e200020e5a2..f6d52122202eb 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -12,7 +12,6 @@ import { getVisualizeInformation, triggerVisualizeActions, } from '@kbn/unified-field-list-plugin/public'; -import { buildChartData } from '@kbn/unified-histogram-plugin/public'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { getUiActions } from '../../../../kibana_services'; import { PLUGIN_ID } from '../../../../../common'; @@ -42,7 +41,7 @@ export const useDiscoverHistogram = ({ isTimeBased: boolean; isPlainRecord: boolean; }) => { - const { storage, data } = useDiscoverServices(); + const { storage } = useDiscoverServices(); /** * Visualize @@ -134,41 +133,15 @@ export const useDiscoverHistogram = ({ [hitsFetchStatus, hitsTotal, isPlainRecord] ); - const { fetchStatus: chartFetchStatus, response, error } = useDataState(savedSearchData$.charts$); - - const { bucketInterval, chartData } = useMemo( - () => - buildChartData({ - data, - dataView, - timeInterval: state.interval, - response, - }), - [data, dataView, response, state.interval] - ); - const chart = useMemo( () => isPlainRecord || !isTimeBased ? undefined : { - status: chartFetchStatus, hidden: state.hideChart, timeInterval: state.interval, - bucketInterval, - data: chartData, - error, }, - [ - bucketInterval, - chartData, - chartFetchStatus, - error, - isPlainRecord, - isTimeBased, - state.hideChart, - state.interval, - ] + [isPlainRecord, isTimeBased, state.hideChart, state.interval] ); /** diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 72396c0578db0..787a077b0d5e3 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -30,7 +30,6 @@ export interface SavedSearchData { main$: DataMain$; documents$: DataDocuments$; totalHits$: DataTotalHits$; - charts$: DataCharts$; availableFields$: AvailableFields$; } diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts index ae5abb36378a8..c0ad37b952207 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts @@ -105,11 +105,6 @@ export function sendResetMsg(data: SavedSearchData, initialFetchStatus: FetchSta result: [], recordRawType, }); - data.charts$.next({ - fetchStatus: initialFetchStatus, - response: undefined, - recordRawType, - }); data.totalHits$.next({ fetchStatus: initialFetchStatus, result: undefined, diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 59dbc3ffe73d8..1c4aa235afea0 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -17,7 +17,6 @@ import { discoverServiceMock } from '../../../__mocks__/services'; import { fetchAll } from './fetch_all'; import { DataAvailableFieldsMsg, - DataChartsMessage, DataDocumentsMsg, DataMainMsg, DataTotalHitsMsg, @@ -26,11 +25,9 @@ import { import { fetchDocuments } from './fetch_documents'; import { fetchSql } from './fetch_sql'; -import { fetchChart } from './fetch_chart'; import { fetchTotalHits } from './fetch_total_hits'; import { buildDataTableRecord } from '../../../utils/build_data_record'; import { dataViewMock } from '../../../__mocks__/data_view'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; jest.mock('./fetch_documents', () => ({ fetchDocuments: jest.fn().mockResolvedValue([]), @@ -40,17 +37,12 @@ jest.mock('./fetch_sql', () => ({ fetchSql: jest.fn().mockResolvedValue([]), })); -jest.mock('./fetch_chart', () => ({ - fetchChart: jest.fn(), -})); - jest.mock('./fetch_total_hits', () => ({ fetchTotalHits: jest.fn(), })); const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction; const mockFetchTotalHits = fetchTotalHits as unknown as jest.MockedFunction; -const mockFetchChart = fetchChart as unknown as jest.MockedFunction; const mockFetchSQL = fetchSql as unknown as jest.MockedFunction; function subjectCollector(subject: Subject): () => Promise { @@ -73,7 +65,6 @@ describe('test fetchAll', () => { main$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), documents$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), totalHits$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), - charts$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED }), availableFields$: new BehaviorSubject({ fetchStatus: FetchStatus.UNINITIALIZED, }), @@ -98,9 +89,6 @@ describe('test fetchAll', () => { mockFetchDocuments.mockReset().mockResolvedValue([]); mockFetchSQL.mockReset().mockResolvedValue([]); mockFetchTotalHits.mockReset().mockResolvedValue(42); - mockFetchChart - .mockReset() - .mockResolvedValue({ totalHits: 42, response: {} as unknown as SearchResponse }); }); test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async () => { @@ -157,25 +145,9 @@ describe('test fetchAll', () => { ]); }); - test('emits loading and response on charts$ correctly', async () => { - const collect = subjectCollector(subjects.charts$); - searchSource.getField('index')!.isTimeBased = () => true; - await fetchAll(subjects, searchSource, false, deps); - expect(await collect()).toEqual([ - { fetchStatus: FetchStatus.UNINITIALIZED }, - { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, - { - fetchStatus: FetchStatus.COMPLETE, - recordRawType: 'document', - response: {}, - }, - ]); - }); - test('should use charts query to fetch total hit count when chart is visible', async () => { const collect = subjectCollector(subjects.totalHits$); searchSource.getField('index')!.isTimeBased = () => true; - mockFetchChart.mockResolvedValue({ totalHits: 32, response: {} as unknown as SearchResponse }); await fetchAll(subjects, searchSource, false, deps); expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 213bdca335e1a..5a09f07dbdbfd 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -22,7 +22,6 @@ import { import { updateSearchSource } from './update_search_source'; import { fetchDocuments } from './fetch_documents'; import { fetchTotalHits } from './fetch_total_hits'; -import { fetchChart } from './fetch_chart'; import { AppState } from '../services/discover_state'; import { FetchStatus } from '../../types'; import { @@ -105,7 +104,6 @@ export function fetchAll( sendLoadingMsg(dataSubjects.main$, recordRawType); sendLoadingMsg(dataSubjects.documents$, recordRawType, query); sendLoadingMsg(dataSubjects.totalHits$, recordRawType); - sendLoadingMsg(dataSubjects.charts$, recordRawType); const isChartVisible = !hideChart && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP; @@ -115,8 +113,6 @@ export function fetchAll( useSql && query ? fetchSql(query, services.dataViews, data, services.expressions) : fetchDocuments(searchSource.createCopy(), fetchDeps); - const charts = - isChartVisible && !useSql ? fetchChart(searchSource.createCopy(), fetchDeps) : undefined; const totalHits = !isChartVisible && !useSql ? fetchTotalHits(searchSource.createCopy(), fetchDeps) : undefined; /** @@ -162,18 +158,6 @@ export function fetchAll( // but their errors will be shown in-place (e.g. of the chart). .catch(sendErrorTo(dataSubjects.documents$, dataSubjects.main$)); - charts - ?.then((chart) => { - dataSubjects.charts$.next({ - fetchStatus: FetchStatus.COMPLETE, - response: chart.response, - recordRawType, - }); - - checkHitCount(chart.totalHits); - }) - .catch(sendErrorTo(dataSubjects.charts$, dataSubjects.totalHits$)); - totalHits ?.then((hitCount) => { dataSubjects.totalHits$.next({ @@ -186,7 +170,7 @@ export function fetchAll( .catch(sendErrorTo(dataSubjects.totalHits$)); // Return a promise that will resolve once all the requests have finished or failed - return Promise.allSettled([documents, charts, totalHits]).then(() => { + return Promise.allSettled([documents, totalHits]).then(() => { // Send a complete message to main$ once all queries are done and if main$ // is not already in an ERROR state, e.g. because the document query has failed. // This will only complete main$, if it hasn't already been completed previously diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts deleted file mode 100644 index e1020404d3996..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ -import { of, throwError as throwErrorRx } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; -import { fetchChart, updateSearchSource } from './fetch_chart'; -import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; -import { AppState } from '../services/discover_state'; -import { discoverServiceMock } from '../../../__mocks__/services'; -import { calculateBounds } from '@kbn/data-plugin/public'; -import { FetchDeps } from './fetch_all'; - -function getDeps() { - const deps = { - appStateContainer: { - getState: () => { - return { interval: 'auto' }; - }, - } as ReduxLikeStateContainer, - abortController: new AbortController(), - data: discoverServiceMock.data, - inspectorAdapters: { requests: new RequestAdapter() }, - onResults: jest.fn(), - savedSearch: savedSearchMockWithTimeField, - searchSessionId: '123', - } as unknown as FetchDeps; - deps.data.query.timefilter.timefilter.getTime = () => { - return { from: '2021-07-07T00:05:13.590', to: '2021-07-07T11:20:13.590' }; - }; - - deps.data.query.timefilter.timefilter.calculateBounds = (timeRange) => calculateBounds(timeRange); - return deps; -} - -const requestResult = { - id: 'Fjk5bndxTHJWU2FldVRVQ0tYR0VqOFEcRWtWNDhOdG5SUzJYcFhONVVZVTBJQToxMDMwOQ==', - rawResponse: { - took: 2, - timed_out: false, - _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, - hits: { max_score: null, hits: [], total: 42 }, - aggregations: { - '2': { - buckets: [ - { - key_as_string: '2021-07-07T06:36:00.000+02:00', - key: 1625632560000, - doc_count: 1, - }, - ], - }, - }, - }, - isPartial: false, - isRunning: false, - total: 1, - loaded: 1, - isRestored: false, -}; - -describe('test fetchCharts', () => { - test('updateSearchSource helper function', () => { - const chartAggConfigs = updateSearchSource( - savedSearchMockWithTimeField.searchSource, - 'auto', - discoverServiceMock.data - ); - expect(chartAggConfigs.aggs).toMatchInlineSnapshot(` - Array [ - Object { - "enabled": true, - "id": "1", - "params": Object { - "emptyAsNull": false, - }, - "schema": "metric", - "type": "count", - }, - Object { - "enabled": true, - "id": "2", - "params": Object { - "drop_partials": false, - "extendToTimeRange": false, - "extended_bounds": Object {}, - "field": "timestamp", - "interval": "auto", - "min_doc_count": 1, - "scaleMetricValues": false, - "useNormalizedEsInterval": true, - "used_interval": "0ms", - }, - "schema": "segment", - "type": "date_histogram", - }, - ] - `); - }); - - test('resolves with summarized chart data', async () => { - savedSearchMockWithTimeField.searchSource.fetch$ = () => of(requestResult); - - const result = await fetchChart(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(result).toHaveProperty('totalHits', 42); - expect(result).toHaveProperty('response'); - }); - - test('rejects promise on query failure', async () => { - savedSearchMockWithTimeField.searchSource.fetch$ = () => - throwErrorRx(() => new Error('Oh noes!')); - - await expect(fetchChart(savedSearchMockWithTimeField.searchSource, getDeps())).rejects.toEqual( - new Error('Oh noes!') - ); - }); - - test('fetch$ is called with request specific execution context', async () => { - const fetch$Mock = jest.fn().mockReturnValue(of(requestResult)); - - savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; - - await fetchChart(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` - Object { - "description": "fetch chart data and total hits", - } - `); - }); -}); diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.ts deleted file mode 100644 index e4e5b67782cb9..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { filter, map } from 'rxjs/operators'; -import { lastValueFrom } from 'rxjs'; -import { DataPublicPluginStart, isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { getChartAggConfigs } from '@kbn/unified-histogram-plugin/public'; -import { FetchDeps } from './fetch_all'; - -interface Result { - totalHits: number; - response: SearchResponse; -} - -export function fetchChart( - searchSource: ISearchSource, - { abortController, appStateContainer, data, inspectorAdapters, searchSessionId }: FetchDeps -): Promise { - const timeInterval = appStateContainer.getState().interval ?? 'auto'; - - updateSearchSource(searchSource, timeInterval, data); - - const executionContext = { - description: 'fetch chart data and total hits', - }; - - const fetch$ = searchSource - .fetch$({ - abortSignal: abortController.signal, - sessionId: searchSessionId, - inspector: { - adapter: inspectorAdapters.requests, - title: i18n.translate('discover.inspectorRequestDataTitleChart', { - defaultMessage: 'Chart data', - }), - description: i18n.translate('discover.inspectorRequestDescriptionChart', { - defaultMessage: - 'This request queries Elasticsearch to fetch the aggregation data for the chart.', - }), - }, - executionContext, - }) - .pipe( - filter((res) => isCompleteResponse(res)), - map((res) => ({ - response: res.rawResponse, - totalHits: res.rawResponse.hits.total as number, - })) - ); - - return lastValueFrom(fetch$); -} - -export function updateSearchSource( - searchSource: ISearchSource, - timeInterval: string, - data: DataPublicPluginStart -) { - const dataView = searchSource.getField('index')!; - searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(dataView)); - searchSource.setField('size', 0); - searchSource.setField('trackTotalHits', true); - const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, data }); - searchSource.setField('aggs', chartAggConfigs.toDsl()); - searchSource.removeField('sort'); - searchSource.removeField('fields'); - return chartAggConfigs; -} diff --git a/src/plugins/unified_histogram/kibana.json b/src/plugins/unified_histogram/kibana.json index d512be7c9bc0e..826a440585b7f 100755 --- a/src/plugins/unified_histogram/kibana.json +++ b/src/plugins/unified_histogram/kibana.json @@ -11,5 +11,5 @@ "ui": true, "requiredPlugins": [], "optionalPlugins": [], - "requiredBundles": ["data", "embeddable"] + "requiredBundles": ["data", "dataViews", "embeddable", "kibanaUtils"] } diff --git a/src/plugins/unified_histogram/public/breakdown/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx similarity index 100% rename from src/plugins/unified_histogram/public/breakdown/breakdown_field_selector.tsx rename to src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx diff --git a/src/plugins/unified_histogram/public/chart/build_chart_data.test.ts b/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts similarity index 94% rename from src/plugins/unified_histogram/public/chart/build_chart_data.test.ts rename to src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts index 6c920a4a1a5ab..0ccd23357b347 100644 --- a/src/plugins/unified_histogram/public/chart/build_chart_data.test.ts +++ b/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts @@ -9,7 +9,7 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { calculateBounds } from '@kbn/data-plugin/public'; -import { buildChartData } from './build_chart_data'; +import { buildBucketInterval } from './build_bucket_interval'; describe('buildChartData', () => { const getOptions = () => { @@ -99,27 +99,27 @@ describe('buildChartData', () => { }; it('should return the correct data', () => { - const { bucketInterval, chartData } = buildChartData(getOptions()); + const { bucketInterval, chartData } = buildBucketInterval(getOptions()); expect(bucketInterval!.toString()).toEqual('P0D'); expect(JSON.stringify(chartData)).toEqual(JSON.stringify(expectedChartData)); }); it('should return an empty object if response or timeInterval is undefined', () => { expect( - buildChartData({ + buildBucketInterval({ ...getOptions(), response: undefined, timeInterval: undefined, }) ).toEqual({}); expect( - buildChartData({ + buildBucketInterval({ ...getOptions(), response: undefined, }) ).toEqual({}); expect( - buildChartData({ + buildBucketInterval({ ...getOptions(), timeInterval: undefined, }) diff --git a/src/plugins/unified_histogram/public/chart/build_chart_data.ts b/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts similarity index 74% rename from src/plugins/unified_histogram/public/chart/build_chart_data.ts rename to src/plugins/unified_histogram/public/chart/build_bucket_interval.ts index 03b208802ac4d..c038b79543b4c 100644 --- a/src/plugins/unified_histogram/public/chart/build_chart_data.ts +++ b/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts @@ -10,16 +10,14 @@ import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { DataPublicPluginStart, search, tabifyAggResponse } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { UnifiedHistogramBucketInterval } from '../types'; -import { buildPointSeriesData } from './build_point_series_data'; import { getChartAggConfigs } from './get_chart_agg_configs'; -import { getDimensions } from './get_dimensions'; /** * Convert the response from the chart request into a format that can be used * by the unified histogram chart. The returned object should be used to update * {@link UnifiedHistogramChartContext.bucketInterval} and {@link UnifiedHistogramChartContext.data}. */ -export const buildChartData = ({ +export const buildBucketInterval = ({ data, dataView, timeInterval, @@ -36,15 +34,10 @@ export const buildChartData = ({ const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, data }); const bucketAggConfig = chartAggConfigs.aggs[1]; - const tabifiedData = tabifyAggResponse(chartAggConfigs, response); - const dimensions = getDimensions(chartAggConfigs, data); - const bucketInterval = search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) + + tabifyAggResponse(chartAggConfigs, response); + + return search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) ? (bucketAggConfig?.buckets?.getInterval() as UnifiedHistogramBucketInterval) : undefined; - const chartData = buildPointSeriesData(tabifiedData, dimensions!); - - return { - bucketInterval, - chartData, - }; }; diff --git a/src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts b/src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts deleted file mode 100644 index 3a7f81aa4cd40..0000000000000 --- a/src/plugins/unified_histogram/public/chart/build_point_series_data.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import { buildPointSeriesData } from './build_point_series_data'; -import moment from 'moment'; -import type { Unit } from '@kbn/datemath'; - -describe('buildPointSeriesData', () => { - test('with valid data', () => { - const table = { - type: 'datatable', - columns: [ - { - id: 'col-0-2', - name: 'order_date per 30 days', - meta: { - type: 'date', - field: 'order_date', - index: 'kibana_sample_data_ecommerce', - params: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - source: 'esaggs', - sourceParams: { - dataViewId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - id: '2', - enabled: true, - type: 'date_histogram', - params: { - field: 'order_date', - timeRange: { from: 'now-15y', to: 'now' }, - useNormalizedEsInterval: true, - scaleMetricValues: false, - interval: 'auto', - used_interval: '30d', - drop_partials: false, - min_doc_count: 1, - extended_bounds: {}, - }, - schema: 'segment', - }, - }, - }, - { - id: 'col-1-1', - name: 'Count', - meta: { - type: 'number', - index: 'kibana_sample_data_ecommerce', - params: { id: 'number' }, - source: 'esaggs', - sourceParams: { - dataViewId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - }, - }, - ], - rows: [{ 'col-0-2': 1625176800000, 'col-1-1': 2139 }], - }; - const dimensions = { - x: { - accessor: 0, - label: 'order_date per 30 days', - format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - params: { - date: true, - interval: moment.duration(30, 'd'), - intervalESValue: 30, - intervalESUnit: 'd' as Unit, - format: 'YYYY-MM-DD', - bounds: { - min: moment('2006-07-29T11:08:13.078Z'), - max: moment('2021-07-29T11:08:13.078Z'), - }, - }, - }, - y: { accessor: 1, format: { id: 'number' }, label: 'Count' }, - } as const; - expect(buildPointSeriesData(table, dimensions)).toMatchInlineSnapshot(` - Object { - "ordered": Object { - "date": true, - "interval": "P30D", - "intervalESUnit": "d", - "intervalESValue": 30, - "max": "2021-07-29T11:08:13.078Z", - "min": "2006-07-29T11:08:13.078Z", - }, - "values": Array [ - Object { - "x": 1625176800000, - "y": 2139, - }, - ], - "xAxisFormat": Object { - "id": "date", - "params": Object { - "pattern": "YYYY-MM-DD", - }, - }, - "xAxisLabel": "order_date per 30 days", - "xAxisOrderedValues": Array [ - 1625176800000, - ], - "yAxisFormat": Object { - "id": "number", - }, - "yAxisLabel": "Count", - } - `); - }); -}); diff --git a/src/plugins/unified_histogram/public/chart/build_point_series_data.ts b/src/plugins/unified_histogram/public/chart/build_point_series_data.ts deleted file mode 100644 index dc9d97fd0708f..0000000000000 --- a/src/plugins/unified_histogram/public/chart/build_point_series_data.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import { uniq } from 'lodash'; -import type { UnifiedHistogramChartData, Dimensions, Table } from '../types'; - -export const buildPointSeriesData = ( - table: Table, - dimensions: Dimensions -): UnifiedHistogramChartData => { - const { x, y } = dimensions; - const xAccessor = table.columns[x.accessor].id; - const yAccessor = table.columns[y.accessor].id; - const chart = {} as UnifiedHistogramChartData; - - chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); - chart.xAxisFormat = x.format; - chart.xAxisLabel = table.columns[x.accessor].name; - chart.yAxisFormat = y.format; - const { intervalESUnit, intervalESValue, interval, bounds } = x.params; - chart.ordered = { - date: true, - interval, - intervalESUnit, - intervalESValue, - min: bounds.min, - max: bounds.max, - }; - - chart.yAxisLabel = table.columns[y.accessor].name; - - chart.values = table.rows - .filter((row) => row && row[yAccessor] !== 'NaN') - .map((row) => ({ - x: row[xAccessor] as number, - y: row[yAccessor] as number, - })); - - return chart; -}; diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 342603dde99a5..e960a94031cbc 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; import { HitsCounter } from '../hits_counter'; import { Histogram } from './histogram'; import { useChartPanels } from './use_chart_panels'; @@ -30,7 +30,7 @@ import type { UnifiedHistogramHitsContext, UnifiedHistogramServices, } from '../types'; -import { BreakdownFieldSelector } from '../breakdown/breakdown_field_selector'; +import { BreakdownFieldSelector } from './breakdown_field_selector'; export interface ChartProps { className?: string; @@ -106,7 +106,12 @@ export function Chart({ setTotalHits(newTotalHits); }, []); - const chartVisible = chart && !chart.hidden; + const chartVisible = + chart && + !chart.hidden && + dataView.id && + dataView.type !== DataViewType.ROLLUP && + dataView.isTimeBased(); const { euiTheme } = useEuiTheme(); const resultCountCss = css` diff --git a/src/plugins/unified_histogram/public/chart/get_dimensions.test.ts b/src/plugins/unified_histogram/public/chart/get_dimensions.test.ts deleted file mode 100644 index fd26fa20ce793..0000000000000 --- a/src/plugins/unified_histogram/public/chart/get_dimensions.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { getDimensions } from './get_dimensions'; -import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; -import { calculateBounds } from '@kbn/data-plugin/public'; -import { getChartAggConfigs } from './get_chart_agg_configs'; - -test('getDimensions', () => { - const dataView = dataViewWithTimefieldMock; - const dataMock = dataPluginMock.createStartContract(); - dataMock.query.timefilter.timefilter.getTime = () => { - return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; - }; - dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { - return calculateBounds(timeRange); - }; - const aggsConfig = getChartAggConfigs({ dataView, timeInterval: 'auto', data: dataMock }); - const actual = getDimensions(aggsConfig!, dataMock); - expect(actual).toMatchInlineSnapshot(` - Object { - "x": Object { - "accessor": 0, - "format": Object { - "id": "date", - "params": Object { - "pattern": "HH:mm:ss.SSS", - }, - }, - "label": "timestamp per 0 milliseconds", - "params": Object { - "bounds": Object { - "max": "2021-03-29T07:04:00.695Z", - "min": "1991-03-29T08:04:00.694Z", - }, - "date": true, - "format": "HH:mm:ss.SSS", - "interval": "P0D", - "intervalESUnit": "ms", - "intervalESValue": 0, - }, - }, - "y": Object { - "accessor": 1, - "format": Object { - "id": "number", - }, - "label": "Count", - }, - } - `); -}); diff --git a/src/plugins/unified_histogram/public/chart/get_dimensions.ts b/src/plugins/unified_histogram/public/chart/get_dimensions.ts deleted file mode 100644 index 94ed3d4540d21..0000000000000 --- a/src/plugins/unified_histogram/public/chart/get_dimensions.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import moment from 'moment'; -import dateMath from '@kbn/datemath'; -import { DataPublicPluginStart, search, IAggConfigs } from '@kbn/data-plugin/public'; -import type { Dimensions, HistogramParamsBounds } from '../types'; - -export function getDimensions( - aggs: IAggConfigs, - data: DataPublicPluginStart -): Dimensions | undefined { - const [metric, agg] = aggs.aggs; - const { from, to } = data.query.timefilter.timefilter.getTime(); - agg.params.timeRange = { - from: dateMath.parse(from), - to: dateMath.parse(to, { roundUp: true }), - }; - const bounds = agg.params.timeRange - ? (data.query.timefilter.timefilter.calculateBounds( - agg.params.timeRange - ) as HistogramParamsBounds) - : null; - const buckets = search.aggs.isDateHistogramBucketAggConfig(agg) ? agg.buckets : undefined; - - if (!buckets || !bounds) { - return; - } - - const { esUnit, esValue } = buckets.getInterval(); - return { - x: { - accessor: 0, - label: agg.makeLabel(), - format: agg.toSerializedFieldFormat(), - params: { - date: true, - interval: moment.duration(esValue, esUnit), - intervalESValue: esValue, - intervalESUnit: esUnit, - format: buckets.getScaledDateFormat(), - bounds, - }, - }, - y: { - accessor: 1, - format: metric.toSerializedFieldFormat(), - label: metric.makeLabel(), - }, - }; -} diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index a172ad4e455f1..283c1e982083d 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -6,20 +6,24 @@ * Side Public License, v 1. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiText, useEuiTheme } from '@elastic/eui'; -import dateMath from '@kbn/datemath'; -import { i18n } from '@kbn/i18n'; +import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; +import { connectToQueryState, IKibanaSearchResponse, QueryState } from '@kbn/data-plugin/public'; +import type { estypes } from '@elastic/elasticsearch'; +import { createStateContainer, useContainerState } from '@kbn/kibana-utils-plugin/public'; import type { UnifiedHistogramBreakdownContext, + UnifiedHistogramBucketInterval, UnifiedHistogramChartContext, UnifiedHistogramServices, } from '../types'; import { getLensAttributes } from './get_lens_attributes'; +import { buildBucketInterval } from './build_bucket_interval'; +import { useTimeRange } from './use_time_range'; export interface HistogramProps { services: UnifiedHistogramServices; @@ -32,53 +36,47 @@ export interface HistogramProps { export function Histogram({ services: { data, lens, uiSettings }, dataView, - chart: { timeInterval, bucketInterval, data: chartData }, + chart: { timeInterval }, breakdown: { field: breakdownField } = {}, onTotalHitsChange, }: HistogramProps) { - const filters = data.query.filterManager.getFilters(); - const query = data.query.queryString.getQuery(); + const queryStateContainer = useMemo(() => { + return createStateContainer({ + filters: data.query.filterManager.getFilters(), + query: data.query.queryString.getQuery(), + refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), + time: data.query.timefilter.timefilter.getTime(), + }); + }, [data.query.filterManager, data.query.queryString, data.query.timefilter.timefilter]); + + const queryState = useContainerState(queryStateContainer); + + useEffect(() => { + return connectToQueryState(data.query, queryStateContainer, { + time: true, + query: true, + filters: true, + refreshInterval: true, + }); + }, [data.query, queryStateContainer]); + + const filters = useMemo(() => queryState.filters ?? [], [queryState.filters]); + const query = useMemo( + () => queryState.query ?? data.query.queryString.getDefaultQuery(), + [data.query.queryString, queryState.query] + ); const attributes = useMemo( () => getLensAttributes({ filters, query, dataView, timeInterval, breakdownField }), [breakdownField, dataView, filters, query, timeInterval] ); - - const { timefilter } = data.query.timefilter; - const { from, to } = timefilter.getAbsoluteTime(); - const dateFormat = useMemo(() => uiSettings.get('dateFormat'), [uiSettings]); - - const toMoment = useCallback( - (datetime: moment.Moment | undefined) => { - if (!datetime) { - return ''; - } - if (!dateFormat) { - return String(datetime); - } - return datetime.format(dateFormat); - }, - [dateFormat] - ); - - const timeRangeText = useMemo(() => { - const timeRange = { - from: dateMath.parse(from), - to: dateMath.parse(to, { roundUp: true }), - }; - const intervalText = i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', { - defaultMessage: '(interval: {value})', - values: { - value: `${ - timeInterval === 'auto' - ? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', { - defaultMessage: 'Auto', - })} - ` - : '' - }${bucketInterval?.description}`, - }, - }); - return `${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${intervalText}`; - }, [from, to, timeInterval, bucketInterval?.description, toMoment]); + const timeRange = data.query.timefilter.timefilter.getAbsoluteTime(); + const [bucketInterval, setBucketInterval] = useState(); + const { timeRangeText, timeRangeDisplay } = useTimeRange({ + uiSettings, + bucketInterval, + timeRange, + timeInterval, + }); const onLoad = useCallback( (_, adapters: Partial | undefined) => { @@ -87,8 +85,23 @@ export function Histogram({ if (totalHits) { onTotalHitsChange(totalHits); } + + const request = adapters?.requests?.getRequests()[0]; + const json = request?.response?.json as IKibanaSearchResponse; + const response = json?.rawResponse; + + if (response) { + const newBucketInterval = buildBucketInterval({ + data, + dataView, + timeInterval, + response, + }); + + setBucketInterval(newBucketInterval); + } }, - [onTotalHitsChange] + [data, dataView, onTotalHitsChange, timeInterval] ); const { euiTheme } = useEuiTheme(); @@ -112,76 +125,19 @@ export function Histogram({ } `; - if (!chartData || !dataView.id || !dataView.isTimeBased()) { - return null; - } - - const toolTipTitle = i18n.translate('unifiedHistogram.timeIntervalWithValueWarning', { - defaultMessage: 'Warning', - }); - - const toolTipContent = i18n.translate('unifiedHistogram.bucketIntervalTooltip', { - defaultMessage: - 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}.', - values: { - bucketsDescription: - bucketInterval!.scale && bucketInterval!.scale > 1 - ? i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooLargeBucketsText', { - defaultMessage: 'buckets that are too large', - }) - : i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooManyBucketsText', { - defaultMessage: 'too many buckets', - }), - bucketIntervalDescription: bucketInterval?.description, - }, - }); - - const timeRangeCss = css` - padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; - `; - - let timeRange = ( - - {timeRangeText} - - ); - - if (bucketInterval?.scaled) { - const timeRangeWrapperCss = css` - flex-grow: 0; - `; - - timeRange = ( - - {timeRange} - - - - - ); - } - - const LensComponent = lens.EmbeddableComponent; - return ( <>
-
- {timeRange} + {timeRangeDisplay} ); } diff --git a/src/plugins/unified_histogram/public/chart/index.ts b/src/plugins/unified_histogram/public/chart/index.ts index e50532f3bfec2..6a6d2d65f6f92 100644 --- a/src/plugins/unified_histogram/public/chart/index.ts +++ b/src/plugins/unified_histogram/public/chart/index.ts @@ -7,5 +7,3 @@ */ export { Chart } from './chart'; -export { getChartAggConfigs } from './get_chart_agg_configs'; -export { buildChartData } from './build_chart_data'; diff --git a/src/plugins/unified_histogram/public/chart/use_time_range.tsx b/src/plugins/unified_histogram/public/chart/use_time_range.tsx new file mode 100644 index 0000000000000..bb66cffca263f --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_time_range.tsx @@ -0,0 +1,126 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiText, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo } from 'react'; +import dateMath from '@kbn/datemath'; +import type { TimeRange } from '@kbn/data-plugin/common'; +import type { UnifiedHistogramBucketInterval } from '../types'; + +export const useTimeRange = ({ + uiSettings, + bucketInterval, + timeRange: { from, to }, + timeInterval, +}: { + uiSettings: IUiSettingsClient; + bucketInterval?: UnifiedHistogramBucketInterval; + timeRange: TimeRange; + timeInterval?: string; +}) => { + const dateFormat = useMemo(() => uiSettings.get('dateFormat'), [uiSettings]); + + const toMoment = useCallback( + (datetime?: moment.Moment) => { + if (!datetime) { + return ''; + } + if (!dateFormat) { + return String(datetime); + } + return datetime.format(dateFormat); + }, + [dateFormat] + ); + + const timeRangeText = useMemo(() => { + if (!bucketInterval) { + return ''; + } + + const timeRange = { + from: dateMath.parse(from), + to: dateMath.parse(to, { roundUp: true }), + }; + + const intervalText = i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', { + defaultMessage: '(interval: {value})', + values: { + value: `${ + timeInterval === 'auto' + ? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', { + defaultMessage: 'Auto', + })} - ` + : '' + }${bucketInterval.description}`, + }, + }); + + return `${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${intervalText}`; + }, [bucketInterval, from, timeInterval, to, toMoment]); + + const { euiTheme } = useEuiTheme(); + const timeRangeCss = css` + padding: 0 ${euiTheme.size.s} 0 ${euiTheme.size.s}; + `; + + let timeRangeDisplay = ( + + {timeRangeText} + + ); + + if (bucketInterval?.scaled) { + const toolTipTitle = i18n.translate('unifiedHistogram.timeIntervalWithValueWarning', { + defaultMessage: 'Warning', + }); + + const toolTipContent = i18n.translate('unifiedHistogram.bucketIntervalTooltip', { + defaultMessage: + 'This interval creates {bucketsDescription} to show in the selected time range, so it has been scaled to {bucketIntervalDescription}.', + values: { + bucketsDescription: + bucketInterval.scale && bucketInterval.scale > 1 + ? i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooLargeBucketsText', { + defaultMessage: 'buckets that are too large', + }) + : i18n.translate('unifiedHistogram.bucketIntervalTooltip.tooManyBucketsText', { + defaultMessage: 'too many buckets', + }), + bucketIntervalDescription: bucketInterval.description, + }, + }); + + const timeRangeWrapperCss = css` + flex-grow: 0; + `; + + timeRangeDisplay = ( + + {timeRangeDisplay} + + + + + ); + } + + return { + timeRangeText, + timeRangeDisplay, + }; +}; diff --git a/src/plugins/unified_histogram/public/index.ts b/src/plugins/unified_histogram/public/index.ts index 4d3ab5d097831..55363a445576c 100644 --- a/src/plugins/unified_histogram/public/index.ts +++ b/src/plugins/unified_histogram/public/index.ts @@ -10,14 +10,12 @@ import { UnifiedHistogramPublicPlugin } from './plugin'; export type { UnifiedHistogramLayoutProps } from './layout'; export { UnifiedHistogramLayout } from './layout'; -export { getChartAggConfigs, buildChartData } from './chart'; export type { UnifiedHistogramServices, UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, UnifiedHistogramChartContext, - UnifiedHistogramChartData, - UnifiedHistogramBucketInterval, + UnifiedHistogramBreakdownContext, } from './types'; export const plugin = () => new UnifiedHistogramPublicPlugin(); diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index 9c0e4e342ca4e..275ed0de96cf3 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -10,9 +10,6 @@ import type { Theme } from '@kbn/charts-plugin/public/plugin'; import type { IUiSettingsClient } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { Duration, Moment } from 'moment'; -import type { Unit } from '@kbn/datemath'; -import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { DataViewField } from '@kbn/data-views-plugin/public'; @@ -37,71 +34,6 @@ export interface UnifiedHistogramServices { lens: LensPublicStart; } -interface Column { - id: string; - name: string; -} - -interface Row { - [key: string]: number | 'NaN'; -} - -interface Dimension { - accessor: 0 | 1; - format: SerializedFieldFormat<{ pattern: string }>; - label: string; -} - -interface Ordered { - date: true; - interval: Duration; - intervalESUnit: string; - intervalESValue: number; - min: Moment; - max: Moment; -} - -interface HistogramParams { - date: true; - interval: Duration; - intervalESValue: number; - intervalESUnit: Unit; - format: string; - bounds: HistogramParamsBounds; -} - -export interface HistogramParamsBounds { - min: Moment; - max: Moment; -} - -export interface Table { - columns: Column[]; - rows: Row[]; -} - -export interface Dimensions { - x: Dimension & { params: HistogramParams }; - y: Dimension; -} - -/** - * The chartData object returned by {@link buildChartData} that - * should be used to set {@link UnifiedHistogramChartContext.data} - */ -export interface UnifiedHistogramChartData { - values: Array<{ - x: number; - y: number; - }>; - xAxisOrderedValues: number[]; - xAxisFormat: Dimension['format']; - yAxisFormat: Dimension['format']; - xAxisLabel: Column['name']; - yAxisLabel?: Column['name']; - ordered: Ordered; -} - /** * The bucketInterval object returned by {@link buildChartData} that * should be used to set {@link UnifiedHistogramChartContext.bucketInterval} @@ -119,7 +51,7 @@ export interface UnifiedHistogramHitsContext { /** * The fetch status of the hits count request */ - status: UnifiedHistogramFetchStatus; + status?: UnifiedHistogramFetchStatus; /** * The total number of hits */ @@ -130,10 +62,6 @@ export interface UnifiedHistogramHitsContext { * Context object for the chart */ export interface UnifiedHistogramChartContext { - /** - * The fetch status of the chart request - */ - status: UnifiedHistogramFetchStatus; /** * Controls whether or not the chart is hidden */ @@ -142,18 +70,6 @@ export interface UnifiedHistogramChartContext { * Controls the time interval of the chart */ timeInterval?: string; - /** - * The bucketInterval object returned by {@link buildChartData} - */ - bucketInterval?: UnifiedHistogramBucketInterval; - /** - * The chartData object returned by {@link buildChartData} - */ - data?: UnifiedHistogramChartData; - /** - * Error from failed chart request - */ - error?: Error; } /** From 838445cc6ee229ae4c5dff5d3e6bc790b4736cb6 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Sat, 29 Oct 2022 02:02:13 -0300 Subject: [PATCH 13/84] [Discover] Update unfiied histogram to make total hits request --- .../layout/__stories__/get_layout_props.ts | 12 - .../components/layout/discover_layout.tsx | 3 + .../layout/discover_main_content.tsx | 12 + .../main/components/layout/types.ts | 2 + .../layout/use_discover_histogram.ts | 68 +++++- .../application/main/discover_main_app.tsx | 2 + .../main/hooks/use_discover_state.ts | 1 + .../main/hooks/use_saved_search.ts | 5 +- .../main/hooks/use_saved_search_messages.ts | 24 +- .../services/discover_search_session.test.ts | 1 + .../main/services/discover_search_session.ts | 12 +- .../application/main/utils/fetch_all.ts | 38 +--- .../main/utils/fetch_total_hits.test.ts | 57 ----- .../main/utils/fetch_total_hits.ts | 58 ----- .../unified_histogram/public/chart/chart.tsx | 136 +++++------- .../public/chart/histogram.tsx | 64 +++--- .../public/chart/use_chart_actions.ts | 53 +++++ .../public/chart/use_chart_panels.ts | 4 +- .../public/chart/use_chart_styles.tsx | 63 ++++++ .../public/chart/use_request_params.tsx | 51 +++++ .../public/chart/use_total_hits.ts | 207 ++++++++++++++++++ .../public/hits_counter/hits_counter.tsx | 19 +- .../public/layout/layout.tsx | 15 ++ src/plugins/unified_histogram/public/types.ts | 19 ++ .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 27 files changed, 617 insertions(+), 315 deletions(-) delete mode 100644 src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts delete mode 100644 src/plugins/discover/public/application/main/utils/fetch_total_hits.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_chart_actions.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_chart_styles.tsx create mode 100644 src/plugins/unified_histogram/public/chart/use_request_params.tsx create mode 100644 src/plugins/unified_histogram/public/chart/use_total_hits.ts diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts index 56c0f349615e0..334e899b04aee 100644 --- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts +++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts @@ -11,11 +11,9 @@ import { SearchSource } from '@kbn/data-plugin/common'; import { BehaviorSubject, Subject } from 'rxjs'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { action } from '@storybook/addon-actions'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { FetchStatus } from '../../../../types'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, @@ -47,11 +45,6 @@ const documentObservables = { fetchStatus: FetchStatus.COMPLETE, result: Number(esHits.length), }) as DataTotalHits$, - - charts$: new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: {} as unknown as SearchResponse, - }) as DataCharts$, }; const plainRecordObservables = { @@ -77,11 +70,6 @@ const plainRecordObservables = { fetchStatus: FetchStatus.COMPLETE, recordRawType: RecordRawType.PLAIN, }) as DataTotalHits$, - - charts$: new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - recordRawType: RecordRawType.PLAIN, - }) as DataCharts$, }; const getCommonProps = (dataView: DataView) => { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index a7cde67d4869b..331e4b04c736d 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -72,6 +72,7 @@ export function DiscoverLayout({ persistDataView, updateAdHocDataViewId, adHocDataViewList, + searchSessionManager, }: DiscoverLayoutProps) { const { trackUiMetric, @@ -324,6 +325,8 @@ export function DiscoverLayout({ onFieldEdited={onFieldEdited} columns={columns} resizeRef={resizeRef} + inspectorAdapters={inspectorAdapters} + searchSessionManager={searchSessionManager} /> )} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 4551f3d6d903e..70ca210e87c81 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -15,6 +15,7 @@ import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; +import type { RequestAdapter } from '@kbn/inspector-plugin/public'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DataTableRecord } from '../../../../types'; import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; @@ -25,6 +26,7 @@ import { FieldStatisticsTable } from '../field_stats_table'; import { DiscoverDocuments } from './discover_documents'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; import { useDiscoverHistogram } from './use_discover_histogram'; +import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); @@ -46,6 +48,8 @@ export interface DiscoverMainContentProps { onFieldEdited: () => Promise; columns: string[]; resizeRef: RefObject; + inspectorAdapters: { requests: RequestAdapter }; + searchSessionManager: DiscoverSearchSessionManager; } export const DiscoverMainContent = ({ @@ -66,6 +70,8 @@ export const DiscoverMainContent = ({ onFieldEdited, columns, resizeRef, + inspectorAdapters, + searchSessionManager, }: DiscoverMainContentProps) => { const services = useDiscoverServices(); const { trackUiMetric } = services; @@ -87,6 +93,7 @@ export const DiscoverMainContent = ({ const { topPanelHeight, + request, hits, chart, breakdown, @@ -95,6 +102,7 @@ export const DiscoverMainContent = ({ onChartHiddenChange, onTimeIntervalChange, onBreakdownFieldChange, + onTotalHitsChange, } = useDiscoverHistogram({ stateContainer, state, @@ -103,6 +111,8 @@ export const DiscoverMainContent = ({ savedSearch, isTimeBased, isPlainRecord, + inspectorAdapters, + searchSessionManager, }); const resetSearchButtonWrapper = css` @@ -114,6 +124,7 @@ export const DiscoverMainContent = ({ className="dscPageContent__inner" services={services} dataView={dataView} + request={request} hits={hits} chart={chart} breakdown={breakdown} @@ -144,6 +155,7 @@ export const DiscoverMainContent = ({ onChartHiddenChange={onChartHiddenChange} onTimeIntervalChange={onTimeIntervalChange} onBreakdownFieldChange={onBreakdownFieldChange} + onTotalHitsChange={onTotalHitsChange} > Promise; updateAdHocDataViewId: (dataView: DataView) => Promise; adHocDataViewList: DataView[]; + searchSessionManager: DiscoverSearchSessionManager; } diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index f6d52122202eb..0681251eb01eb 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -13,12 +13,17 @@ import { triggerVisualizeActions, } from '@kbn/unified-field-list-plugin/public'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; +import type { RequestAdapter } from '@kbn/inspector-plugin/public'; +import useDebounce from 'react-use/lib/useDebounce'; import { getUiActions } from '../../../../kibana_services'; import { PLUGIN_ID } from '../../../../../common'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDataState } from '../../hooks/use_data_state'; import type { SavedSearchData } from '../../hooks/use_saved_search'; import type { AppState, GetStateReturn } from '../../services/discover_state'; +import { FetchStatus } from '../../../types'; +import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; export const CHART_HIDDEN_KEY = 'discover:chartHidden'; export const HISTOGRAM_HEIGHT_KEY = 'discover:histogramHeight'; @@ -32,6 +37,8 @@ export const useDiscoverHistogram = ({ savedSearch, isTimeBased, isPlainRecord, + inspectorAdapters, + searchSessionManager, }: { stateContainer: GetStateReturn; state: AppState; @@ -40,6 +47,8 @@ export const useDiscoverHistogram = ({ savedSearch: SavedSearch; isTimeBased: boolean; isPlainRecord: boolean; + inspectorAdapters: { requests: RequestAdapter }; + searchSessionManager: DiscoverSearchSessionManager; }) => { const { storage } = useDiscoverServices(); @@ -115,9 +124,60 @@ export const useDiscoverHistogram = ({ ); /** - * Data + * Request */ + const [lastReloadRequestTime, setLastReloadRequestTime] = useState(0); + const { fetchStatus: mainFetchStatus } = useDataState(savedSearchData$.main$); + + // Reload unified histogram when a refetch is triggered, + // with a debounce to avoid multiple requests + const [, cancelDebounce] = useDebounce( + () => { + if (mainFetchStatus === FetchStatus.LOADING) { + setLastReloadRequestTime(Date.now()); + } + }, + 100, + [mainFetchStatus] + ); + + // A refetch is triggered when the data view is changed, + // but we don't want to reload unified histogram in this case, + // so cancel the debounced effect on unmount + useEffect(() => cancelDebounce, [cancelDebounce]); + + const searchSessionId = searchSessionManager.getLastSearchSessionId(); + const request = useMemo( + () => ({ + searchSessionId, + adapter: inspectorAdapters.requests, + lastReloadRequestTime, + }), + [inspectorAdapters.requests, lastReloadRequestTime, searchSessionId] + ); + + /** + * Total hits + */ + + const onTotalHitsChange = useCallback( + (status: UnifiedHistogramFetchStatus, totalHits?: number) => { + const { fetchStatus, recordRawType } = savedSearchData$.totalHits$.getValue(); + + if (fetchStatus === 'partial' && status === 'loading') { + return; + } + + savedSearchData$.totalHits$.next({ + fetchStatus: status as FetchStatus, + result: totalHits, + recordRawType, + }); + }, + [savedSearchData$.totalHits$] + ); + const { fetchStatus: hitsFetchStatus, result: hitsTotal } = useDataState( savedSearchData$.totalHits$ ); @@ -133,6 +193,10 @@ export const useDiscoverHistogram = ({ [hitsFetchStatus, hitsTotal, isPlainRecord] ); + /** + * Chart + */ + const chart = useMemo( () => isPlainRecord || !isTimeBased @@ -168,6 +232,7 @@ export const useDiscoverHistogram = ({ return { topPanelHeight, + request, hits, chart, breakdown, @@ -176,5 +241,6 @@ export const useDiscoverHistogram = ({ onChartHiddenChange, onTimeIntervalChange, onBreakdownFieldChange, + onTotalHitsChange, }; }; diff --git a/src/plugins/discover/public/application/main/discover_main_app.tsx b/src/plugins/discover/public/application/main/discover_main_app.tsx index abd714fea8f07..5b407c35a210d 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.tsx @@ -62,6 +62,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { state, stateContainer, adHocDataViewList, + searchSessionManager, } = useDiscoverState({ services, history: usedHistory, @@ -121,6 +122,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { persistDataView={persistDataView} updateAdHocDataViewId={updateAdHocDataViewId} adHocDataViewList={adHocDataViewList} + searchSessionManager={searchSessionManager} /> ); diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts index ff448e59917a8..7d17279af2d4f 100644 --- a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts @@ -308,5 +308,6 @@ export function useDiscoverState({ adHocDataViewList, persistDataView, updateAdHocDataViewId, + searchSessionManager, }; } diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 787a077b0d5e3..8c47ba95ec892 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -36,7 +36,6 @@ export interface SavedSearchData { export type DataMain$ = BehaviorSubject; export type DataDocuments$ = BehaviorSubject; export type DataTotalHits$ = BehaviorSubject; -export type DataCharts$ = BehaviorSubject; export type AvailableFields$ = BehaviorSubject; export type DataRefetch$ = Subject; @@ -125,7 +124,6 @@ export const useSavedSearch = ({ const main$: DataMain$ = useBehaviorSubject(initialState) as DataMain$; const documents$: DataDocuments$ = useBehaviorSubject(initialState) as DataDocuments$; const totalHits$: DataTotalHits$ = useBehaviorSubject(initialState) as DataTotalHits$; - const charts$: DataCharts$ = useBehaviorSubject(initialState) as DataCharts$; const availableFields$: AvailableFields$ = useBehaviorSubject(initialState) as AvailableFields$; const dataSubjects = useMemo(() => { @@ -133,10 +131,9 @@ export const useSavedSearch = ({ main$, documents$, totalHits$, - charts$, availableFields$, }; - }, [main$, charts$, documents$, totalHits$, availableFields$]); + }, [main$, documents$, totalHits$, availableFields$]); /** * The observable to trigger data fetching in UI diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts index c0ad37b952207..1657534353a75 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts @@ -6,14 +6,13 @@ * Side Public License, v 1. */ -import { AggregateQuery, Query } from '@kbn/es-query'; +import type { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../types'; -import { - DataCharts$, +import type { DataDocuments$, DataMain$, + DataMsg, DataTotalHits$, - RecordRawType, SavedSearchData, } from './use_saved_search'; @@ -60,27 +59,22 @@ export function sendPartialMsg(main$: DataMain$) { /** * Send LOADING message via main observable */ -export function sendLoadingMsg( - data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$, - recordRawType: RecordRawType, - query?: AggregateQuery | Query +export function sendLoadingMsg( + data$: BehaviorSubject, + props: Omit ) { if (data$.getValue().fetchStatus !== FetchStatus.LOADING) { data$.next({ + ...props, fetchStatus: FetchStatus.LOADING, - recordRawType, - query, - }); + } as T); } } /** * Send ERROR message */ -export function sendErrorMsg( - data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$, - error: Error -) { +export function sendErrorMsg(data$: DataMain$ | DataDocuments$ | DataTotalHits$, error: Error) { const recordRawType = data$.getValue().recordRawType; data$.next({ fetchStatus: FetchStatus.ERROR, diff --git a/src/plugins/discover/public/application/main/services/discover_search_session.test.ts b/src/plugins/discover/public/application/main/services/discover_search_session.test.ts index 0f854438b6749..d1d1fb398727c 100644 --- a/src/plugins/discover/public/application/main/services/discover_search_session.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.test.ts @@ -28,6 +28,7 @@ describe('DiscoverSearchSessionManager', () => { const id = searchSessionManager.getNextSearchSessionId(); expect(id).toEqual(nextId); expect(session.start).toBeCalled(); + expect(searchSessionManager.getLastSearchSessionId()).toEqual(id); }); test('restores a session using query param from the URL', () => { diff --git a/src/plugins/discover/public/application/main/services/discover_search_session.ts b/src/plugins/discover/public/application/main/services/discover_search_session.ts index 5797b0381b1bf..e91238b8a1c47 100644 --- a/src/plugins/discover/public/application/main/services/discover_search_session.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.ts @@ -32,6 +32,7 @@ export class DiscoverSearchSessionManager { */ readonly newSearchSessionIdFromURL$: Rx.Observable; private readonly deps: DiscoverSearchSessionManagerDeps; + private lastSearchSessionId?: string; constructor(deps: DiscoverSearchSessionManagerDeps) { this.deps = deps; @@ -65,7 +66,16 @@ export class DiscoverSearchSessionManager { } } - return searchSessionIdFromURL ?? this.deps.session.start(); + this.lastSearchSessionId = searchSessionIdFromURL ?? this.deps.session.start(); + + return this.lastSearchSessionId; + } + + /** + * Get the last returned session id by {@link getNextSearchSessionId} + */ + getLastSearchSessionId() { + return this.lastSearchSessionId; } /** diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 5a09f07dbdbfd..146f41c871276 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -8,7 +8,6 @@ import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; import { Adapters } from '@kbn/inspector-plugin/common'; import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; -import { DataViewType } from '@kbn/data-views-plugin/public'; import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; import { getRawRecordType } from './get_raw_record_type'; import { @@ -21,14 +20,11 @@ import { } from '../hooks/use_saved_search_messages'; import { updateSearchSource } from './update_search_source'; import { fetchDocuments } from './fetch_documents'; -import { fetchTotalHits } from './fetch_total_hits'; import { AppState } from '../services/discover_state'; import { FetchStatus } from '../../types'; import { - DataCharts$, DataDocuments$, DataMain$, - DataTotalHits$, RecordRawType, SavedSearchData, } from '../hooks/use_saved_search'; @@ -49,8 +45,7 @@ export interface FetchDeps { /** * This function starts fetching all required queries in Discover. This will be the query to load the individual - * documents, and depending on whether a chart is shown either the aggregation query to load the chart data - * or a query to retrieve just the total hits. + * documents as well as any other requests that might be required to load the main view. * * This method returns a promise, which will resolve (without a value), as soon as all queries that have been started * have been completed (failed or successfully). @@ -68,9 +63,7 @@ export function fetchAll( * to the specified subjects. It will ignore AbortErrors and will use the data * plugin to show a toast for the error (e.g. allowing better insights into shard failures). */ - const sendErrorTo = ( - ...errorSubjects: Array - ) => { + const sendErrorTo = (...errorSubjects: Array) => { return (error: Error) => { if (error instanceof Error && error.name === 'AbortError') { return; @@ -86,7 +79,7 @@ export function fetchAll( if (reset) { sendResetMsg(dataSubjects, initialFetchStatus); } - const { hideChart, sort, query } = appStateContainer.getState(); + const { sort, query } = appStateContainer.getState(); const recordRawType = getRawRecordType(query); const useSql = recordRawType === RecordRawType.PLAIN; @@ -101,20 +94,16 @@ export function fetchAll( } // Mark all subjects as loading - sendLoadingMsg(dataSubjects.main$, recordRawType); - sendLoadingMsg(dataSubjects.documents$, recordRawType, query); - sendLoadingMsg(dataSubjects.totalHits$, recordRawType); - - const isChartVisible = - !hideChart && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP; + sendLoadingMsg(dataSubjects.main$, { recordRawType }); + sendLoadingMsg(dataSubjects.documents$, { recordRawType, query }); + sendLoadingMsg(dataSubjects.totalHits$, { recordRawType }); // Start fetching all required requests const documents = useSql && query ? fetchSql(query, services.dataViews, data, services.expressions) : fetchDocuments(searchSource.createCopy(), fetchDeps); - const totalHits = - !isChartVisible && !useSql ? fetchTotalHits(searchSource.createCopy(), fetchDeps) : undefined; + /** * This method checks the passed in hit count and will send a PARTIAL message to main$ * if there are results, indicating that we have finished some of the requests that have been @@ -158,19 +147,8 @@ export function fetchAll( // but their errors will be shown in-place (e.g. of the chart). .catch(sendErrorTo(dataSubjects.documents$, dataSubjects.main$)); - totalHits - ?.then((hitCount) => { - dataSubjects.totalHits$.next({ - fetchStatus: FetchStatus.COMPLETE, - result: hitCount, - recordRawType, - }); - checkHitCount(hitCount); - }) - .catch(sendErrorTo(dataSubjects.totalHits$)); - // Return a promise that will resolve once all the requests have finished or failed - return Promise.allSettled([documents, totalHits]).then(() => { + return Promise.allSettled([documents]).then(() => { // Send a complete message to main$ once all queries are done and if main$ // is not already in an ERROR state, e.g. because the document query has failed. // This will only complete main$, if it hasn't already been completed previously diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts deleted file mode 100644 index f2851a57e7365..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ -import { throwError as throwErrorRx, of } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; -import { fetchTotalHits } from './fetch_total_hits'; -import { discoverServiceMock } from '../../../__mocks__/services'; -import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; -import { FetchDeps } from './fetch_all'; - -const getDeps = () => - ({ - abortController: new AbortController(), - inspectorAdapters: { requests: new RequestAdapter() }, - searchSessionId: '123', - data: discoverServiceMock.data, - savedSearch: savedSearchMock, - } as FetchDeps); - -describe('test fetchTotalHits', () => { - test('resolves returned promise with hit count', async () => { - savedSearchMock.searchSource.fetch$ = () => - of({ rawResponse: { hits: { total: 45 } } } as IKibanaSearchResponse>); - - await expect(fetchTotalHits(savedSearchMock.searchSource, getDeps())).resolves.toBe(45); - }); - - test('rejects in case of an error', async () => { - savedSearchMock.searchSource.fetch$ = () => throwErrorRx(() => new Error('Oh noes!')); - - await expect(fetchTotalHits(savedSearchMock.searchSource, getDeps())).rejects.toEqual( - new Error('Oh noes!') - ); - }); - test('fetch$ is called with execution context containing savedSearch id', async () => { - const fetch$Mock = jest - .fn() - .mockReturnValue( - of({ rawResponse: { hits: { total: 45 } } } as IKibanaSearchResponse) - ); - - savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; - - await fetchTotalHits(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` - Object { - "description": "fetch total hits", - } - `); - }); -}); diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts deleted file mode 100644 index 16bd138e2caf5..0000000000000 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { filter, map } from 'rxjs/operators'; -import { lastValueFrom } from 'rxjs'; -import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; -import { DataViewType } from '@kbn/data-views-plugin/public'; -import { FetchDeps } from './fetch_all'; - -export function fetchTotalHits( - searchSource: ISearchSource, - { abortController, inspectorAdapters, searchSessionId, savedSearch }: FetchDeps -) { - searchSource.setField('trackTotalHits', true); - searchSource.setField('size', 0); - searchSource.removeField('sort'); - searchSource.removeField('fields'); - searchSource.removeField('aggs'); - if (searchSource.getField('index')?.type === DataViewType.ROLLUP) { - // We treat that data view as "normal" even if it was a rollup data view, - // since the rollup endpoint does not support querying individual documents, but we - // can get them from the regular _search API that will be used if the data view - // not a rollup data view. - searchSource.setOverwriteDataViewType(undefined); - } - - const executionContext = { - description: 'fetch total hits', - }; - - const fetch$ = searchSource - .fetch$({ - inspector: { - adapter: inspectorAdapters.requests, - title: i18n.translate('discover.inspectorRequestDataTitleTotalHits', { - defaultMessage: 'Total hits', - }), - description: i18n.translate('discover.inspectorRequestDescriptionTotalHits', { - defaultMessage: 'This request queries Elasticsearch to fetch the total hits.', - }), - }, - abortSignal: abortController.signal, - sessionId: searchSessionId, - executionContext, - }) - .pipe( - filter((res) => isCompleteResponse(res)), - map((res) => res.rawResponse.hits.total as number) - ); - - return lastValueFrom(fetch$); -} diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index e960a94031cbc..50b856829b182 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -7,7 +7,7 @@ */ import type { ReactElement } from 'react'; -import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; +import React, { memo } from 'react'; import { EuiButtonIcon, EuiContextMenu, @@ -15,11 +15,8 @@ import { EuiFlexItem, EuiPopover, EuiToolTip, - useEuiBreakpoint, - useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/react'; import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; import { HitsCounter } from '../hits_counter'; import { Histogram } from './histogram'; @@ -27,15 +24,22 @@ import { useChartPanels } from './use_chart_panels'; import type { UnifiedHistogramBreakdownContext, UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; import { BreakdownFieldSelector } from './breakdown_field_selector'; +import { useTotalHits } from './use_total_hits'; +import { useRequestParams } from './use_request_params'; +import { useChartStyles } from './use_chart_styles'; +import { useChartActions } from './use_chart_actions'; export interface ChartProps { className?: string; services: UnifiedHistogramServices; dataView: DataView; + request?: UnifiedHistogramRequestContext; hits?: UnifiedHistogramHitsContext; chart?: UnifiedHistogramChartContext; breakdown?: UnifiedHistogramBreakdownContext; @@ -46,6 +50,7 @@ export interface ChartProps { onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; } const HistogramMemoized = memo(Histogram); @@ -54,6 +59,7 @@ export function Chart({ className, services, dataView, + request, hits, chart, breakdown, @@ -64,95 +70,58 @@ export function Chart({ onChartHiddenChange, onTimeIntervalChange, onBreakdownFieldChange, + onTotalHitsChange, }: ChartProps) { - const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); - - const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ - element: null, - moveFocus: false, + const { + showChartOptionsPopover, + chartRef, + toggleChartOptions, + closeChartOptions, + toggleHideChart, + } = useChartActions({ + chart, + onChartHiddenChange, }); - const onShowChartOptions = useCallback(() => { - setShowChartOptionsPopover(!showChartOptionsPopover); - }, [showChartOptionsPopover]); - - const closeChartOptions = useCallback(() => { - setShowChartOptionsPopover(false); - }, [setShowChartOptionsPopover]); - - useEffect(() => { - if (chartRef.current.moveFocus && chartRef.current.element) { - chartRef.current.element.focus(); - } - }, [chart?.hidden]); - - const toggleHideChart = useCallback(() => { - const chartHidden = !chart?.hidden; - chartRef.current.moveFocus = !chartHidden; - onChartHiddenChange?.(chartHidden); - }, [chart?.hidden, onChartHiddenChange]); - const panels = useChartPanels({ chart, toggleHideChart, - onTimeIntervalChange: (timeInterval) => onTimeIntervalChange?.(timeInterval), - closePopover: () => setShowChartOptionsPopover(false), + onTimeIntervalChange, + closePopover: closeChartOptions, onResetChartHeight, }); - const [totalHits, setTotalHits] = useState(); - - const onTotalHitsChange = useCallback((newTotalHits: number) => { - setTotalHits(newTotalHits); - }, []); - - const chartVisible = + const chartVisible = !!( chart && !chart.hidden && dataView.id && dataView.type !== DataViewType.ROLLUP && - dataView.isTimeBased(); + dataView.isTimeBased() + ); - const { euiTheme } = useEuiTheme(); - const resultCountCss = css` - padding: ${euiTheme.size.s} ${euiTheme.size.s} ${chartVisible ? 0 : euiTheme.size.s} - ${euiTheme.size.s}; - min-height: ${euiTheme.base * 2.5}px; - `; - const resultCountTitleCss = css` - ${useEuiBreakpoint(['xs', 's'])} { - margin-bottom: 0 !important; - } - `; - const resultCountToggleCss = css` - ${useEuiBreakpoint(['xs', 's'])} { - align-items: flex-end; - } - `; - const timechartCss = css` - flex-grow: 1; - display: flex; - flex-direction: column; - position: relative; + const { filters, query, timeRange } = useRequestParams(services); + + useTotalHits({ + services, + request, + chartVisible, + hits, + dataView, + filters, + query, + timeRange, + onTotalHitsChange, + }); - // SASSTODO: the visualizing component should have an option or a modifier - .series > rect { - fill-opacity: 0.5; - stroke-width: 1; - } - `; - const breakdownFieldSelectorGroupCss = css` - width: 100%; - `; - const breakdownFieldSelectorItemCss = css` - align-items: flex-end; - padding-left: ${euiTheme.size.s}; - `; - const chartToolButtonCss = css` - display: flex; - justify-content: center; - padding-left: ${euiTheme.size.s}; - `; + const { + resultCountCss, + resultCountTitleCss, + resultCountToggleCss, + histogramCss, + breakdownFieldSelectorGroupCss, + breakdownFieldSelectorItemCss, + chartToolButtonCss, + } = useChartStyles(chartVisible); return ( - {hits && } + {hits && }
{chart && ( @@ -220,7 +189,7 @@ export function Chart({ diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 283c1e982083d..e5981574a228b 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -8,17 +8,20 @@ import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; -import { connectToQueryState, IKibanaSearchResponse, QueryState } from '@kbn/data-plugin/public'; +import type { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import type { estypes } from '@elastic/elasticsearch'; -import { createStateContainer, useContainerState } from '@kbn/kibana-utils-plugin/public'; +import type { AggregateQuery, Query, Filter, TimeRange } from '@kbn/es-query'; import type { UnifiedHistogramBreakdownContext, UnifiedHistogramBucketInterval, UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; import { getLensAttributes } from './get_lens_attributes'; @@ -28,48 +31,32 @@ import { useTimeRange } from './use_time_range'; export interface HistogramProps { services: UnifiedHistogramServices; dataView: DataView; + request?: UnifiedHistogramRequestContext; + hits?: UnifiedHistogramHitsContext; chart: UnifiedHistogramChartContext; breakdown?: UnifiedHistogramBreakdownContext; - onTotalHitsChange: (totalHits: number) => void; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; } export function Histogram({ services: { data, lens, uiSettings }, dataView, + request, + hits, chart: { timeInterval }, breakdown: { field: breakdownField } = {}, + filters, + query, + timeRange, onTotalHitsChange, }: HistogramProps) { - const queryStateContainer = useMemo(() => { - return createStateContainer({ - filters: data.query.filterManager.getFilters(), - query: data.query.queryString.getQuery(), - refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), - time: data.query.timefilter.timefilter.getTime(), - }); - }, [data.query.filterManager, data.query.queryString, data.query.timefilter.timefilter]); - - const queryState = useContainerState(queryStateContainer); - - useEffect(() => { - return connectToQueryState(data.query, queryStateContainer, { - time: true, - query: true, - filters: true, - refreshInterval: true, - }); - }, [data.query, queryStateContainer]); - - const filters = useMemo(() => queryState.filters ?? [], [queryState.filters]); - const query = useMemo( - () => queryState.query ?? data.query.queryString.getDefaultQuery(), - [data.query.queryString, queryState.query] - ); const attributes = useMemo( () => getLensAttributes({ filters, query, dataView, timeInterval, breakdownField }), [breakdownField, dataView, filters, query, timeInterval] ); - const timeRange = data.query.timefilter.timefilter.getAbsoluteTime(); const [bucketInterval, setBucketInterval] = useState(); const { timeRangeText, timeRangeDisplay } = useTimeRange({ uiSettings, @@ -79,15 +66,13 @@ export function Histogram({ }); const onLoad = useCallback( - (_, adapters: Partial | undefined) => { + (isLoading, adapters: Partial | undefined) => { const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount; - if (totalHits) { - onTotalHitsChange(totalHits); - } + onTotalHitsChange?.(isLoading ? 'loading' : 'complete', totalHits ?? hits?.total); - const request = adapters?.requests?.getRequests()[0]; - const json = request?.response?.json as IKibanaSearchResponse; + const lensRequest = adapters?.requests?.getRequests()[0]; + const json = lensRequest?.response?.json as IKibanaSearchResponse; const response = json?.rawResponse; if (response) { @@ -101,7 +86,7 @@ export function Histogram({ setBucketInterval(newBucketInterval); } }, - [data, dataView, onTotalHitsChange, timeInterval] + [data, dataView, hits?.total, onTotalHitsChange, timeInterval] ); const { euiTheme } = useEuiTheme(); @@ -134,6 +119,11 @@ export function Histogram({ timeRange={timeRange} attributes={attributes} noPadding + searchSessionId={request?.searchSessionId} + executionContext={{ + description: 'fetch chart data and total hits', + }} + lastReloadRequestTime={request?.lastReloadRequestTime} onLoad={onLoad} /> diff --git a/src/plugins/unified_histogram/public/chart/use_chart_actions.ts b/src/plugins/unified_histogram/public/chart/use_chart_actions.ts new file mode 100644 index 0000000000000..85b876e0862c1 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_chart_actions.ts @@ -0,0 +1,53 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { UnifiedHistogramChartContext } from '../types'; + +export const useChartActions = ({ + chart, + onChartHiddenChange, +}: { + chart: UnifiedHistogramChartContext | undefined; + onChartHiddenChange?: (chartHidden: boolean) => void; +}) => { + const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false); + + const toggleChartOptions = useCallback(() => { + setShowChartOptionsPopover(!showChartOptionsPopover); + }, [showChartOptionsPopover]); + + const closeChartOptions = useCallback(() => { + setShowChartOptionsPopover(false); + }, [setShowChartOptionsPopover]); + + const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({ + element: null, + moveFocus: false, + }); + + useEffect(() => { + if (chartRef.current.moveFocus && chartRef.current.element) { + chartRef.current.element.focus(); + } + }, [chart?.hidden]); + + const toggleHideChart = useCallback(() => { + const chartHidden = !chart?.hidden; + chartRef.current.moveFocus = !chartHidden; + onChartHiddenChange?.(chartHidden); + }, [chart?.hidden, onChartHiddenChange]); + + return { + showChartOptionsPopover, + chartRef, + toggleChartOptions, + closeChartOptions, + toggleHideChart, + }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_chart_panels.ts b/src/plugins/unified_histogram/public/chart/use_chart_panels.ts index dd6f162b352f6..8f2874baa624e 100644 --- a/src/plugins/unified_histogram/public/chart/use_chart_panels.ts +++ b/src/plugins/unified_histogram/public/chart/use_chart_panels.ts @@ -23,7 +23,7 @@ export function useChartPanels({ }: { chart?: UnifiedHistogramChartContext; toggleHideChart: () => void; - onTimeIntervalChange: (timeInterval: string) => void; + onTimeIntervalChange?: (timeInterval: string) => void; closePopover: () => void; onResetChartHeight?: () => void; }) { @@ -107,7 +107,7 @@ export function useChartPanels({ label: display, icon: val === chart.timeInterval ? 'check' : 'empty', onClick: () => { - onTimeIntervalChange(val); + onTimeIntervalChange?.(val); closePopover(); }, 'data-test-subj': `unifiedHistogramTimeInterval-${display}`, diff --git a/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx b/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx new file mode 100644 index 0000000000000..dc551626bc791 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_chart_styles.tsx @@ -0,0 +1,63 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { useEuiBreakpoint, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const useChartStyles = (chartVisible: boolean) => { + const { euiTheme } = useEuiTheme(); + const resultCountCss = css` + padding: ${euiTheme.size.s} ${euiTheme.size.s} ${chartVisible ? 0 : euiTheme.size.s} + ${euiTheme.size.s}; + min-height: ${euiTheme.base * 2.5}px; + `; + const resultCountTitleCss = css` + ${useEuiBreakpoint(['xs', 's'])} { + margin-bottom: 0 !important; + } + `; + const resultCountToggleCss = css` + ${useEuiBreakpoint(['xs', 's'])} { + align-items: flex-end; + } + `; + const histogramCss = css` + flex-grow: 1; + display: flex; + flex-direction: column; + position: relative; + + // SASSTODO: the visualizing component should have an option or a modifier + .series > rect { + fill-opacity: 0.5; + stroke-width: 1; + } + `; + const breakdownFieldSelectorGroupCss = css` + width: 100%; + `; + const breakdownFieldSelectorItemCss = css` + align-items: flex-end; + padding-left: ${euiTheme.size.s}; + `; + const chartToolButtonCss = css` + display: flex; + justify-content: center; + padding-left: ${euiTheme.size.s}; + `; + + return { + resultCountCss, + resultCountTitleCss, + resultCountToggleCss, + histogramCss, + breakdownFieldSelectorGroupCss, + breakdownFieldSelectorItemCss, + chartToolButtonCss, + }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_request_params.tsx b/src/plugins/unified_histogram/public/chart/use_request_params.tsx new file mode 100644 index 0000000000000..e132e0532280f --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_request_params.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import { connectToQueryState, QueryState } from '@kbn/data-plugin/public'; +import { createStateContainer, useContainerState } from '@kbn/kibana-utils-plugin/public'; +import { useEffect, useMemo } from 'react'; +import type { UnifiedHistogramServices } from '../types'; + +export const useRequestParams = (services: UnifiedHistogramServices) => { + const { data } = services; + + const queryStateContainer = useMemo(() => { + return createStateContainer({ + filters: data.query.filterManager.getFilters(), + query: data.query.queryString.getQuery(), + refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), + time: data.query.timefilter.timefilter.getTime(), + }); + }, [data.query.filterManager, data.query.queryString, data.query.timefilter.timefilter]); + + const queryState = useContainerState(queryStateContainer); + + useEffect(() => { + return connectToQueryState(data.query, queryStateContainer, { + time: true, + query: true, + filters: true, + refreshInterval: true, + }); + }, [data.query, queryStateContainer]); + + const filters = useMemo(() => queryState.filters ?? [], [queryState.filters]); + + const query = useMemo( + () => queryState.query ?? data.query.queryString.getDefaultQuery(), + [data.query.queryString, queryState.query] + ); + + const timeRange = useMemo( + () => data.query.timefilter.timefilter.getAbsoluteTime(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [data.query.timefilter.timefilter, queryState.time] + ); + + return { filters, query, timeRange }; +}; diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.ts new file mode 100644 index 0000000000000..5ec6f4768d9a5 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.ts @@ -0,0 +1,207 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { isCompleteResponse } from '@kbn/data-plugin/public'; +import { DataView, DataViewType } from '@kbn/data-views-plugin/public'; +import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { cloneDeep, isEqual } from 'lodash'; +import { MutableRefObject, useEffect, useRef } from 'react'; +import { filter, lastValueFrom, map } from 'rxjs'; +import type { + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, + UnifiedHistogramServices, +} from '../types'; + +export const useTotalHits = ({ + services, + request, + chartVisible, + hits, + dataView, + filters, + query, + timeRange, + onTotalHitsChange, +}: { + services: UnifiedHistogramServices; + request: UnifiedHistogramRequestContext | undefined; + chartVisible: boolean; + hits: UnifiedHistogramHitsContext | undefined; + dataView: DataView; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; +}) => { + const abortController = useRef(); + const totalHitsDeps = useRef>(); + + useEffect(() => { + const newTotalHitsDeps = getTotalHitsDeps({ + chartVisible, + request, + hits, + dataView, + filters, + query, + timeRange, + }); + + if (!isEqual(totalHitsDeps.current, newTotalHitsDeps)) { + totalHitsDeps.current = newTotalHitsDeps; + + fetchTotalHits({ + services, + abortController, + request, + chartVisible, + hits, + dataView, + filters, + query, + timeRange, + onTotalHitsChange, + }); + } + }, [ + chartVisible, + dataView, + filters, + hits, + onTotalHitsChange, + query, + request, + services, + timeRange, + ]); +}; + +const getTotalHitsDeps = ({ + chartVisible, + request, + hits, + dataView, + filters, + query, + timeRange, +}: { + chartVisible: boolean; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + dataView: DataView; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; +}) => + cloneDeep([ + chartVisible, + Boolean(hits), + dataView.id, + filters, + query, + timeRange, + request?.lastReloadRequestTime, + ]); + +const fetchTotalHits = async ({ + services: { data }, + abortController, + request, + chartVisible, + hits, + dataView, + filters: originalFilters, + query, + timeRange, + onTotalHitsChange, +}: { + services: UnifiedHistogramServices; + abortController: MutableRefObject; + request: UnifiedHistogramRequestContext | undefined; + chartVisible: boolean; + hits: UnifiedHistogramHitsContext | undefined; + dataView: DataView; + filters: Filter[]; + query: Query | AggregateQuery; + timeRange: TimeRange; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; +}) => { + abortController.current?.abort(); + abortController.current = undefined; + + // Either the chart is visible, in which case Lens will make the request, + // or there is no hits context, which means the total hits should be hidden + if (chartVisible || !hits) { + return; + } + + onTotalHitsChange?.('loading', hits.total); + + const searchSource = data.search.searchSource.createEmpty(); + + searchSource + .setField('index', dataView) + .setField('query', query) + .setField('size', 0) + .setField('trackTotalHits', true); + + let filters = originalFilters; + + if (dataView.type === DataViewType.ROLLUP) { + // We treat that data view as "normal" even if it was a rollup data view, + // since the rollup endpoint does not support querying individual documents, but we + // can get them from the regular _search API that will be used if the data view + // not a rollup data view. + searchSource.setOverwriteDataViewType(undefined); + } else { + // Set the date range filter fields from timeFilter using the absolute format. + // Search sessions requires that it be converted from a relative range + const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + + if (timeFilter) { + filters = [...filters, timeFilter]; + } + } + + searchSource.setField('filter', filters); + + abortController.current = new AbortController(); + + const inspector = request?.adapter + ? { + adapter: request.adapter, + title: i18n.translate('unifiedHistogram.inspectorRequestDataTitleTotalHits', { + defaultMessage: 'Total hits', + }), + description: i18n.translate('unifiedHistogram.inspectorRequestDescriptionTotalHits', { + defaultMessage: 'This request queries Elasticsearch to fetch the total hits.', + }), + } + : undefined; + + const fetch$ = searchSource + .fetch$({ + inspector, + sessionId: request?.searchSessionId, + abortSignal: abortController.current.signal, + executionContext: { + description: 'fetch total hits', + }, + }) + .pipe( + filter((res) => isCompleteResponse(res)), + map((res) => res.rawResponse.hits.total as number) + ); + + const totalHits = await lastValueFrom(fetch$); + + onTotalHitsChange?.('complete', totalHits); +}; diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx index e6296429576a8..39df40650557c 100644 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx @@ -16,24 +16,21 @@ import type { UnifiedHistogramHitsContext } from '../types'; export interface HitsCounterProps { hits: UnifiedHistogramHitsContext; - totalHits?: number; append?: ReactElement; } -export function HitsCounter({ hits, totalHits, append }: HitsCounterProps) { - if (!hits.total && hits.status === 'loading' && !totalHits) { +export function HitsCounter({ hits, append }: HitsCounterProps) { + if (!hits.total && hits.status === 'loading') { return null; } const formattedHits = ( - + ); @@ -51,23 +48,23 @@ export function HitsCounter({ hits, totalHits, append }: HitsCounterProps) { > - {hits.status === 'partial' && !totalHits && ( + {hits.status === 'partial' && ( )} - {(hits.status !== 'partial' || totalHits) && ( + {hits.status !== 'partial' && ( )} - {hits.status === 'partial' && !totalHits && ( + {hits.status === 'partial' && ( { className?: string; services: UnifiedHistogramServices; dataView: DataView; + /** + * Context object for requests made by unified histogram components -- optional + */ + request?: UnifiedHistogramRequestContext; /** * Context object for the hits count -- leave undefined to hide the hits count */ @@ -69,12 +75,18 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * Callback to update the breakdown field -- should set {@link UnifiedHistogramBreakdownContext.field} to breakdownField */ onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; + /** + * Callback to update the total hits -- should set {@link UnifiedHistogramHitsContext.status} to status + * and {@link UnifiedHistogramHitsContext.total} to totalHits + */ + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; } export const UnifiedHistogramLayout = ({ className, services, dataView, + request, hits, chart, breakdown, @@ -86,6 +98,7 @@ export const UnifiedHistogramLayout = ({ onChartHiddenChange, onTimeIntervalChange, onBreakdownFieldChange, + onTotalHitsChange, children, }: UnifiedHistogramLayoutProps) => { const topPanelNode = useMemo( @@ -134,6 +147,7 @@ export const UnifiedHistogramLayout = ({ className={chartClassName} services={services} dataView={dataView} + request={request} hits={hits} chart={chart} breakdown={breakdown} @@ -144,6 +158,7 @@ export const UnifiedHistogramLayout = ({ onChartHiddenChange={onChartHiddenChange} onTimeIntervalChange={onTimeIntervalChange} onBreakdownFieldChange={onBreakdownFieldChange} + onTotalHitsChange={onTotalHitsChange} /> {children} diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index 275ed0de96cf3..1c213f8bd8e49 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -12,6 +12,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { DataViewField } from '@kbn/data-views-plugin/public'; +import type { RequestAdapter } from '@kbn/inspector-plugin/public'; /** * The fetch status of a unified histogram request @@ -44,6 +45,24 @@ export interface UnifiedHistogramBucketInterval { scale?: number; } +/** + * Context object for requests made by unified histogram components + */ +export interface UnifiedHistogramRequestContext { + /** + * Current search session ID + */ + searchSessionId?: string; + /** + * The adapter to use for requests + */ + adapter?: RequestAdapter; + /** + * Can be updated to `Date.now()` to force a refresh + */ + lastReloadRequestTime?: number; +} + /** * Context object for the hits count */ diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index faacde782d509..57fc3ad319fe7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2221,10 +2221,8 @@ "discover.helpMenu.appName": "Découverte", "discover.inspectorRequestDataTitleChart": "Données du graphique", "discover.inspectorRequestDataTitleDocuments": "Documents", - "discover.inspectorRequestDataTitleTotalHits": "Nombre total de résultats", "discover.inspectorRequestDescriptionChart": "Cette requête interroge Elasticsearch afin de récupérer les données d'agrégation pour le graphique.", "discover.inspectorRequestDescriptionDocument": "Cette requête interroge Elasticsearch afin de récupérer les documents.", - "discover.inspectorRequestDescriptionTotalHits": "Cette requête interroge Elasticsearch afin de récupérer le nombre total de résultats.", "discover.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", "discover.json.copyToClipboardLabel": "Copier dans le presse-papiers", "discover.loadingDocuments": "Chargement des documents", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 59e637a55f71c..3e0425fcb222b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2217,10 +2217,8 @@ "discover.helpMenu.appName": "Discover", "discover.inspectorRequestDataTitleChart": "グラフデータ", "discover.inspectorRequestDataTitleDocuments": "ドキュメント", - "discover.inspectorRequestDataTitleTotalHits": "総ヒット数", "discover.inspectorRequestDescriptionChart": "このリクエストはElasticsearchにクエリをかけ、グラフの集計データを取得します。", "discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、ドキュメントを取得します。", - "discover.inspectorRequestDescriptionTotalHits": "このリクエストはElasticsearchにクエリをかけ、合計一致数を取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.json.copyToClipboardLabel": "クリップボードにコピー", "discover.loadingDocuments": "ドキュメントを読み込み中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 921639d7cdab4..4ca62119353f0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2221,10 +2221,8 @@ "discover.helpMenu.appName": "Discover", "discover.inspectorRequestDataTitleChart": "图表数据", "discover.inspectorRequestDataTitleDocuments": "文档", - "discover.inspectorRequestDataTitleTotalHits": "总命中数", "discover.inspectorRequestDescriptionChart": "此请求将查询 Elasticsearch 以获取图表的聚合数据。", "discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取文档。", - "discover.inspectorRequestDescriptionTotalHits": "此请求将查询 Elasticsearch 以获取总命中数。", "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "discover.json.copyToClipboardLabel": "复制到剪贴板", "discover.loadingDocuments": "正在加载文档", From 017a7cb590d62258010176ffd713a32af578f531 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 2 Nov 2022 14:45:21 -0300 Subject: [PATCH 14/84] [Discover] Moved breakdown field to URL app state instead of local storage --- .../components/layout/use_discover_histogram.ts | 17 ++++++++--------- .../application/main/services/discover_state.ts | 4 ++++ .../main/utils/get_state_defaults.ts | 1 + 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 0681251eb01eb..49c359fa50304 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -212,21 +212,20 @@ export const useDiscoverHistogram = ({ * Breakdown */ - const [field, setField] = useState(() => { - const fieldName = storage.get(HISTOGRAM_BREAKDOWN_FIELD_KEY); - return dataView.getFieldByName(fieldName); - }); - const onBreakdownFieldChange = useCallback( (breakdownField: DataViewField | undefined) => { - storage.set(HISTOGRAM_BREAKDOWN_FIELD_KEY, breakdownField?.name); - setField(breakdownField); + stateContainer.setAppState({ breakdownField: breakdownField?.name }); }, - [storage] + [stateContainer] + ); + + const field = useMemo( + () => (state.breakdownField ? dataView.getFieldByName(state.breakdownField) : undefined), + [dataView, state.breakdownField] ); const breakdown = useMemo( - () => (isPlainRecord || !isTimeBased ? undefined : { field }), + () => (isPlainRecord || !isTimeBased || !field ? undefined : { field }), [field, isPlainRecord, isTimeBased] ); diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 611b59eacdc79..23042a4b955f6 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -98,6 +98,10 @@ export interface AppState { * Number of rows in the grid per page */ rowsPerPage?: number; + /** + * Current histogram breakdown field name + */ + breakdownField?: string; } export interface AppStateUrl extends Omit { diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts index b8b3c3579f343..208bf97fc376e 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts @@ -70,6 +70,7 @@ export function getStateDefaults({ rowHeight: undefined, rowsPerPage: undefined, grid: undefined, + breakdownField: undefined, }; if (savedSearch.grid) { defaultState.grid = savedSearch.grid; From f38547eb80dd36b575b812c38f6d2fb6fe31553a Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 2 Nov 2022 22:39:57 -0300 Subject: [PATCH 15/84] [Discover] Updating histogram labels and settings --- .../public/chart/breakdown_field_selector.tsx | 5 +++- .../public/chart/get_lens_attributes.ts | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index 1369ffce513d4..6a5a953d20781 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -9,6 +9,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; import { UnifiedHistogramBreakdownContext } from '../types'; @@ -47,7 +48,9 @@ export const BreakdownFieldSelector = ({ return ( Date: Thu, 3 Nov 2022 18:40:09 +0300 Subject: [PATCH 16/84] [Discover] fix functionals --- .../main/components/layout/discover_main_content.tsx | 8 ++++---- test/functional/apps/discover/group1/_discover.ts | 2 +- x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx | 2 +- .../lens/public/datasources/form_based/field_item.tsx | 2 +- x-pack/plugins/lens/public/plugin.ts | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 70ca210e87c81..d36623dc58515 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -52,6 +52,10 @@ export interface DiscoverMainContentProps { searchSessionManager: DiscoverSearchSessionManager; } +const resetSearchButtonWrapper = css` + overflow: hidden; +`; + export const DiscoverMainContent = ({ isPlainRecord, dataView, @@ -115,10 +119,6 @@ export const DiscoverMainContent = ({ searchSessionManager, }); - const resetSearchButtonWrapper = css` - overflow: hidden; - `; - return ( { if (!activeDatasourceId || !discoverLocator) { diff --git a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx index 36727780944ac..35bd95dd7c9f1 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx @@ -309,7 +309,7 @@ function FieldItemPopoverContents( [indexPattern], getEsQueryConfig(services.uiSettings) ); - const discoverLocator = services.share?.url.locators.get('discover'); + const discoverLocator = services.share?.url.locators.get('DISCOVER_APP_LOCATOR'); if (!discoverLocator || !services.application.capabilities.discover.show) { return; } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b178e9e776000..8a12a58f16087 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -317,7 +317,7 @@ export class LensPlugin { visualizations.registerAlias(getLensAliasConfig()); - const discoverLocator = share?.url.locators.get('discover'); + const discoverLocator = share?.url.locators.get('DISCOVER_APP_LOCATOR'); if (discoverLocator) { uiActionsEnhanced.registerDrilldown( @@ -499,7 +499,7 @@ export class LensPlugin { visualizeAggBasedVisAction(core.application) ); - const discoverLocator = startDependencies.share?.url.locators.get('discover'); + const discoverLocator = startDependencies.share?.url.locators.get('DISCOVER_APP_LOCATOR'); if (discoverLocator) { startDependencies.uiActions.addTriggerAction( From 4c68c1bffb6d263df6d8a3d2403afe632032d4f0 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 3 Nov 2022 17:56:32 -0300 Subject: [PATCH 17/84] [Discover] Add support for inspecting Lens requests in Discover --- .../layout/discover_main_content.tsx | 6 +- .../main/components/layout/types.ts | 4 +- .../layout/use_discover_histogram.ts | 14 +- .../application/main/hooks/use_inspector.ts | 29 +++- .../main/hooks/use_saved_search.ts | 3 +- .../main/utils/aggregate_request_adapter.ts | 132 ++++++++++++++++++ .../unified_histogram/public/chart/chart.tsx | 4 + .../public/chart/histogram.tsx | 9 +- src/plugins/unified_histogram/public/index.ts | 2 + .../public/layout/layout.tsx | 7 + src/plugins/unified_histogram/public/types.ts | 17 +++ 11 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 src/plugins/discover/public/application/main/utils/aggregate_request_adapter.ts diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index d36623dc58515..92d4a5aa1eead 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -15,7 +15,6 @@ import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; -import type { RequestAdapter } from '@kbn/inspector-plugin/public'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DataTableRecord } from '../../../../types'; import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; @@ -27,6 +26,7 @@ import { DiscoverDocuments } from './discover_documents'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; import { useDiscoverHistogram } from './use_discover_histogram'; import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; +import type { InspectorAdapters } from '../../hooks/use_inspector'; const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); @@ -48,7 +48,7 @@ export interface DiscoverMainContentProps { onFieldEdited: () => Promise; columns: string[]; resizeRef: RefObject; - inspectorAdapters: { requests: RequestAdapter }; + inspectorAdapters: InspectorAdapters; searchSessionManager: DiscoverSearchSessionManager; } @@ -107,6 +107,7 @@ export const DiscoverMainContent = ({ onTimeIntervalChange, onBreakdownFieldChange, onTotalHitsChange, + onChartLoad, } = useDiscoverHistogram({ stateContainer, state, @@ -156,6 +157,7 @@ export const DiscoverMainContent = ({ onTimeIntervalChange={onTimeIntervalChange} onBreakdownFieldChange={onBreakdownFieldChange} onTotalHitsChange={onTotalHitsChange} + onChartLoad={onChartLoad} > void; onChangeDataView: (id: string) => void; onUpdateQuery: ( diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 49c359fa50304..55871fb94ea6b 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -14,8 +14,8 @@ import { } from '@kbn/unified-field-list-plugin/public'; import { useCallback, useEffect, useMemo, useState } from 'react'; import type { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; -import type { RequestAdapter } from '@kbn/inspector-plugin/public'; import useDebounce from 'react-use/lib/useDebounce'; +import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; import { getUiActions } from '../../../../kibana_services'; import { PLUGIN_ID } from '../../../../../common'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; @@ -24,6 +24,7 @@ import type { SavedSearchData } from '../../hooks/use_saved_search'; import type { AppState, GetStateReturn } from '../../services/discover_state'; import { FetchStatus } from '../../../types'; import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; +import type { InspectorAdapters } from '../../hooks/use_inspector'; export const CHART_HIDDEN_KEY = 'discover:chartHidden'; export const HISTOGRAM_HEIGHT_KEY = 'discover:histogramHeight'; @@ -47,7 +48,7 @@ export const useDiscoverHistogram = ({ savedSearch: SavedSearch; isTimeBased: boolean; isPlainRecord: boolean; - inspectorAdapters: { requests: RequestAdapter }; + inspectorAdapters: InspectorAdapters; searchSessionManager: DiscoverSearchSessionManager; }) => { const { storage } = useDiscoverServices(); @@ -197,6 +198,14 @@ export const useDiscoverHistogram = ({ * Chart */ + const onChartLoad = useCallback( + (event: UnifiedHistogramChartLoadEvent) => { + // We need to store the Lens request adapter in order to inspect its requests + inspectorAdapters.lensRequests = event.adapters.requests; + }, + [inspectorAdapters] + ); + const chart = useMemo( () => isPlainRecord || !isTimeBased @@ -241,5 +250,6 @@ export const useDiscoverHistogram = ({ onTimeIntervalChange, onBreakdownFieldChange, onTotalHitsChange, + onChartLoad, }; }; diff --git a/src/plugins/discover/public/application/main/hooks/use_inspector.ts b/src/plugins/discover/public/application/main/hooks/use_inspector.ts index c7bcc0ba1cb4b..e23ca6425aa71 100644 --- a/src/plugins/discover/public/application/main/hooks/use_inspector.ts +++ b/src/plugins/discover/public/application/main/hooks/use_inspector.ts @@ -14,6 +14,12 @@ import { } from '@kbn/inspector-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { DataTableRecord } from '../../../types'; +import { AggregateRequestAdapter } from '../utils/aggregate_request_adapter'; + +export interface InspectorAdapters { + requests: RequestAdapter; + lensRequests?: RequestAdapter; +} export function useInspector({ setExpandedDoc, @@ -21,7 +27,7 @@ export function useInspector({ inspectorAdapters, savedSearch, }: { - inspectorAdapters: { requests: RequestAdapter }; + inspectorAdapters: InspectorAdapters; savedSearch: SavedSearch; setExpandedDoc: (doc?: DataTableRecord) => void; inspector: InspectorPublicPluginStart; @@ -31,11 +37,24 @@ export function useInspector({ const onOpenInspector = useCallback(() => { // prevent overlapping setExpandedDoc(undefined); - const session = inspector.open(inspectorAdapters, { - title: savedSearch.title, - }); + + const requestAdapters = inspectorAdapters.lensRequests + ? [inspectorAdapters.requests, inspectorAdapters.lensRequests] + : [inspectorAdapters.requests]; + + const session = inspector.open( + { requests: new AggregateRequestAdapter(requestAdapters) }, + { title: savedSearch.title } + ); + setInspectorSession(session); - }, [setExpandedDoc, inspectorAdapters, savedSearch, inspector]); + }, [ + setExpandedDoc, + inspectorAdapters.lensRequests, + inspectorAdapters.requests, + inspector, + savedSearch.title, + ]); useEffect(() => { return () => { diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 8c47ba95ec892..4f48945daaad1 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -25,6 +25,7 @@ import { useBehaviorSubject } from './use_behavior_subject'; import { sendResetMsg } from './use_saved_search_messages'; import { getFetch$ } from '../utils/get_fetch_observable'; import type { DataTableRecord } from '../../../types'; +import type { InspectorAdapters } from './use_inspector'; export interface SavedSearchData { main$: DataMain$; @@ -44,7 +45,7 @@ export interface UseSavedSearch { refetch$: DataRefetch$; data$: SavedSearchData; reset: () => void; - inspectorAdapters: { requests: RequestAdapter }; + inspectorAdapters: InspectorAdapters; } export enum RecordRawType { diff --git a/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.ts b/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.ts new file mode 100644 index 0000000000000..4ce55c0723a11 --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.ts @@ -0,0 +1,132 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { RequestAdapter, Request } from '@kbn/inspector-plugin/public'; + +/** + * A request adapter that aggregates multiple separate adapters into one to allow inspection + */ +export class AggregateRequestAdapter extends RequestAdapter { + private readonly adapters: RequestAdapter[]; + + constructor(adapters: RequestAdapter[]) { + super(); + this.adapters = adapters; + } + + public reset(...args: Parameters): void { + super.reset(...args); + this.adapters.forEach((adapter) => adapter.reset(...args)); + } + + public resetRequest(...args: Parameters): void { + super.resetRequest(...args); + this.adapters.forEach((adapter) => adapter.resetRequest(...args)); + } + + public getRequests(...args: Parameters): Request[] { + return [ + ...super.getRequests(), + ...this.adapters.map((adapter) => adapter.getRequests(...args)).flat(), + ]; + } + + public addListener(...args: Parameters): this { + super.addListener(...args); + this.adapters.forEach((adapter) => adapter.addListener(...args)); + return this; + } + + public on(...args: Parameters): this { + super.on(...args); + this.adapters.forEach((adapter) => adapter.on(...args)); + return this; + } + + public once(...args: Parameters): this { + super.once(...args); + this.adapters.forEach((adapter) => adapter.once(...args)); + return this; + } + + public removeListener(...args: Parameters): this { + super.removeListener(...args); + this.adapters.forEach((adapter) => adapter.removeListener(...args)); + return this; + } + + public off(...args: Parameters): this { + super.off(...args); + this.adapters.forEach((adapter) => adapter.off(...args)); + return this; + } + + public removeAllListeners(...args: Parameters): this { + super.removeAllListeners(...args); + this.adapters.forEach((adapter) => adapter.removeAllListeners(...args)); + return this; + } + + public setMaxListeners(...args: Parameters): this { + super.setMaxListeners(...args); + this.adapters.forEach((adapter) => adapter.setMaxListeners(...args)); + return this; + } + + public getMaxListeners(...args: Parameters): number { + return Math.min( + super.getMaxListeners(...args), + ...this.adapters.map((adapter) => adapter.getMaxListeners(...args)) + ); + } + + public listeners(...args: Parameters): Function[] { + return [ + ...super.listeners(...args), + ...this.adapters.map((adapter) => adapter.listeners(...args)).flat(), + ]; + } + + public rawListeners(...args: Parameters): Function[] { + return [ + ...super.rawListeners(...args), + ...this.adapters.map((adapter) => adapter.rawListeners(...args)).flat(), + ]; + } + + public emit(...args: Parameters): boolean { + return [super.emit(...args), ...this.adapters.map((adapter) => adapter.emit(...args))].every( + (result) => result + ); + } + + public listenerCount(...args: Parameters): number { + return this.adapters + .map((adapter) => adapter.listenerCount(...args)) + .reduce((a, b) => a + b, super.listenerCount(...args)); + } + + public prependListener(...args: Parameters): this { + super.prependListener(...args); + this.adapters.forEach((adapter) => adapter.prependListener(...args)); + return this; + } + + public prependOnceListener(...args: Parameters): this { + super.prependOnceListener(...args); + this.adapters.forEach((adapter) => adapter.prependOnceListener(...args)); + return this; + } + + public eventNames(...args: Parameters): Array { + return [ + ...super.eventNames(...args), + ...this.adapters.map((adapter) => adapter.eventNames(...args)).flat(), + ]; + } +} diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 50b856829b182..a4eb8c78e1a16 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -26,6 +26,7 @@ import type { UnifiedHistogramChartContext, UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, + UnifiedHistogramChartLoadEvent, UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; @@ -51,6 +52,7 @@ export interface ChartProps { onTimeIntervalChange?: (timeInterval: string) => void; onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; + onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } const HistogramMemoized = memo(Histogram); @@ -71,6 +73,7 @@ export function Chart({ onTimeIntervalChange, onBreakdownFieldChange, onTotalHitsChange, + onChartLoad, }: ChartProps) { const { showChartOptionsPopover, @@ -231,6 +234,7 @@ export function Chart({ query={query} timeRange={timeRange} onTotalHitsChange={onTotalHitsChange} + onChartLoad={onChartLoad} /> {appendHistogram} diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index e5981574a228b..33df5c0611c94 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -21,6 +21,7 @@ import type { UnifiedHistogramChartContext, UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, + UnifiedHistogramChartLoadEvent, UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; @@ -39,6 +40,7 @@ export interface HistogramProps { query: Query | AggregateQuery; timeRange: TimeRange; onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; + onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } export function Histogram({ @@ -52,6 +54,7 @@ export function Histogram({ query, timeRange, onTotalHitsChange, + onChartLoad, }: HistogramProps) { const attributes = useMemo( () => getLensAttributes({ filters, query, dataView, timeInterval, breakdownField }), @@ -66,7 +69,7 @@ export function Histogram({ }); const onLoad = useCallback( - (isLoading, adapters: Partial | undefined) => { + (isLoading: boolean, adapters: Partial | undefined) => { const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount; onTotalHitsChange?.(isLoading ? 'loading' : 'complete', totalHits ?? hits?.total); @@ -85,8 +88,10 @@ export function Histogram({ setBucketInterval(newBucketInterval); } + + onChartLoad?.({ complete: !isLoading, adapters: adapters ?? {} }); }, - [data, dataView, hits?.total, onTotalHitsChange, timeInterval] + [data, dataView, hits?.total, onChartLoad, onTotalHitsChange, timeInterval] ); const { euiTheme } = useEuiTheme(); diff --git a/src/plugins/unified_histogram/public/index.ts b/src/plugins/unified_histogram/public/index.ts index 55363a445576c..eaf2c21451463 100644 --- a/src/plugins/unified_histogram/public/index.ts +++ b/src/plugins/unified_histogram/public/index.ts @@ -16,6 +16,8 @@ export type { UnifiedHistogramHitsContext, UnifiedHistogramChartContext, UnifiedHistogramBreakdownContext, + UnifiedHistogramChartLoadEvent, + UnifiedHistogramAdapters, } from './types'; export const plugin = () => new UnifiedHistogramPublicPlugin(); diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index bfa48bd9dd1b9..a6f6b3955e7d0 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -21,6 +21,7 @@ import type { UnifiedHistogramBreakdownContext, UnifiedHistogramFetchStatus, UnifiedHistogramRequestContext, + UnifiedHistogramChartLoadEvent, } from '../types'; export interface UnifiedHistogramLayoutProps extends PropsWithChildren { @@ -80,6 +81,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * and {@link UnifiedHistogramHitsContext.total} to totalHits */ onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; + /** + * Called when the histogram loading status changes + */ + onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } export const UnifiedHistogramLayout = ({ @@ -99,6 +104,7 @@ export const UnifiedHistogramLayout = ({ onTimeIntervalChange, onBreakdownFieldChange, onTotalHitsChange, + onChartLoad, children, }: UnifiedHistogramLayoutProps) => { const topPanelNode = useMemo( @@ -159,6 +165,7 @@ export const UnifiedHistogramLayout = ({ onTimeIntervalChange={onTimeIntervalChange} onBreakdownFieldChange={onBreakdownFieldChange} onTotalHitsChange={onTotalHitsChange} + onChartLoad={onChartLoad} /> {children} diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index 1c213f8bd8e49..01a1fe2de01af 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -13,6 +13,7 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { DataViewField } from '@kbn/data-views-plugin/public'; import type { RequestAdapter } from '@kbn/inspector-plugin/public'; +import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; /** * The fetch status of a unified histogram request @@ -45,6 +46,22 @@ export interface UnifiedHistogramBucketInterval { scale?: number; } +export type UnifiedHistogramAdapters = Partial; + +/** + * Emitted when the histogram loading status changes + */ +export interface UnifiedHistogramChartLoadEvent { + /** + * True if loading is complete + */ + complete: boolean; + /** + * Inspector adapters for the request + */ + adapters: UnifiedHistogramAdapters; +} + /** * Context object for requests made by unified histogram components */ From 1543ff62601899b4b229aa5df28cf2450f295795 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 3 Nov 2022 18:05:01 -0300 Subject: [PATCH 18/84] [Discover] Clear Lens request adapter on chart hidden --- .../layout/use_discover_histogram.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 55871fb94ea6b..353252b800c9c 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -109,14 +109,6 @@ export const useDiscoverHistogram = ({ * Other callbacks */ - const onChartHiddenChange = useCallback( - (chartHidden: boolean) => { - storage.set(CHART_HIDDEN_KEY, chartHidden); - stateContainer.setAppState({ hideChart: chartHidden }); - }, - [stateContainer, storage] - ); - const onTimeIntervalChange = useCallback( (newInterval: string) => { stateContainer.setAppState({ interval: newInterval }); @@ -198,6 +190,19 @@ export const useDiscoverHistogram = ({ * Chart */ + const onChartHiddenChange = useCallback( + (chartHidden: boolean) => { + // Clear the Lens request adapter when the chart is hidden + if (chartHidden) { + inspectorAdapters.lensRequests = undefined; + } + + storage.set(CHART_HIDDEN_KEY, chartHidden); + stateContainer.setAppState({ hideChart: chartHidden }); + }, + [inspectorAdapters, stateContainer, storage] + ); + const onChartLoad = useCallback( (event: UnifiedHistogramChartLoadEvent) => { // We need to store the Lens request adapter in order to inspect its requests From 8c5a9fbae8fc003ec2e0600ec72b4c60e3c34d25 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 3 Nov 2022 21:36:44 -0300 Subject: [PATCH 19/84] [Discover] Field unified histogram breakdown fields --- .../components/layout/use_discover_histogram.ts | 2 ++ .../public/chart/breakdown_field_selector.tsx | 12 ++++++++---- .../public/chart/field_supports_breakdown.ts | 14 ++++++++++++++ .../public/chart/get_lens_attributes.ts | 3 ++- 4 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 src/plugins/unified_histogram/public/chart/field_supports_breakdown.ts diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 353252b800c9c..8b98fa659b57d 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -158,6 +158,8 @@ export const useDiscoverHistogram = ({ (status: UnifiedHistogramFetchStatus, totalHits?: number) => { const { fetchStatus, recordRawType } = savedSearchData$.totalHits$.getValue(); + // If we have a partial result already, we don't + // want to update the total hits back to loading if (fetchStatus === 'partial' && status === 'loading') { return; } diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index 6a5a953d20781..cfb05ecfa785d 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -12,6 +12,7 @@ import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; import { UnifiedHistogramBreakdownContext } from '../types'; +import { fieldSupportsBreakdown } from './field_supports_breakdown'; export interface BreakdownFieldSelectorProps { dataView: DataView; @@ -25,15 +26,18 @@ export const BreakdownFieldSelector = ({ onBreakdownFieldChange, }: BreakdownFieldSelectorProps) => { const fieldOptions = dataView.fields - .filter((field) => field.aggregatable) - .map((field) => ({ label: field.name })); + .filter((field) => fieldSupportsBreakdown(field)) + .map((field) => ({ label: field.displayName, value: field.name })) + .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); - const selectedFields = breakdown?.field ? [{ label: breakdown.field.name }] : []; + const selectedFields = breakdown?.field + ? [{ label: breakdown.field.displayName, value: breakdown.field.name }] + : []; const onFieldChange = useCallback( (newOptions: EuiComboBoxOptionOption[]) => { const field = newOptions.length - ? dataView.fields.find((currentField) => currentField.name === newOptions[0].label) + ? dataView.fields.find((currentField) => currentField.name === newOptions[0].value) : undefined; onBreakdownFieldChange?.(field); diff --git a/src/plugins/unified_histogram/public/chart/field_supports_breakdown.ts b/src/plugins/unified_histogram/public/chart/field_supports_breakdown.ts new file mode 100644 index 0000000000000..302a5950fefcb --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/field_supports_breakdown.ts @@ -0,0 +1,14 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { DataViewField } from '@kbn/data-views-plugin/public'; + +const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']); + +export const fieldSupportsBreakdown = (field: DataViewField) => + supportedTypes.has(field.type) && field.aggregatable && !field.scripted; diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts index 10164b5392f7d..4a5d1fced183b 100644 --- a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -16,6 +16,7 @@ import type { TermsIndexPatternColumn, TypedLensByValueInput, } from '@kbn/lens-plugin/public'; +import { fieldSupportsBreakdown } from './field_supports_breakdown'; export const getLensAttributes = ({ filters, @@ -30,7 +31,7 @@ export const getLensAttributes = ({ timeInterval: string | undefined; breakdownField: DataViewField | undefined; }) => { - const showBreakdown = breakdownField?.aggregatable; + const showBreakdown = breakdownField && fieldSupportsBreakdown(breakdownField); let columnOrder = ['date_column', 'count_column']; From 8482445dafed1796cffdd74c3394f711ccdefc2b Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 3 Nov 2022 22:41:41 -0300 Subject: [PATCH 20/84] [Discover] Fix Lens services breaking from merge --- .../plugins/lens/public/datasources/form_based/form_based.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index 52be5d72108d5..ff526d81dbaa7 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -432,7 +432,7 @@ export function getFormBasedDatasource({ fieldFormats, charts, unifiedSearch, - discover, + share, }} > From f19c72a728dfa503db597cb5b15344258fa28f5f Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 3 Nov 2022 23:05:44 -0300 Subject: [PATCH 21/84] [Discover] Fix grid rendering when chart visibility changes --- .../main/components/layout/discover_main_content.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 92d4a5aa1eead..3ff0021b4fb73 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; -import React, { RefObject, useCallback } from 'react'; +import React, { RefObject, useCallback, useEffect, useState } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE } from '@kbn/analytics'; import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; @@ -120,6 +120,14 @@ export const DiscoverMainContent = ({ searchSessionManager, }); + // The documents grid doesn't rerender when the chart visbility changes + // which causes it to render blank space, so we need to force a rerender + const [documentsKey, setDocumentsKey] = useState(0); + + useEffect(() => { + setDocumentsKey((key) => key + 1); + }, [chart?.hidden]); + return ( Date: Thu, 3 Nov 2022 23:52:20 -0300 Subject: [PATCH 22/84] [Discover] Fix issue where time range in total hits request was not being updated on refresh --- .../unified_histogram/public/chart/chart.tsx | 2 +- .../public/chart/use_request_params.tsx | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index a4eb8c78e1a16..bf0a3f8ae8a4e 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -102,7 +102,7 @@ export function Chart({ dataView.isTimeBased() ); - const { filters, query, timeRange } = useRequestParams(services); + const { filters, query, timeRange } = useRequestParams({ services, request }); useTotalHits({ services, diff --git a/src/plugins/unified_histogram/public/chart/use_request_params.tsx b/src/plugins/unified_histogram/public/chart/use_request_params.tsx index e132e0532280f..01cb89ad6646a 100644 --- a/src/plugins/unified_histogram/public/chart/use_request_params.tsx +++ b/src/plugins/unified_histogram/public/chart/use_request_params.tsx @@ -9,9 +9,15 @@ import { connectToQueryState, QueryState } from '@kbn/data-plugin/public'; import { createStateContainer, useContainerState } from '@kbn/kibana-utils-plugin/public'; import { useEffect, useMemo } from 'react'; -import type { UnifiedHistogramServices } from '../types'; - -export const useRequestParams = (services: UnifiedHistogramServices) => { +import type { UnifiedHistogramRequestContext, UnifiedHistogramServices } from '../types'; + +export const useRequestParams = ({ + services, + request, +}: { + services: UnifiedHistogramServices; + request?: UnifiedHistogramRequestContext; +}) => { const { data } = services; const queryStateContainer = useMemo(() => { @@ -41,10 +47,12 @@ export const useRequestParams = (services: UnifiedHistogramServices) => { [data.query.queryString, queryState.query] ); + // We need to update the absolute time range whenever the relative + // time range changes, or when the lastReloadRequestTime changes const timeRange = useMemo( () => data.query.timefilter.timefilter.getAbsoluteTime(), // eslint-disable-next-line react-hooks/exhaustive-deps - [data.query.timefilter.timefilter, queryState.time] + [data.query.timefilter.timefilter, queryState.time, request?.lastReloadRequestTime] ); return { filters, query, timeRange }; From af9a681b58ea71d0b400c494b79f307c20f60ac0 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 4 Nov 2022 00:19:00 -0300 Subject: [PATCH 23/84] [Discover] Improve time range text loading state --- .../unified_histogram/public/chart/use_time_range.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/use_time_range.tsx b/src/plugins/unified_histogram/public/chart/use_time_range.tsx index bb66cffca263f..b12d7f3040a7b 100644 --- a/src/plugins/unified_histogram/public/chart/use_time_range.tsx +++ b/src/plugins/unified_histogram/public/chart/use_time_range.tsx @@ -42,10 +42,6 @@ export const useTimeRange = ({ ); const timeRangeText = useMemo(() => { - if (!bucketInterval) { - return ''; - } - const timeRange = { from: dateMath.parse(from), to: dateMath.parse(to, { roundUp: true }), @@ -60,7 +56,12 @@ export const useTimeRange = ({ defaultMessage: 'Auto', })} - ` : '' - }${bucketInterval.description}`, + }${ + bucketInterval?.description ?? + i18n.translate('unifiedHistogram.histogramTimeRangeIntervalLoading', { + defaultMessage: 'loading...', + }) + }`, }, }); From 3186657be3422512bad7b67821f313c8d173a5b7 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 4 Nov 2022 13:56:18 +0300 Subject: [PATCH 24/84] [Discover] fix dashboard tests --- x-pack/plugins/lens/public/plugin.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 8a12a58f16087..e995bfe319e82 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -317,18 +317,14 @@ export class LensPlugin { visualizations.registerAlias(getLensAliasConfig()); - const discoverLocator = share?.url.locators.get('DISCOVER_APP_LOCATOR'); - - if (discoverLocator) { - uiActionsEnhanced.registerDrilldown( - new OpenInDiscoverDrilldown({ - dataViews: () => this.dataViewsService!, - locator: () => discoverLocator, - hasDiscoverAccess: () => this.hasDiscoverAccess, - application: () => startServices().core.application, - }) - ); - } + uiActionsEnhanced.registerDrilldown( + new OpenInDiscoverDrilldown({ + dataViews: () => this.dataViewsService!, + locator: () => share?.url.locators.get('DISCOVER_APP_LOCATOR'), + hasDiscoverAccess: () => this.hasDiscoverAccess, + application: () => startServices().core.application, + }) + ); setupExpressions( expressions, @@ -500,7 +496,6 @@ export class LensPlugin { ); const discoverLocator = startDependencies.share?.url.locators.get('DISCOVER_APP_LOCATOR'); - if (discoverLocator) { startDependencies.uiActions.addTriggerAction( CONTEXT_MENU_TRIGGER, From 3e4418b1fe67b9695d2033c835d1c53458826bdf Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 4 Nov 2022 16:34:24 +0300 Subject: [PATCH 25/84] [Discover] fix jest tests --- .../__mocks__/data_view_with_timefield.ts | 5 ++ .../discover/public/__mocks__/services.ts | 1 + .../layout/discover_layout.test.tsx | 83 ++++-------------- .../layout/discover_main_content.test.tsx | 83 ++++-------------- .../layout/use_discover_histogram.test.ts | 84 +------------------ .../__mocks__/data_view_with_timefield.ts | 6 ++ .../public/__mocks__/services.ts | 1 + .../public/chart/chart.test.tsx | 53 +++--------- 8 files changed, 60 insertions(+), 256 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/data_view_with_timefield.ts b/src/plugins/discover/public/__mocks__/data_view_with_timefield.ts index 803fb7c6f70db..a481c648aad20 100644 --- a/src/plugins/discover/public/__mocks__/data_view_with_timefield.ts +++ b/src/plugins/discover/public/__mocks__/data_view_with_timefield.ts @@ -18,6 +18,7 @@ const fields = [ }, { name: 'timestamp', + displayName: 'timestamp', type: 'date', scripted: false, filterable: true, @@ -26,12 +27,14 @@ const fields = [ }, { name: 'message', + displayName: 'message', type: 'string', scripted: false, filterable: false, }, { name: 'extension', + displayName: 'extension', type: 'string', scripted: false, filterable: true, @@ -39,6 +42,7 @@ const fields = [ }, { name: 'bytes', + displayName: 'bytes', type: 'number', scripted: false, filterable: true, @@ -46,6 +50,7 @@ const fields = [ }, { name: 'scripted', + displayName: 'scripted', type: 'number', scripted: true, filterable: false, diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 97de63d231a46..c6b307098c1a8 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -126,4 +126,5 @@ export const discoverServiceMock = { savedObjectsTagging: {}, dataViews: dataViewsMock, timefilter: { createFilter: jest.fn() }, + lens: { EmbeddableComponent: jest.fn(() => null) }, } as unknown as DiscoverServices; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 0e9c7f8449520..0a76d979c71f8 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { Subject, BehaviorSubject } from 'rxjs'; +import { Subject, BehaviorSubject, of } from 'rxjs'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import type { Query, AggregateQuery } from '@kbn/es-query'; import { setHeaderActionMenuMounter } from '../../../../kibana_services'; @@ -15,14 +15,16 @@ import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout'; import { esHits } from '../../../../__mocks__/es_hits'; import { dataViewMock } from '../../../../__mocks__/data_view'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { + createSearchSourceMock, + searchSourceInstanceMock, +} from '@kbn/data-plugin/common/search/search_source/mocks'; import type { DataView } from '@kbn/data-views-plugin/public'; import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; import { GetStateReturn } from '../../services/discover_state'; import { DiscoverLayoutProps } from './types'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, @@ -37,66 +39,10 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { DiscoverServices } from '../../../../build_services'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; import { DiscoverAppStateProvider } from '../../services/discover_app_state_container'; -import type { UnifiedHistogramChartData } from '@kbn/unified-histogram-plugin/public'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { setTimeout } from 'timers/promises'; import { act } from 'react-dom/test-utils'; - -jest.mock('@kbn/unified-histogram-plugin/public', () => { - const originalModule = jest.requireActual('@kbn/unified-histogram-plugin/public'); - - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as UnifiedHistogramChartData; - - return { - ...originalModule, - buildChartData: jest.fn().mockImplementation(() => ({ - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - })), - }; -}); +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; function getAppStateContainer() { const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; @@ -127,6 +73,14 @@ async function mountComponent( return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) + ); + const dataViewList = [dataView]; const main$ = new BehaviorSubject({ @@ -150,16 +104,10 @@ async function mountComponent( result: Number(esHits.length), }) as DataTotalHits$; - const charts$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: {} as unknown as SearchResponse, - }) as DataCharts$; - const savedSearchData$ = { main$, documents$, totalHits$, - charts$, availableFields$, }; @@ -175,7 +123,7 @@ async function mountComponent( savedSearchData$, savedSearchRefetch$: new Subject(), searchSource: searchSourceMock, - state: { columns: [], query }, + state: { columns: [], query, hideChart: false, interval: 'auto' }, stateContainer: { setAppState: () => {}, appStateContainer: { @@ -188,6 +136,7 @@ async function mountComponent( persistDataView: jest.fn(), updateAdHocDataViewId: jest.fn(), adHocDataViewList: [], + searchSessionManager: createSearchSessionMock().searchSessionManager, }; const component = mountWithIntl( diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx index 54e3fa0b19ca5..7d94bd86ec842 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { Subject, BehaviorSubject } from 'rxjs'; +import { Subject, BehaviorSubject, of } from 'rxjs'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; import { esHits } from '../../../../__mocks__/es_hits'; import { dataViewMock } from '../../../../__mocks__/data_view'; @@ -15,7 +15,6 @@ import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { GetStateReturn } from '../../services/discover_state'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, @@ -33,67 +32,11 @@ import { setTimeout } from 'timers/promises'; import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; -import { - UnifiedHistogramChartData, - UnifiedHistogramLayout, -} from '@kbn/unified-histogram-plugin/public'; +import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; import { HISTOGRAM_HEIGHT_KEY } from './use_discover_histogram'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; - -jest.mock('@kbn/unified-histogram-plugin/public', () => { - const originalModule = jest.requireActual('@kbn/unified-histogram-plugin/public'); - - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as UnifiedHistogramChartData; - - return { - ...originalModule, - buildChartData: jest.fn().mockImplementation(() => ({ - chartData, - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - })), - }; -}); +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { RequestAdapter } from '@kbn/inspector-plugin/public'; +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; const mountComponent = async ({ isPlainRecord = false, @@ -115,6 +58,14 @@ const mountComponent = async ({ return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) + ); + if (storage) { services = { ...services, storage }; } @@ -140,16 +91,10 @@ const mountComponent = async ({ result: Number(esHits.length), }) as DataTotalHits$; - const charts$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: {} as unknown as SearchResponse, - }) as DataCharts$; - const savedSearchData$ = { main$, documents$, totalHits$, - charts$, availableFields$, }; @@ -177,6 +122,8 @@ const mountComponent = async ({ onFieldEdited: jest.fn(), columns: [], resizeRef: { current: null }, + searchSessionManager: createSearchSessionMock().searchSessionManager, + inspectorAdapters: { requests: new RequestAdapter() }, }; const coreTheme$ = new BehaviorSubject({ darkMode: false }); diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts index 12fde6a5b1061..59b2e0492f62b 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; import { esHits } from '../../../../__mocks__/es_hits'; import { act, renderHook } from '@testing-library/react-hooks'; @@ -14,7 +13,6 @@ import { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../../types'; import { AvailableFields$, - DataCharts$, DataDocuments$, DataMain$, DataTotalHits$, @@ -33,6 +31,8 @@ import { } from './use_discover_histogram'; import { setTimeout } from 'timers/promises'; import { calculateBounds } from '@kbn/data-plugin/public'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { RequestAdapter } from '@kbn/inspector-plugin/public'; const mockData = dataPluginMock.createStartContract(); @@ -100,61 +100,10 @@ describe('useDiscoverHistogram', () => { result: Number(esHits.length), }) as DataTotalHits$; - const charts$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - response: { - took: 0, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: 29, - max_score: null, - hits: [], - }, - aggregations: { - '2': { - buckets: [ - { - key_as_string: '2022-10-05T16:00:00.000-03:00', - key: 1664996400000, - doc_count: 6, - }, - { - key_as_string: '2022-10-05T16:30:00.000-03:00', - key: 1664998200000, - doc_count: 2, - }, - { - key_as_string: '2022-10-05T17:00:00.000-03:00', - key: 1665000000000, - doc_count: 3, - }, - { - key_as_string: '2022-10-05T17:30:00.000-03:00', - key: 1665001800000, - doc_count: 8, - }, - { - key_as_string: '2022-10-05T18:00:00.000-03:00', - key: 1665003600000, - doc_count: 10, - }, - ], - }, - }, - } as SearchResponse, - }) as DataCharts$; - const savedSearchData$ = { main$, documents$, totalHits$, - charts$, availableFields$, }; @@ -167,6 +116,8 @@ describe('useDiscoverHistogram', () => { savedSearch: savedSearchMock, isTimeBased, isPlainRecord, + inspectorAdapters: { requests: new RequestAdapter() }, + searchSessionManager: createSearchSessionMock().searchSessionManager, }); }); @@ -175,29 +126,6 @@ describe('useDiscoverHistogram', () => { return hook; }; - const expectedChartData = { - xAxisOrderedValues: [1664996400000, 1664998200000, 1665000000000, 1665001800000, 1665003600000], - xAxisFormat: { id: 'date', params: { pattern: 'HH:mm:ss.SSS' } }, - xAxisLabel: 'timestamp per 0 milliseconds', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: 'P0D', - intervalESUnit: 'ms', - intervalESValue: 0, - min: '1991-03-29T08:04:00.694Z', - max: '2021-03-29T07:04:00.695Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1664996400000, y: 6 }, - { x: 1664998200000, y: 2 }, - { x: 1665000000000, y: 3 }, - { x: 1665001800000, y: 8 }, - { x: 1665003600000, y: 10 }, - ], - }; - describe('contexts', () => { it('should output the correct hits context', async () => { const { result } = await renderUseDiscoverHistogram(); @@ -207,12 +135,8 @@ describe('useDiscoverHistogram', () => { it('should output the correct chart context', async () => { const { result } = await renderUseDiscoverHistogram(); - expect(result.current.chart?.status).toBe(FetchStatus.COMPLETE); expect(result.current.chart?.hidden).toBe(false); expect(result.current.chart?.timeInterval).toBe('auto'); - expect(result.current.chart?.bucketInterval?.toString()).toBe('P0D'); - expect(JSON.stringify(result.current.chart?.data)).toBe(JSON.stringify(expectedChartData)); - expect(result.current.chart?.error).toBeUndefined(); }); it('should output undefined for hits and chart if isPlainRecord is true', async () => { diff --git a/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts b/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts index 158d697d67c71..b0ec2fcf84ebb 100644 --- a/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts +++ b/src/plugins/unified_histogram/public/__mocks__/data_view_with_timefield.ts @@ -12,12 +12,14 @@ import { buildDataViewMock } from './data_view'; const fields = [ { name: '_index', + displayName: '_index', type: 'string', scripted: false, filterable: true, }, { name: 'timestamp', + displayName: 'timestamp', type: 'date', scripted: false, filterable: true, @@ -26,12 +28,14 @@ const fields = [ }, { name: 'message', + displayName: 'message', type: 'string', scripted: false, filterable: false, }, { name: 'extension', + displayName: 'extension', type: 'string', scripted: false, filterable: true, @@ -39,6 +43,7 @@ const fields = [ }, { name: 'bytes', + displayName: 'bytes', type: 'number', scripted: false, filterable: true, @@ -46,6 +51,7 @@ const fields = [ }, { name: 'scripted', + displayName: 'scripted', type: 'number', scripted: true, filterable: false, diff --git a/src/plugins/unified_histogram/public/__mocks__/services.ts b/src/plugins/unified_histogram/public/__mocks__/services.ts index e827596d88feb..1ce16ad8fae85 100644 --- a/src/plugins/unified_histogram/public/__mocks__/services.ts +++ b/src/plugins/unified_histogram/public/__mocks__/services.ts @@ -25,4 +25,5 @@ export const unifiedHistogramServicesMock = { useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme), }, + lens: { EmbeddableComponent: jest.fn(() => null) }, } as unknown as UnifiedHistogramServices; diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index 41de0687acfa6..80d3a37d0c27b 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -9,11 +9,14 @@ import React, { ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import type { UnifiedHistogramChartData, UnifiedHistogramFetchStatus } from '../types'; +import type { UnifiedHistogramFetchStatus } from '../types'; import { Chart } from './chart'; import type { ReactWrapper } from 'enzyme'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { of } from 'rxjs'; import { HitsCounter } from '../hits_counter'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; async function mountComponent({ noChart, @@ -32,47 +35,16 @@ async function mountComponent({ services.data.query.timefilter.timefilter.getAbsoluteTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; - - const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], - } as unknown as UnifiedHistogramChartData; + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: noHits ? 0 : 2 } } })) + ); const props = { + dataView: dataViewWithTimefieldMock, services: unifiedHistogramServicesMock, hits: noHits ? undefined @@ -91,7 +63,6 @@ async function mountComponent({ description: 'test', scale: 2, }, - data: chartData, }, appendHistogram, onEditVisualization: onEditVisualization || undefined, From e45eef313bc44a8e0407b2e189533e6efc4d340e Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 4 Nov 2022 18:07:26 +0300 Subject: [PATCH 26/84] [Discover] fix some jest tests --- .../main/hooks/use_saved_search.test.ts | 1 - .../hooks/use_saved_search_messages.test.ts | 5 +- .../application/main/utils/fetch_all.ts | 2 +- .../main/utils/fetch_documents.test.ts | 20 +---- .../chart/build_bucket_interval.test.ts | 31 +------ .../public/chart/build_bucket_interval.ts | 2 +- .../public/chart/histogram.test.tsx | 83 +++---------------- .../public/chart/use_chart_panels.test.ts | 4 - .../public/layout/layout.test.tsx | 18 ++-- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 12 files changed, 33 insertions(+), 136 deletions(-) diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts index 34cdeb232be88..f46378053d355 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts @@ -45,7 +45,6 @@ describe('test useSavedSearch', () => { expect(result.current.data$.main$.getValue().fetchStatus).toBe(FetchStatus.LOADING); expect(result.current.data$.documents$.getValue().fetchStatus).toBe(FetchStatus.LOADING); expect(result.current.data$.totalHits$.getValue().fetchStatus).toBe(FetchStatus.LOADING); - expect(result.current.data$.charts$.getValue().fetchStatus).toBe(FetchStatus.LOADING); }); test('refetch$ triggers a search', async () => { const { history, searchSessionManager } = createSearchSessionMock(); diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts index 1159aee1c5d13..5b9b96db1afaf 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts @@ -62,7 +62,10 @@ describe('test useSavedSearch message generators', () => { done(); } }); - sendLoadingMsg(main$, RecordRawType.DOCUMENT); + sendLoadingMsg(main$, { + foundDocuments: true, + recordRawType: RecordRawType.DOCUMENT, + }); }); test('sendErrorMsg', (done) => { const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.PARTIAL }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 146f41c871276..82cf4712848b3 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -148,7 +148,7 @@ export function fetchAll( .catch(sendErrorTo(dataSubjects.documents$, dataSubjects.main$)); // Return a promise that will resolve once all the requests have finished or failed - return Promise.allSettled([documents]).then(() => { + return documents.then(() => { // Send a complete message to main$ once all queries are done and if main$ // is not already in an ERROR state, e.g. because the document query has failed. // This will only complete main$, if it hasn't already been completed previously diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts index 4809da54655be..28738cdc522c9 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts @@ -8,12 +8,11 @@ import { fetchDocuments } from './fetch_documents'; import { throwError as throwErrorRx, of } from 'rxjs'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; import { discoverServiceMock } from '../../../__mocks__/services'; import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { FetchDeps } from './fetch_all'; -import { fetchTotalHits } from './fetch_total_hits'; import type { EsHitRecord } from '../../../types'; import { buildDataTableRecord } from '../../../utils/build_data_record'; import { dataViewMock } from '../../../__mocks__/data_view'; @@ -47,21 +46,4 @@ describe('test fetchDocuments', () => { new Error('Oh noes!') ); }); - - test('fetch$ is called with execution context containing savedSearch id', async () => { - const fetch$Mock = jest.fn().mockReturnValue( - of({ - rawResponse: { hits: { hits: [] } }, - } as unknown as IKibanaSearchResponse) - ); - - savedSearchMockWithTimeField.searchSource.fetch$ = fetch$Mock; - - await fetchTotalHits(savedSearchMockWithTimeField.searchSource, getDeps()); - expect(fetch$Mock.mock.calls[0][0].executionContext).toMatchInlineSnapshot(` - Object { - "description": "fetch total hits", - } - `); - }); }); diff --git a/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts b/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts index 0ccd23357b347..737d77ced1c02 100644 --- a/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts +++ b/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts @@ -11,7 +11,7 @@ import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield import { calculateBounds } from '@kbn/data-plugin/public'; import { buildBucketInterval } from './build_bucket_interval'; -describe('buildChartData', () => { +describe('buildBucketInterval', () => { const getOptions = () => { const response = { took: 0, @@ -75,35 +75,6 @@ describe('buildChartData', () => { }; }; - const expectedChartData = { - xAxisOrderedValues: [1664996400000, 1664998200000, 1665000000000, 1665001800000, 1665003600000], - xAxisFormat: { id: 'date', params: { pattern: 'HH:mm:ss.SSS' } }, - xAxisLabel: 'timestamp per 0 milliseconds', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: 'P0D', - intervalESUnit: 'ms', - intervalESValue: 0, - min: '1991-03-29T08:04:00.694Z', - max: '2021-03-29T07:04:00.695Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1664996400000, y: 6 }, - { x: 1664998200000, y: 2 }, - { x: 1665000000000, y: 3 }, - { x: 1665001800000, y: 8 }, - { x: 1665003600000, y: 10 }, - ], - }; - - it('should return the correct data', () => { - const { bucketInterval, chartData } = buildBucketInterval(getOptions()); - expect(bucketInterval!.toString()).toEqual('P0D'); - expect(JSON.stringify(chartData)).toEqual(JSON.stringify(expectedChartData)); - }); - it('should return an empty object if response or timeInterval is undefined', () => { expect( buildBucketInterval({ diff --git a/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts b/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts index c038b79543b4c..84e286cfc05ee 100644 --- a/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts +++ b/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts @@ -15,7 +15,7 @@ import { getChartAggConfigs } from './get_chart_agg_configs'; /** * Convert the response from the chart request into a format that can be used * by the unified histogram chart. The returned object should be used to update - * {@link UnifiedHistogramChartContext.bucketInterval} and {@link UnifiedHistogramChartContext.data}. + * time range interval of histogram. */ export const buildBucketInterval = ({ data, diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index 3e1213978e385..b684988c6d54c 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -6,55 +6,13 @@ * Side Public License, v 1. */ import { mountWithIntl } from '@kbn/test-jest-helpers'; -import type { UnifiedHistogramChartData, UnifiedHistogramFetchStatus } from '../types'; +import type { UnifiedHistogramFetchStatus } from '../types'; import { Histogram } from './histogram'; import React from 'react'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; -const chartData = { - xAxisOrderedValues: [ - 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000, - 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000, - 1624917600000, 1625004000000, 1625090400000, - ], - xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, - xAxisLabel: 'order_date per day', - yAxisFormat: { id: 'number' }, - ordered: { - date: true, - interval: { - asMilliseconds: jest.fn(), - }, - intervalESUnit: 'd', - intervalESValue: 1, - min: '2021-03-18T08:28:56.411Z', - max: '2021-07-01T07:28:56.411Z', - }, - yAxisLabel: 'Count', - values: [ - { x: 1623880800000, y: 134 }, - { x: 1623967200000, y: 152 }, - { x: 1624053600000, y: 141 }, - { x: 1624140000000, y: 138 }, - { x: 1624226400000, y: 142 }, - { x: 1624312800000, y: 157 }, - { x: 1624399200000, y: 149 }, - { x: 1624485600000, y: 146 }, - { x: 1624572000000, y: 170 }, - { x: 1624658400000, y: 137 }, - { x: 1624744800000, y: 150 }, - { x: 1624831200000, y: 144 }, - { x: 1624917600000, y: 147 }, - { x: 1625004000000, y: 137 }, - { x: 1625090400000, y: 66 }, - ], -} as unknown as UnifiedHistogramChartData; - -function mountComponent( - status: UnifiedHistogramFetchStatus, - data: UnifiedHistogramChartData | null = chartData, - error?: Error -) { +function mountComponent(status: UnifiedHistogramFetchStatus, error?: Error) { const services = unifiedHistogramServicesMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; @@ -65,18 +23,20 @@ function mountComponent( const props = { services: unifiedHistogramServicesMock, chart: { - status, hidden: false, timeInterval: 'auto', - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, - data: data ?? undefined, - error, }, timefilterUpdateHandler, + dataView: dataViewWithTimefieldMock, + filters: [], + query: { + language: 'kuery', + query: '', + }, + timeRange: { + from: '2020-05-14T11:05:13.590', + to: '2020-05-14T11:20:13.590', + }, }; return mountWithIntl(); @@ -87,21 +47,4 @@ describe('Histogram', () => { const component = mountComponent('complete'); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true); }); - - it('renders error correctly', () => { - const component = mountComponent('error', null, new Error('Loading error')); - expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(false); - expect(component.find('[data-test-subj="unifiedHistogramErrorChartContainer"]').exists()).toBe( - true - ); - expect( - component.find('[data-test-subj="unifiedHistogramErrorChartText"]').get(1).props.children - ).toBe('Loading error'); - }); - - it('renders loading state correctly', () => { - const component = mountComponent('loading', null); - expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true); - expect(component.find('[data-test-subj="unifiedHistogramChartLoading"]').exists()).toBe(true); - }); }); diff --git a/src/plugins/unified_histogram/public/chart/use_chart_panels.test.ts b/src/plugins/unified_histogram/public/chart/use_chart_panels.test.ts index 71e2d3e4a705a..aec3f1a8e291f 100644 --- a/src/plugins/unified_histogram/public/chart/use_chart_panels.test.ts +++ b/src/plugins/unified_histogram/public/chart/use_chart_panels.test.ts @@ -19,7 +19,6 @@ describe('test useChartPanels', () => { closePopover: jest.fn(), onResetChartHeight: jest.fn(), chart: { - status: 'complete', hidden: true, timeInterval: 'auto', }, @@ -39,7 +38,6 @@ describe('test useChartPanels', () => { closePopover: jest.fn(), onResetChartHeight: jest.fn(), chart: { - status: 'complete', hidden: false, timeInterval: 'auto', }, @@ -59,7 +57,6 @@ describe('test useChartPanels', () => { onTimeIntervalChange: jest.fn(), closePopover: jest.fn(), chart: { - status: 'complete', hidden: false, timeInterval: 'auto', }, @@ -78,7 +75,6 @@ describe('test useChartPanels', () => { closePopover: jest.fn(), onResetChartHeight, chart: { - status: 'complete', hidden: false, timeInterval: 'auto', }, diff --git a/src/plugins/unified_histogram/public/layout/layout.test.tsx b/src/plugins/unified_histogram/public/layout/layout.test.tsx index 73b97c8f64def..0673d255362c4 100644 --- a/src/plugins/unified_histogram/public/layout/layout.test.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.test.tsx @@ -6,13 +6,16 @@ * Side Public License, v 1. */ +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import type { ReactWrapper } from 'enzyme'; import React from 'react'; import { act } from 'react-dom/test-utils'; +import { of } from 'rxjs'; import { Chart } from '../chart'; import { Panels, PANELS_MODE } from '../panels'; import type { UnifiedHistogramChartContext, UnifiedHistogramHitsContext } from '../types'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from './layout'; @@ -35,14 +38,8 @@ describe('Layout', () => { }); const createChart = (): UnifiedHistogramChartContext => ({ - status: 'complete', hidden: false, timeInterval: 'auto', - bucketInterval: { - scaled: true, - description: 'test', - scale: 2, - }, }); const mountComponent = async ({ @@ -59,12 +56,21 @@ describe('Layout', () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) + ); + const component = mountWithIntl( ); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 57fc3ad319fe7..c42c82362e468 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2221,7 +2221,6 @@ "discover.helpMenu.appName": "Découverte", "discover.inspectorRequestDataTitleChart": "Données du graphique", "discover.inspectorRequestDataTitleDocuments": "Documents", - "discover.inspectorRequestDescriptionChart": "Cette requête interroge Elasticsearch afin de récupérer les données d'agrégation pour le graphique.", "discover.inspectorRequestDescriptionDocument": "Cette requête interroge Elasticsearch afin de récupérer les documents.", "discover.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", "discover.json.copyToClipboardLabel": "Copier dans le presse-papiers", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3e0425fcb222b..b3ab75165960f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2217,7 +2217,6 @@ "discover.helpMenu.appName": "Discover", "discover.inspectorRequestDataTitleChart": "グラフデータ", "discover.inspectorRequestDataTitleDocuments": "ドキュメント", - "discover.inspectorRequestDescriptionChart": "このリクエストはElasticsearchにクエリをかけ、グラフの集計データを取得します。", "discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、ドキュメントを取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.json.copyToClipboardLabel": "クリップボードにコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4ca62119353f0..1adc45ea80d44 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2221,7 +2221,6 @@ "discover.helpMenu.appName": "Discover", "discover.inspectorRequestDataTitleChart": "图表数据", "discover.inspectorRequestDataTitleDocuments": "文档", - "discover.inspectorRequestDescriptionChart": "此请求将查询 Elasticsearch 以获取图表的聚合数据。", "discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取文档。", "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "discover.json.copyToClipboardLabel": "复制到剪贴板", From b100682f6d98a4d46d3e5752578a67cdc1dfddf8 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 7 Nov 2022 16:38:14 -0400 Subject: [PATCH 27/84] [Discover] Clean up doc table force rerender code --- .../components/layout/discover_main_content.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 3ff0021b4fb73..98c2be74775fa 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { SavedSearch } from '@kbn/saved-search-plugin/public'; -import React, { RefObject, useCallback, useEffect, useState } from 'react'; +import React, { RefObject, useCallback } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE } from '@kbn/analytics'; import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; @@ -120,14 +120,6 @@ export const DiscoverMainContent = ({ searchSessionManager, }); - // The documents grid doesn't rerender when the chart visbility changes - // which causes it to render blank space, so we need to force a rerender - const [documentsKey, setDocumentsKey] = useState(0); - - useEffect(() => { - setDocumentsKey((key) => key + 1); - }, [chart?.hidden]); - return ( Date: Mon, 7 Nov 2022 17:04:54 -0400 Subject: [PATCH 28/84] [Discover] Make lastReloadRequestTime a top level prop --- .../layout/discover_main_content.tsx | 2 + .../layout/use_discover_histogram.ts | 48 ++++++++++--------- .../unified_histogram/public/chart/chart.tsx | 10 +++- .../public/chart/histogram.tsx | 4 +- .../public/chart/use_request_params.tsx | 4 +- .../public/chart/use_total_hits.ts | 11 +++-- .../public/layout/layout.tsx | 6 +++ src/plugins/unified_histogram/public/types.ts | 4 -- 8 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 98c2be74775fa..81d59d2f8c249 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -97,6 +97,7 @@ export const DiscoverMainContent = ({ const { topPanelHeight, + lastReloadRequestTime, request, hits, chart, @@ -125,6 +126,7 @@ export const DiscoverMainContent = ({ className="dscPageContent__inner" services={services} dataView={dataView} + lastReloadRequestTime={lastReloadRequestTime} request={request} hits={hits} chart={chart} diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 8b98fa659b57d..f2f97501ec2fa 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -120,34 +120,13 @@ export const useDiscoverHistogram = ({ * Request */ - const [lastReloadRequestTime, setLastReloadRequestTime] = useState(0); - const { fetchStatus: mainFetchStatus } = useDataState(savedSearchData$.main$); - - // Reload unified histogram when a refetch is triggered, - // with a debounce to avoid multiple requests - const [, cancelDebounce] = useDebounce( - () => { - if (mainFetchStatus === FetchStatus.LOADING) { - setLastReloadRequestTime(Date.now()); - } - }, - 100, - [mainFetchStatus] - ); - - // A refetch is triggered when the data view is changed, - // but we don't want to reload unified histogram in this case, - // so cancel the debounced effect on unmount - useEffect(() => cancelDebounce, [cancelDebounce]); - const searchSessionId = searchSessionManager.getLastSearchSessionId(); const request = useMemo( () => ({ searchSessionId, adapter: inspectorAdapters.requests, - lastReloadRequestTime, }), - [inspectorAdapters.requests, lastReloadRequestTime, searchSessionId] + [inspectorAdapters.requests, searchSessionId] ); /** @@ -245,8 +224,33 @@ export const useDiscoverHistogram = ({ [field, isPlainRecord, isTimeBased] ); + /** + * Reload + */ + + const [lastReloadRequestTime, setLastReloadRequestTime] = useState(0); + const { fetchStatus: mainFetchStatus } = useDataState(savedSearchData$.main$); + + // Reload unified histogram when a refetch is triggered, + // with a debounce to avoid multiple requests + const [, cancelDebounce] = useDebounce( + () => { + if (mainFetchStatus === FetchStatus.LOADING) { + setLastReloadRequestTime(Date.now()); + } + }, + 100, + [mainFetchStatus] + ); + + // A refetch is triggered when the data view is changed, + // but we don't want to reload unified histogram in this case, + // so cancel the debounced effect on unmount + useEffect(() => cancelDebounce, [cancelDebounce]); + return { topPanelHeight, + lastReloadRequestTime, request, hits, chart, diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index bf0a3f8ae8a4e..94ab5c770e598 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -40,6 +40,7 @@ export interface ChartProps { className?: string; services: UnifiedHistogramServices; dataView: DataView; + lastReloadRequestTime: number | undefined; request?: UnifiedHistogramRequestContext; hits?: UnifiedHistogramHitsContext; chart?: UnifiedHistogramChartContext; @@ -61,6 +62,7 @@ export function Chart({ className, services, dataView, + lastReloadRequestTime, request, hits, chart, @@ -102,10 +104,15 @@ export function Chart({ dataView.isTimeBased() ); - const { filters, query, timeRange } = useRequestParams({ services, request }); + const { filters, query, timeRange } = useRequestParams({ + services, + lastReloadRequestTime, + request, + }); useTotalHits({ services, + lastReloadRequestTime, request, chartVisible, hits, @@ -226,6 +233,7 @@ export function Chart({ diff --git a/src/plugins/unified_histogram/public/chart/use_request_params.tsx b/src/plugins/unified_histogram/public/chart/use_request_params.tsx index 01cb89ad6646a..4fbd748864a67 100644 --- a/src/plugins/unified_histogram/public/chart/use_request_params.tsx +++ b/src/plugins/unified_histogram/public/chart/use_request_params.tsx @@ -13,9 +13,11 @@ import type { UnifiedHistogramRequestContext, UnifiedHistogramServices } from '. export const useRequestParams = ({ services, + lastReloadRequestTime, request, }: { services: UnifiedHistogramServices; + lastReloadRequestTime: number | undefined; request?: UnifiedHistogramRequestContext; }) => { const { data } = services; @@ -52,7 +54,7 @@ export const useRequestParams = ({ const timeRange = useMemo( () => data.query.timefilter.timefilter.getAbsoluteTime(), // eslint-disable-next-line react-hooks/exhaustive-deps - [data.query.timefilter.timefilter, queryState.time, request?.lastReloadRequestTime] + [data.query.timefilter.timefilter, queryState.time, lastReloadRequestTime] ); return { filters, query, timeRange }; diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.ts index 5ec6f4768d9a5..3a6361125a916 100644 --- a/src/plugins/unified_histogram/public/chart/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.ts @@ -22,6 +22,7 @@ import type { export const useTotalHits = ({ services, + lastReloadRequestTime, request, chartVisible, hits, @@ -32,6 +33,7 @@ export const useTotalHits = ({ onTotalHitsChange, }: { services: UnifiedHistogramServices; + lastReloadRequestTime: number | undefined; request: UnifiedHistogramRequestContext | undefined; chartVisible: boolean; hits: UnifiedHistogramHitsContext | undefined; @@ -47,7 +49,7 @@ export const useTotalHits = ({ useEffect(() => { const newTotalHitsDeps = getTotalHitsDeps({ chartVisible, - request, + lastReloadRequestTime, hits, dataView, filters, @@ -76,6 +78,7 @@ export const useTotalHits = ({ dataView, filters, hits, + lastReloadRequestTime, onTotalHitsChange, query, request, @@ -86,7 +89,7 @@ export const useTotalHits = ({ const getTotalHitsDeps = ({ chartVisible, - request, + lastReloadRequestTime, hits, dataView, filters, @@ -94,7 +97,7 @@ const getTotalHitsDeps = ({ timeRange, }: { chartVisible: boolean; - request: UnifiedHistogramRequestContext | undefined; + lastReloadRequestTime: number | undefined; hits: UnifiedHistogramHitsContext | undefined; dataView: DataView; filters: Filter[]; @@ -108,7 +111,7 @@ const getTotalHitsDeps = ({ filters, query, timeRange, - request?.lastReloadRequestTime, + lastReloadRequestTime, ]); const fetchTotalHits = async ({ diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index a6f6b3955e7d0..e58271104ee17 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -28,6 +28,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren className?: string; services: UnifiedHistogramServices; dataView: DataView; + /** + * Can be updated to `Date.now()` to force a refresh + */ + lastReloadRequestTime?: number; /** * Context object for requests made by unified histogram components -- optional */ @@ -91,6 +95,7 @@ export const UnifiedHistogramLayout = ({ className, services, dataView, + lastReloadRequestTime, request, hits, chart, @@ -153,6 +158,7 @@ export const UnifiedHistogramLayout = ({ className={chartClassName} services={services} dataView={dataView} + lastReloadRequestTime={lastReloadRequestTime} request={request} hits={hits} chart={chart} diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index 01a1fe2de01af..d62ea48b6c598 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -74,10 +74,6 @@ export interface UnifiedHistogramRequestContext { * The adapter to use for requests */ adapter?: RequestAdapter; - /** - * Can be updated to `Date.now()` to force a refresh - */ - lastReloadRequestTime?: number; } /** From 2848897b65d1c6867a76ac73b14ebe4bda776df4 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 7 Nov 2022 17:08:23 -0400 Subject: [PATCH 29/84] [Discover] Update queryStateContainer to be a ref --- .../public/chart/use_request_params.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/use_request_params.tsx b/src/plugins/unified_histogram/public/chart/use_request_params.tsx index 4fbd748864a67..533a92d1a3ec6 100644 --- a/src/plugins/unified_histogram/public/chart/use_request_params.tsx +++ b/src/plugins/unified_histogram/public/chart/use_request_params.tsx @@ -8,7 +8,7 @@ import { connectToQueryState, QueryState } from '@kbn/data-plugin/public'; import { createStateContainer, useContainerState } from '@kbn/kibana-utils-plugin/public'; -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import type { UnifiedHistogramRequestContext, UnifiedHistogramServices } from '../types'; export const useRequestParams = ({ @@ -22,14 +22,14 @@ export const useRequestParams = ({ }) => { const { data } = services; - const queryStateContainer = useMemo(() => { - return createStateContainer({ + const queryStateContainer = useRef( + createStateContainer({ filters: data.query.filterManager.getFilters(), query: data.query.queryString.getQuery(), refreshInterval: data.query.timefilter.timefilter.getRefreshInterval(), time: data.query.timefilter.timefilter.getTime(), - }); - }, [data.query.filterManager, data.query.queryString, data.query.timefilter.timefilter]); + }) + ).current; const queryState = useContainerState(queryStateContainer); From ee84cf84286bc7645233c9a1f8a361848d292a4c Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 7 Nov 2022 17:25:31 -0400 Subject: [PATCH 30/84] [Discover] Add documentation for useTotalHits --- .../public/chart/use_total_hits.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.ts index 3a6361125a916..0edb1aa92a71e 100644 --- a/src/plugins/unified_histogram/public/chart/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.ts @@ -46,6 +46,21 @@ export const useTotalHits = ({ const abortController = useRef(); const totalHitsDeps = useRef>(); + // When the unified histogram props change, we must compare the current subset + // that should trigger a total hits refetch against the previous subset. If they + // are different, we must refetch the total hits to ensure it's up to date with + // the chart. These are the props we care about: + // - chartVisible: + // We only need to fetch the total hits when the chart is hidden, + // otherwise Lens will be responsible for updating the display. + // - lastReloadRequestTime: A refetch has been manually triggered by the consumer. + // - hits: + // If the hits context is undefined, we don't need to fetch the + // total hits because the display will be hidden. + // - dataView: The current data view has changed. + // - filters: The current filters have changed. + // - query: The current query has been updated. + // - timeRange: The selected time range has changed. useEffect(() => { const newTotalHitsDeps = getTotalHitsDeps({ chartVisible, @@ -178,6 +193,7 @@ const fetchTotalHits = async ({ abortController.current = new AbortController(); + // Let the consumer inspect the request if they want to track it const inspector = request?.adapter ? { adapter: request.adapter, From b07d286a083c4550298e2f4d7f5226aeea744541 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 7 Nov 2022 17:46:18 -0400 Subject: [PATCH 31/84] [Discover] Change UnifiedHistogramFetchStatus to an enum --- .../components/layout/use_discover_histogram.ts | 8 ++++---- .../unified_histogram/public/chart/histogram.tsx | 7 +++++-- .../public/chart/use_total_hits.ts | 6 +++--- src/plugins/unified_histogram/public/index.ts | 2 +- src/plugins/unified_histogram/public/types.ts | 13 +++++++------ 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index f2f97501ec2fa..a21bd5340af2a 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -13,7 +13,7 @@ import { triggerVisualizeActions, } from '@kbn/unified-field-list-plugin/public'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import type { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; +import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; import useDebounce from 'react-use/lib/useDebounce'; import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; import { getUiActions } from '../../../../kibana_services'; @@ -139,12 +139,12 @@ export const useDiscoverHistogram = ({ // If we have a partial result already, we don't // want to update the total hits back to loading - if (fetchStatus === 'partial' && status === 'loading') { + if (fetchStatus === FetchStatus.PARTIAL && status === UnifiedHistogramFetchStatus.loading) { return; } savedSearchData$.totalHits$.next({ - fetchStatus: status as FetchStatus, + fetchStatus: status.toString() as FetchStatus, result: totalHits, recordRawType, }); @@ -161,7 +161,7 @@ export const useDiscoverHistogram = ({ isPlainRecord ? undefined : { - status: hitsFetchStatus, + status: hitsFetchStatus.toString() as UnifiedHistogramFetchStatus, total: hitsTotal, }, [hitsFetchStatus, hitsTotal, isPlainRecord] diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 89d1f12086afb..7828bd6389b65 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -15,7 +15,7 @@ import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; import type { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import type { estypes } from '@elastic/elasticsearch'; import type { AggregateQuery, Query, Filter, TimeRange } from '@kbn/es-query'; -import type { +import { UnifiedHistogramBreakdownContext, UnifiedHistogramBucketInterval, UnifiedHistogramChartContext, @@ -74,7 +74,10 @@ export function Histogram({ (isLoading: boolean, adapters: Partial | undefined) => { const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount; - onTotalHitsChange?.(isLoading ? 'loading' : 'complete', totalHits ?? hits?.total); + onTotalHitsChange?.( + isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete, + totalHits ?? hits?.total + ); const lensRequest = adapters?.requests?.getRequests()[0]; const json = lensRequest?.response?.json as IKibanaSearchResponse; diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.ts index 0edb1aa92a71e..65308a75190b2 100644 --- a/src/plugins/unified_histogram/public/chart/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.ts @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { cloneDeep, isEqual } from 'lodash'; import { MutableRefObject, useEffect, useRef } from 'react'; import { filter, lastValueFrom, map } from 'rxjs'; -import type { +import { UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, UnifiedHistogramRequestContext, @@ -161,7 +161,7 @@ const fetchTotalHits = async ({ return; } - onTotalHitsChange?.('loading', hits.total); + onTotalHitsChange?.(UnifiedHistogramFetchStatus.loading, hits.total); const searchSource = data.search.searchSource.createEmpty(); @@ -222,5 +222,5 @@ const fetchTotalHits = async ({ const totalHits = await lastValueFrom(fetch$); - onTotalHitsChange?.('complete', totalHits); + onTotalHitsChange?.(UnifiedHistogramFetchStatus.complete, totalHits); }; diff --git a/src/plugins/unified_histogram/public/index.ts b/src/plugins/unified_histogram/public/index.ts index eaf2c21451463..6843a4c824333 100644 --- a/src/plugins/unified_histogram/public/index.ts +++ b/src/plugins/unified_histogram/public/index.ts @@ -12,12 +12,12 @@ export type { UnifiedHistogramLayoutProps } from './layout'; export { UnifiedHistogramLayout } from './layout'; export type { UnifiedHistogramServices, - UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, UnifiedHistogramChartContext, UnifiedHistogramBreakdownContext, UnifiedHistogramChartLoadEvent, UnifiedHistogramAdapters, } from './types'; +export { UnifiedHistogramFetchStatus } from './types'; export const plugin = () => new UnifiedHistogramPublicPlugin(); diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index d62ea48b6c598..1fda1e45cb78e 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -18,12 +18,13 @@ import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; /** * The fetch status of a unified histogram request */ -export type UnifiedHistogramFetchStatus = - | 'uninitialized' - | 'loading' - | 'partial' - | 'complete' - | 'error'; +export enum UnifiedHistogramFetchStatus { + uninitialized = 'uninitialized', + loading = 'loading', + partial = 'partial', + complete = 'complete', + error = 'error', +} /** * The services required by the unified histogram components From 633db2c0a908027ab71486dabba30d7976825e23 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 7 Nov 2022 20:17:45 -0400 Subject: [PATCH 32/84] [Discover] Fix issue where unified histogram doesn't start fetching until documents are fetched --- .../components/layout/discover_layout.tsx | 105 +++++++++++------- .../discover_field_visualize.stories.tsx | 15 --- .../loading_spinner/loading_spinner.scss | 4 - .../loading_spinner/loading_spinner.test.tsx | 23 ---- .../loading_spinner/loading_spinner.tsx | 27 ----- 5 files changed, 67 insertions(+), 107 deletions(-) delete mode 100644 src/plugins/discover/public/application/main/components/loading_spinner/discover_field_visualize.stories.tsx delete mode 100644 src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss delete mode 100644 src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx delete mode 100644 src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 331e4b04c736d..1e2a425e1e122 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -26,7 +26,6 @@ import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/pu import { useInspector } from '../../hooks/use_inspector'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverNoResults } from '../no_results'; -import { LoadingSpinner } from '../loading_spinner/loading_spinner'; import { DiscoverSidebarResponsive } from '../sidebar'; import { DiscoverLayoutProps } from './types'; import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS } from '../../../../../common'; @@ -196,6 +195,72 @@ export function DiscoverLayout({ const resizeRef = useRef(null); + const mainDisplay = useMemo(() => { + if (resultState === 'none') { + return ( + + ); + } + + if (resultState === 'uninitialized') { + return savedSearchRefetch$.next(undefined)} />; + } + + return ( + + ); + }, [ + columns, + data, + dataState.error, + dataView, + expandedDoc, + inspectorAdapters, + isPlainRecord, + isTimeBased, + navigateTo, + onAddFilter, + onDisableFilters, + onFieldEdited, + resetSavedSearch, + resultState, + savedSearch, + savedSearchData$, + savedSearchRefetch$, + searchSessionManager, + setExpandedDoc, + state, + stateContainer, + viewMode, + ]); + return (

- {resultState === 'none' && ( - - )} - {resultState === 'uninitialized' && ( - savedSearchRefetch$.next(undefined)} /> - )} - {resultState === 'loading' && } - {resultState === 'ready' && ( - - )} + {mainDisplay} diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/discover_field_visualize.stories.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/discover_field_visualize.stories.tsx deleted file mode 100644 index e61ac272c182f..0000000000000 --- a/src/plugins/discover/public/application/main/components/loading_spinner/discover_field_visualize.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import { storiesOf } from '@storybook/react'; -import React from 'react'; -import { LoadingSpinner } from './loading_spinner'; - -storiesOf('components/loading_spinner/LoadingSpinner', module).add('default', () => ( - -)); diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss deleted file mode 100644 index a58897e43b615..0000000000000 --- a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.scss +++ /dev/null @@ -1,4 +0,0 @@ -.dscLoading { - text-align: center; - padding: $euiSizeL 0; -} diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx deleted file mode 100644 index 36978a1f72684..0000000000000 --- a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { ReactWrapper } from 'enzyme'; -import { LoadingSpinner } from './loading_spinner'; -import { findTestSubject } from '@elastic/eui/lib/test'; - -describe('loading spinner', function () { - let component: ReactWrapper; - - it('LoadingSpinner renders a Searching text and a spinner', () => { - component = mountWithIntl(); - expect(findTestSubject(component, 'loadingSpinnerText').text()).toBe('Searching'); - expect(findTestSubject(component, 'loadingSpinner').length).toBe(1); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx deleted file mode 100644 index 949880d6c27d0..0000000000000 --- a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 or the Server - * Side Public License, v 1. - */ - -import './loading_spinner.scss'; - -import React from 'react'; -import { EuiLoadingSpinner, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -export function LoadingSpinner() { - return ( -
- -

- -

-
- - -
- ); -} From d646cb35d47543ddf8fe1b353940c5f25fe72dea Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 8 Nov 2022 05:27:40 +0100 Subject: [PATCH 33/84] Implement saved search breakdownField persistence --- .../application/main/utils/get_state_defaults.test.ts | 2 ++ .../public/application/main/utils/get_state_defaults.ts | 4 ++++ .../public/application/main/utils/persist_saved_search.ts | 6 ++++++ .../public/services/saved_searches/saved_searches_utils.ts | 2 ++ .../saved_search/public/services/saved_searches/types.ts | 2 ++ src/plugins/saved_search/server/saved_objects/search.ts | 1 + 6 files changed, 17 insertions(+) diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts index 1d5cf07446d60..aed900821c747 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts @@ -22,6 +22,7 @@ describe('getStateDefaults', () => { }); expect(actual).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "default_column", ], @@ -55,6 +56,7 @@ describe('getStateDefaults', () => { }); expect(actual).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "default_column", ], diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts index 208bf97fc376e..f32af6ec5f23b 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts @@ -91,5 +91,9 @@ export function getStateDefaults({ defaultState.rowsPerPage = savedSearch.rowsPerPage; } + if (savedSearch.breakdownField) { + defaultState.breakdownField = savedSearch.breakdownField; + } + return defaultState; } diff --git a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts index edc7d96f9decd..c0222adb9b35a 100644 --- a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts +++ b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts @@ -56,6 +56,12 @@ export async function persistSavedSearch( savedSearch.viewMode = state.viewMode; } + if (typeof state.breakdownField !== 'undefined') { + savedSearch.breakdownField = state.breakdownField; + } else if (savedSearch.breakdownField) { + savedSearch.breakdownField = ''; + } + if (state.hideAggregatedPreview) { savedSearch.hideAggregatedPreview = state.hideAggregatedPreview; } diff --git a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts index 8ca6dde21d6fc..ec5b9bd32869a 100644 --- a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts +++ b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.ts @@ -49,6 +49,7 @@ export const fromSavedSearchAttributes = ( timeRange: attributes.timeRange, refreshInterval: attributes.refreshInterval, rowsPerPage: attributes.rowsPerPage, + breakdownField: attributes.breakdownField, }); export const toSavedSearchAttributes = ( @@ -70,4 +71,5 @@ export const toSavedSearchAttributes = ( timeRange: savedSearch.timeRange, refreshInterval: savedSearch.refreshInterval, rowsPerPage: savedSearch.rowsPerPage, + breakdownField: savedSearch.breakdownField, }); diff --git a/src/plugins/saved_search/public/services/saved_searches/types.ts b/src/plugins/saved_search/public/services/saved_searches/types.ts index 58972dd40bfb8..bb9051a7baf2d 100644 --- a/src/plugins/saved_search/public/services/saved_searches/types.ts +++ b/src/plugins/saved_search/public/services/saved_searches/types.ts @@ -45,6 +45,7 @@ export interface SavedSearchAttributes { refreshInterval?: RefreshInterval; rowsPerPage?: number; + breakdownField?: string; } /** @internal **/ @@ -80,4 +81,5 @@ export interface SavedSearch { refreshInterval?: RefreshInterval; rowsPerPage?: number; + breakdownField?: string; } diff --git a/src/plugins/saved_search/server/saved_objects/search.ts b/src/plugins/saved_search/server/saved_objects/search.ts index dec1c852aee20..14644f59538a6 100644 --- a/src/plugins/saved_search/server/saved_objects/search.ts +++ b/src/plugins/saved_search/server/saved_objects/search.ts @@ -67,6 +67,7 @@ export function getSavedSearchObjectType( }, }, rowsPerPage: { type: 'integer', index: false, doc_values: false }, + breakdownField: { type: 'keyword', index: false, doc_values: false }, }, }, migrations: () => getAllMigrations(getSearchSourceMigrations()), From fff2b03d2c4dcd99483b49bbe3899113ca07a7ac Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 8 Nov 2022 17:39:49 +0300 Subject: [PATCH 34/84] [Discover] improve tests --- .../layout/use_discover_histogram.ts | 19 ++++-- .../application/main/utils/fetch_all.test.ts | 30 ++++++--- .../application/main/utils/fetch_all.ts | 37 ++++++----- .../saved_searches/get_saved_searches.test.ts | 2 + .../public/chart/breakdown_field_selector.tsx | 1 + .../unified_histogram/public/chart/chart.tsx | 2 +- .../public/chart/histogram.tsx | 2 +- .../public/chart/use_total_hits.ts | 17 +++-- .../public/layout/layout.tsx | 2 +- .../apps/discover/group1/_discover.ts | 63 ------------------- .../discover/group1/_discover_histogram.ts | 61 ++++++++++++++++++ .../group1/_discover_histogram_breakdown.ts | 61 ++++++++++++++++++ test/functional/apps/discover/group1/index.ts | 1 + test/functional/page_objects/discover_page.ts | 18 ++++++ .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 17 files changed, 213 insertions(+), 109 deletions(-) create mode 100644 test/functional/apps/discover/group1/_discover_histogram_breakdown.ts diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index a21bd5340af2a..958288703e2e7 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -25,6 +25,7 @@ import type { AppState, GetStateReturn } from '../../services/discover_state'; import { FetchStatus } from '../../../types'; import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; import type { InspectorAdapters } from '../../hooks/use_inspector'; +import { sendErrorTo } from '../../utils/fetch_all'; export const CHART_HIDDEN_KEY = 'discover:chartHidden'; export const HISTOGRAM_HEIGHT_KEY = 'discover:histogramHeight'; @@ -51,7 +52,7 @@ export const useDiscoverHistogram = ({ inspectorAdapters: InspectorAdapters; searchSessionManager: DiscoverSearchSessionManager; }) => { - const { storage } = useDiscoverServices(); + const { storage, data } = useDiscoverServices(); /** * Visualize @@ -133,10 +134,20 @@ export const useDiscoverHistogram = ({ * Total hits */ + const sendTotalHitsError = useMemo( + () => sendErrorTo(data, savedSearchData$.totalHits$), + [data, savedSearchData$.totalHits$] + ); + const onTotalHitsChange = useCallback( - (status: UnifiedHistogramFetchStatus, totalHits?: number) => { + (status: UnifiedHistogramFetchStatus, result?: number | Error) => { const { fetchStatus, recordRawType } = savedSearchData$.totalHits$.getValue(); + if (result instanceof Error) { + sendTotalHitsError(result); + return; + } + // If we have a partial result already, we don't // want to update the total hits back to loading if (fetchStatus === FetchStatus.PARTIAL && status === UnifiedHistogramFetchStatus.loading) { @@ -145,11 +156,11 @@ export const useDiscoverHistogram = ({ savedSearchData$.totalHits$.next({ fetchStatus: status.toString() as FetchStatus, - result: totalHits, + result, recordRawType, }); }, - [savedSearchData$.totalHits$] + [savedSearchData$.totalHits$, sendTotalHitsError] ); const { fetchStatus: hitsFetchStatus, result: hitsTotal } = useDataState( diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 1c4aa235afea0..43a4fc0cafa71 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -20,12 +20,12 @@ import { DataDocumentsMsg, DataMainMsg, DataTotalHitsMsg, + RecordRawType, SavedSearchData, } from '../hooks/use_saved_search'; import { fetchDocuments } from './fetch_documents'; import { fetchSql } from './fetch_sql'; -import { fetchTotalHits } from './fetch_total_hits'; import { buildDataTableRecord } from '../../../utils/build_data_record'; import { dataViewMock } from '../../../__mocks__/data_view'; @@ -37,12 +37,7 @@ jest.mock('./fetch_sql', () => ({ fetchSql: jest.fn().mockResolvedValue([]), })); -jest.mock('./fetch_total_hits', () => ({ - fetchTotalHits: jest.fn(), -})); - const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction; -const mockFetchTotalHits = fetchTotalHits as unknown as jest.MockedFunction; const mockFetchSQL = fetchSql as unknown as jest.MockedFunction; function subjectCollector(subject: Subject): () => Promise { @@ -88,7 +83,6 @@ describe('test fetchAll', () => { mockFetchDocuments.mockReset().mockResolvedValue([]); mockFetchSQL.mockReset().mockResolvedValue([]); - mockFetchTotalHits.mockReset().mockResolvedValue(42); }); test('changes of fetchStatus when starting with FetchStatus.UNINITIALIZED', async () => { @@ -135,8 +129,12 @@ describe('test fetchAll', () => { const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchDocuments.mockResolvedValue(documents); - mockFetchTotalHits.mockResolvedValue(42); await fetchAll(subjects, searchSource, false, deps); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: RecordRawType.DOCUMENT, + result: 42, + }); expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, @@ -149,24 +147,36 @@ describe('test fetchAll', () => { const collect = subjectCollector(subjects.totalHits$); searchSource.getField('index')!.isTimeBased = () => true; await fetchAll(subjects, searchSource, false, deps); + + subjects.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: RecordRawType.DOCUMENT, + result: 32, + }); + expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document', result: 0 }, // From documents query { fetchStatus: FetchStatus.COMPLETE, recordRawType: 'document', result: 32 }, ]); - expect(mockFetchTotalHits).not.toHaveBeenCalled(); }); test('should only fail totalHits$ query not main$ for error from that query', async () => { const collectTotalHits = subjectCollector(subjects.totalHits$); const collectMain = subjectCollector(subjects.main$); searchSource.getField('index')!.isTimeBased = () => false; - mockFetchTotalHits.mockRejectedValue({ msg: 'Oh noes!' }); const hits = [{ _id: '1', _index: 'logs' }]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchDocuments.mockResolvedValue(documents); await fetchAll(subjects, searchSource, false, deps); + + subjects.totalHits$.next({ + fetchStatus: FetchStatus.ERROR, + recordRawType: RecordRawType.DOCUMENT, + error: { msg: 'Oh noes!' } as unknown as Error, + }); + expect(await collectTotalHits()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 82cf4712848b3..348013ed547fb 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -43,6 +43,25 @@ export interface FetchDeps { useNewFieldsApi: boolean; } +/** + * Method to create an error handler that will forward the received error + * to the specified subjects. It will ignore AbortErrors and will use the data + * plugin to show a toast for the error (e.g. allowing better insights into shard failures). + */ +export const sendErrorTo = ( + data: DataPublicPluginStart, + ...errorSubjects: Array +) => { + return (error: Error) => { + if (error instanceof Error && error.name === 'AbortError') { + return; + } + + data.search.showError(error); + errorSubjects.forEach((subject) => sendErrorMsg(subject, error)); + }; +}; + /** * This function starts fetching all required queries in Discover. This will be the query to load the individual * documents as well as any other requests that might be required to load the main view. @@ -58,22 +77,6 @@ export function fetchAll( ): Promise { const { initialFetchStatus, appStateContainer, services, useNewFieldsApi, data } = fetchDeps; - /** - * Method to create an error handler that will forward the received error - * to the specified subjects. It will ignore AbortErrors and will use the data - * plugin to show a toast for the error (e.g. allowing better insights into shard failures). - */ - const sendErrorTo = (...errorSubjects: Array) => { - return (error: Error) => { - if (error instanceof Error && error.name === 'AbortError') { - return; - } - - data.search.showError(error); - errorSubjects.forEach((subject) => sendErrorMsg(subject, error)); - }; - }; - try { const dataView = searchSource.getField('index')!; if (reset) { @@ -145,7 +148,7 @@ export function fetchAll( // Only the document query should send its errors to main$, to cause the full Discover app // to get into an error state. The other queries will not cause all of Discover to error out // but their errors will be shown in-place (e.g. of the chart). - .catch(sendErrorTo(dataSubjects.documents$, dataSubjects.main$)); + .catch(sendErrorTo(data, dataSubjects.documents$, dataSubjects.main$)); // Return a promise that will resolve once all the requests have finished or failed return documents.then(() => { diff --git a/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts b/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts index ca405537c363d..8e854a54947f8 100644 --- a/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts +++ b/src/plugins/saved_search/public/services/saved_searches/get_saved_searches.test.ts @@ -100,6 +100,7 @@ describe('getSavedSearch', () => { expect(savedObjectsClient.resolve).toHaveBeenCalled(); expect(savedSearch).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "_source", ], @@ -197,6 +198,7 @@ describe('getSavedSearch', () => { expect(savedObjectsClient.resolve).toHaveBeenCalled(); expect(savedSearch).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "_source", ], diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index cfb05ecfa785d..b34192604d24e 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -52,6 +52,7 @@ export const BreakdownFieldSelector = ({ return ( void; onTimeIntervalChange?: (timeInterval: string) => void; onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; - onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 7828bd6389b65..6eaefc40c49cc 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -40,7 +40,7 @@ export interface HistogramProps { filters: Filter[]; query: Query | AggregateQuery; timeRange: TimeRange; - onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.ts index 65308a75190b2..af4fca24dd3d6 100644 --- a/src/plugins/unified_histogram/public/chart/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.ts @@ -12,7 +12,7 @@ import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { cloneDeep, isEqual } from 'lodash'; import { MutableRefObject, useEffect, useRef } from 'react'; -import { filter, lastValueFrom, map } from 'rxjs'; +import { catchError, filter, lastValueFrom, map, of } from 'rxjs'; import { UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, @@ -41,7 +41,7 @@ export const useTotalHits = ({ filters: Filter[]; query: Query | AggregateQuery; timeRange: TimeRange; - onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; }) => { const abortController = useRef(); const totalHitsDeps = useRef>(); @@ -150,7 +150,7 @@ const fetchTotalHits = async ({ filters: Filter[]; query: Query | AggregateQuery; timeRange: TimeRange; - onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; }) => { abortController.current?.abort(); abortController.current = undefined; @@ -217,10 +217,15 @@ const fetchTotalHits = async ({ }) .pipe( filter((res) => isCompleteResponse(res)), - map((res) => res.rawResponse.hits.total as number) + map((res) => res.rawResponse.hits.total as number), + catchError((error: Error) => of(error)) ); - const totalHits = await lastValueFrom(fetch$); + const result = await lastValueFrom(fetch$); - onTotalHitsChange?.(UnifiedHistogramFetchStatus.complete, totalHits); + const resultStatus = + result instanceof Error + ? UnifiedHistogramFetchStatus.error + : UnifiedHistogramFetchStatus.complete; + onTotalHitsChange?.(resultStatus, result); }; diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index e58271104ee17..2c8f92101b15e 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -84,7 +84,7 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * Callback to update the total hits -- should set {@link UnifiedHistogramHitsContext.status} to status * and {@link UnifiedHistogramHitsContext.total} to totalHits */ - onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, totalHits?: number) => void; + onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; /** * Called when the histogram loading status changes */ diff --git a/test/functional/apps/discover/group1/_discover.ts b/test/functional/apps/discover/group1/_discover.ts index b67126ff2c8cb..1cba5aa4812d8 100644 --- a/test/functional/apps/discover/group1/_discover.ts +++ b/test/functional/apps/discover/group1/_discover.ts @@ -18,7 +18,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); const inspector = getService('inspector'); - const elasticChart = getService('elasticChart'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); @@ -106,48 +105,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should modify the time range when the histogram is brushed', async function () { - // this is the number of renderings of the histogram needed when new data is fetched - // this needs to be improved - const renderingCountInc = 1; - const prevRenderingCount = await elasticChart.getVisualizationRenderingCount(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await retry.waitFor('chart rendering complete', async () => { - const actualCount = await elasticChart.getVisualizationRenderingCount(); - const expectedCount = prevRenderingCount + renderingCountInc; - log.debug( - `renderings before brushing - actual: ${actualCount} expected: ${expectedCount}` - ); - return actualCount === expectedCount; - }); - let prevRowData = ''; - // to make sure the table is already rendered - await retry.try(async () => { - prevRowData = await PageObjects.discover.getDocTableField(1); - log.debug(`The first timestamp value in doc table before brushing: ${prevRowData}`); - }); - - await PageObjects.discover.brushHistogram(); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await retry.waitFor('chart rendering complete after being brushed', async () => { - const actualCount = await elasticChart.getVisualizationRenderingCount(); - const expectedCount = prevRenderingCount + renderingCountInc * 2; - log.debug( - `renderings after brushing - actual: ${actualCount} expected: ${expectedCount}` - ); - return actualCount === expectedCount; - }); - const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(Math.round(newDurationHours)).to.be(26); - - await retry.waitFor('doc table containing the documents of the brushed range', async () => { - const rowData = await PageObjects.discover.getDocTableField(1); - log.debug(`The first timestamp value in doc table after brushing: ${rowData}`); - return prevRowData !== rowData; - }); - }); - it('should show correct initial chart interval of Auto', async function () { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); @@ -265,26 +222,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('empty query', function () { - it('should update the histogram timerange when the query is resubmitted', async function () { - await kibanaServer.uiSettings.update({ - 'timepicker:timeDefaults': '{ "from": "2015-09-18T19:37:13.000Z", "to": "now"}', - }); - await PageObjects.common.navigateToApp('discover'); - await PageObjects.header.awaitKibanaChrome(); - const initialTimeString = await PageObjects.discover.getChartTimespan(); - await queryBar.submitQuery(); - - await retry.waitFor('chart timespan to have changed', async () => { - const refreshedTimeString = await PageObjects.discover.getChartTimespan(); - log.debug( - `Timestamp before: ${initialTimeString}, Timestamp after: ${refreshedTimeString}` - ); - return refreshedTimeString !== initialTimeString; - }); - }); - }); - describe('managing fields', function () { it('should add a field, sort by it, remove it and also sorting by it', async function () { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); diff --git a/test/functional/apps/discover/group1/_discover_histogram.ts b/test/functional/apps/discover/group1/_discover_histogram.ts index 12effb75cb7f3..99775893c8b40 100644 --- a/test/functional/apps/discover/group1/_discover_histogram.ts +++ b/test/functional/apps/discover/group1/_discover_histogram.ts @@ -23,6 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); + const log = getService('log'); + const queryBar = getService('queryBar'); describe('discover histogram', function describeIndexTests() { before(async () => { @@ -52,6 +54,65 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } } + it('should modify the time range when the histogram is brushed', async function () { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + // this is the number of renderings of the histogram needed when new data is fetched + const renderingCountInc = 1; + const prevRenderingCount = await elasticChart.getVisualizationRenderingCount(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await retry.waitFor('chart rendering complete', async () => { + const actualCount = await elasticChart.getVisualizationRenderingCount(); + const expectedCount = prevRenderingCount + renderingCountInc; + log.debug(`renderings before brushing - actual: ${actualCount} expected: ${expectedCount}`); + return actualCount === expectedCount; + }); + let prevRowData = ''; + // to make sure the table is already rendered + await retry.try(async () => { + prevRowData = await PageObjects.discover.getDocTableField(1); + log.debug(`The first timestamp value in doc table before brushing: ${prevRowData}`); + }); + + await PageObjects.discover.brushHistogram(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await retry.waitFor('chart rendering complete after being brushed', async () => { + const actualCount = await elasticChart.getVisualizationRenderingCount(); + const expectedCount = prevRenderingCount + renderingCountInc * 2; + log.debug(`renderings after brushing - actual: ${actualCount} expected: ${expectedCount}`); + return actualCount === expectedCount; + }); + const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(Math.round(newDurationHours)).to.be(26); + + await retry.waitFor('doc table containing the documents of the brushed range', async () => { + const rowData = await PageObjects.discover.getDocTableField(1); + log.debug(`The first timestamp value in doc table after brushing: ${rowData}`); + return prevRowData !== rowData; + }); + }); + + it('should update the histogram timerange when the query is resubmitted', async function () { + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': '{ "from": "2015-09-18T19:37:13.000Z", "to": "now"}', + }); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.awaitKibanaChrome(); + const initialTimeString = await PageObjects.discover.getChartTimespan(); + await queryBar.submitQuery(); + + await retry.waitFor('chart timespan to have changed', async () => { + const refreshedTimeString = await PageObjects.discover.getChartTimespan(); + log.debug( + `Timestamp before: ${initialTimeString}, Timestamp after: ${refreshedTimeString}` + ); + return refreshedTimeString !== initialTimeString; + }); + }); + it('should visualize monthly data with different day intervals', async () => { const from = 'Nov 1, 2017 @ 00:00:00.000'; const to = 'Mar 21, 2018 @ 00:00:00.000'; diff --git a/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts b/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts new file mode 100644 index 0000000000000..f83a76c45954b --- /dev/null +++ b/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const filterBar = getService('filterBar'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + + describe('discover histogram breakdown', function describeIndexTests() { + before(async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + + it('should choose breakdown field', async () => { + await PageObjects.discover.chooseBreakdownField('extension.raw'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['png', 'css', 'jpg']); + }); + + it('should add filter using histogram legend values', async () => { + await PageObjects.discover.clickLegendFilter('png', '+'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await filterBar.hasFilter('extension.raw', 'png')).to.be(true); + }); + + it('should save breakdown field in saved search', async () => { + await filterBar.removeFilter('extension.raw'); + await PageObjects.discover.saveSearch('with breakdown'); + + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const prevList = await PageObjects.discover.getHistogramLegendList(); + expect(prevList).to.eql([]); + + await PageObjects.discover.loadSavedSearch('with breakdown'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['png', 'css', 'jpg']); + }); + }); +} diff --git a/test/functional/apps/discover/group1/index.ts b/test/functional/apps/discover/group1/index.ts index ab6798400b7a2..82fd341ccce04 100644 --- a/test/functional/apps/discover/group1/index.ts +++ b/test/functional/apps/discover/group1/index.ts @@ -23,6 +23,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_no_data')); loadTestFile(require.resolve('./_discover')); loadTestFile(require.resolve('./_discover_accessibility')); + loadTestFile(require.resolve('./_discover_histogram_breakdown')); loadTestFile(require.resolve('./_discover_histogram')); loadTestFile(require.resolve('./_doc_accessibility')); loadTestFile(require.resolve('./_filter_editor')); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 44a29441d2707..36290cf49190c 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrService } from '../ftr_provider_context'; +import { WebElementWrapper } from '../services/lib/web_element_wrapper'; export class DiscoverPageObject extends FtrService { private readonly retry = this.ctx.getService('retry'); @@ -25,6 +26,7 @@ export class DiscoverPageObject extends FtrService { private readonly kibanaServer = this.ctx.getService('kibanaServer'); private readonly fieldEditor = this.ctx.getService('fieldEditor'); private readonly queryBar = this.ctx.getService('queryBar'); + private readonly comboBox = this.ctx.getService('comboBox'); private readonly defaultFindTimeout = this.config.get('timeouts.find'); @@ -202,6 +204,22 @@ export class DiscoverPageObject extends FtrService { ); } + public async chooseBreakdownField(field: string) { + await this.comboBox.set('unifiedHistogramBreakdownFieldSelector', field); + } + + public async getHistogramLegendList() { + const unifiedHistogram = await this.testSubjects.find('unifiedHistogramChart'); + const list = await unifiedHistogram.findAllByClassName('echLegendItem__label'); + return Promise.all(list.map((elem: WebElementWrapper) => elem.getVisibleText())); + } + + public async clickLegendFilter(field: string, type: '+' | '-') { + const filterType = type === '+' ? 'filterIn' : 'filterOut'; + await this.testSubjects.click(`legend-${field}`); + await this.testSubjects.click(`legend-${field}-${filterType}`); + } + public async getCurrentQueryName() { return await this.globalNav.getLastBreadcrumb(); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c42c82362e468..4276e29bb9826 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2219,7 +2219,6 @@ "discover.grid.viewDoc": "Afficher/Masquer les détails de la boîte de dialogue", "discover.gridSampleSize.advancedSettingsLinkLabel": "Paramètres avancés", "discover.helpMenu.appName": "Découverte", - "discover.inspectorRequestDataTitleChart": "Données du graphique", "discover.inspectorRequestDataTitleDocuments": "Documents", "discover.inspectorRequestDescriptionDocument": "Cette requête interroge Elasticsearch afin de récupérer les documents.", "discover.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", @@ -2269,7 +2268,6 @@ "discover.sampleData.viewLinkLabel": "Découverte", "discover.savedSearch.savedObjectName": "Recherche enregistrée", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "Ouvrir dans Discover", - "discover.searchingTitle": "Recherche", "discover.selectColumnHeader": "Sélectionner la colonne", "discover.showAllDocuments": "Afficher tous les documents", "discover.showErrorMessageAgain": "Afficher le message d'erreur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b3ab75165960f..866f88d818348 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2215,7 +2215,6 @@ "discover.grid.viewDoc": "詳細ダイアログを切り替え", "discover.gridSampleSize.advancedSettingsLinkLabel": "高度な設定", "discover.helpMenu.appName": "Discover", - "discover.inspectorRequestDataTitleChart": "グラフデータ", "discover.inspectorRequestDataTitleDocuments": "ドキュメント", "discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、ドキュメントを取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", @@ -2265,7 +2264,6 @@ "discover.sampleData.viewLinkLabel": "Discover", "discover.savedSearch.savedObjectName": "保存検索", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "Discoverで開く", - "discover.searchingTitle": "検索中", "discover.selectColumnHeader": "列を選択", "discover.showAllDocuments": "すべてのドキュメントを表示", "discover.showErrorMessageAgain": "エラーメッセージを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1adc45ea80d44..de482c8782f68 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2219,7 +2219,6 @@ "discover.grid.viewDoc": "切换具有详情的对话框", "discover.gridSampleSize.advancedSettingsLinkLabel": "高级设置", "discover.helpMenu.appName": "Discover", - "discover.inspectorRequestDataTitleChart": "图表数据", "discover.inspectorRequestDataTitleDocuments": "文档", "discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取文档。", "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", @@ -2269,7 +2268,6 @@ "discover.sampleData.viewLinkLabel": "Discover", "discover.savedSearch.savedObjectName": "已保存搜索", "discover.savedSearchEmbeddable.action.viewSavedSearch.displayName": "在 Discover 中打开", - "discover.searchingTitle": "正在搜索", "discover.selectColumnHeader": "选择列", "discover.showAllDocuments": "显示所有文档", "discover.showErrorMessageAgain": "显示错误消息", From 042c700f6d3947f6822fe925a77148051b4c74e0 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 8 Nov 2022 18:47:33 -0400 Subject: [PATCH 35/84] [Discover] Make fetchAll wait on unified histogram to finish loading as well --- .../application/main/utils/fetch_all.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 348013ed547fb..513f2f85e33e7 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -9,6 +9,7 @@ import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; import { Adapters } from '@kbn/inspector-plugin/common'; import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; +import { BehaviorSubject, filter, firstValueFrom, map, merge, scan } from 'rxjs'; import { getRawRecordType } from './get_raw_record_type'; import { sendCompleteMsg, @@ -25,6 +26,7 @@ import { FetchStatus } from '../../types'; import { DataDocuments$, DataMain$, + DataMsg, RecordRawType, SavedSearchData, } from '../hooks/use_saved_search'; @@ -123,7 +125,6 @@ export function fetchAll( }; // Handle results of the individual queries and forward the results to the corresponding dataSubjects - documents .then((docs) => { // If the total hits (or chart) query is still loading, emit a partial @@ -151,7 +152,12 @@ export function fetchAll( .catch(sendErrorTo(data, dataSubjects.documents$, dataSubjects.main$)); // Return a promise that will resolve once all the requests have finished or failed - return documents.then(() => { + return firstValueFrom( + merge( + fetchStatusByType(dataSubjects.documents$, 'documents'), + fetchStatusByType(dataSubjects.totalHits$, 'totalHits') + ).pipe(scan(toRequestFinishedMap, {}), filter(allRequestsFinished)) + ).then(() => { // Send a complete message to main$ once all queries are done and if main$ // is not already in an ERROR state, e.g. because the document query has failed. // This will only complete main$, if it hasn't already been completed previously @@ -166,3 +172,17 @@ export function fetchAll( return Promise.resolve(); } } + +const fetchStatusByType = (subject: BehaviorSubject, type: string) => + subject.pipe(map(({ fetchStatus }) => ({ type, fetchStatus }))); + +const toRequestFinishedMap = ( + currentMap: Record, + { type, fetchStatus }: { type: string; fetchStatus: FetchStatus } +) => ({ + ...currentMap, + [type]: [FetchStatus.COMPLETE, FetchStatus.ERROR].includes(fetchStatus), +}); + +const allRequestsFinished = (requests: Record) => + Object.values(requests).every((finished) => finished); From 43c00f6e95abe3bdc9d17918eec2e602837ead51 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 8 Nov 2022 18:59:24 -0400 Subject: [PATCH 36/84] [Discover] Include 'other' bucket in unified histogram --- .../unified_histogram/public/chart/get_lens_attributes.ts | 2 +- src/plugins/unified_histogram/public/layout/layout.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts index 4a5d1fced183b..d1eb7313ab5a5 100644 --- a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -91,7 +91,7 @@ export const getLensAttributes = ({ columnId: 'count_column', }, orderDirection: 'desc', - otherBucket: false, + otherBucket: true, missingBucket: false, parentFormat: { id: 'terms', diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index 2c8f92101b15e..bc4ebb054b0f4 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -126,7 +126,6 @@ export const UnifiedHistogramLayout = ({ const showFixedPanels = isMobile || !chart || chart.hidden; const { euiTheme } = useEuiTheme(); const defaultTopPanelHeight = euiTheme.base * 12; - const minTopPanelHeight = euiTheme.base * 11; const minMainPanelHeight = euiTheme.base * 10; const chartClassName = @@ -180,7 +179,7 @@ export const UnifiedHistogramLayout = ({ mode={panelsMode} resizeRef={resizeRef} topPanelHeight={currentTopPanelHeight} - minTopPanelHeight={minTopPanelHeight} + minTopPanelHeight={defaultTopPanelHeight} minMainPanelHeight={minMainPanelHeight} topPanel={} mainPanel={} From ef02c447e49fa2d2f2e6e0e7f770cf0b8b463a96 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 8 Nov 2022 19:53:40 -0400 Subject: [PATCH 37/84] [Discover] Skip first lastReloadRequestTime effect --- .../main/components/layout/use_discover_histogram.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 958288703e2e7..4121084f1eb60 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -12,7 +12,7 @@ import { getVisualizeInformation, triggerVisualizeActions, } from '@kbn/unified-field-list-plugin/public'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; import useDebounce from 'react-use/lib/useDebounce'; import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; @@ -241,14 +241,17 @@ export const useDiscoverHistogram = ({ const [lastReloadRequestTime, setLastReloadRequestTime] = useState(0); const { fetchStatus: mainFetchStatus } = useDataState(savedSearchData$.main$); + const firstRun = useRef(true); // Reload unified histogram when a refetch is triggered, // with a debounce to avoid multiple requests const [, cancelDebounce] = useDebounce( () => { - if (mainFetchStatus === FetchStatus.LOADING) { + if (mainFetchStatus === FetchStatus.LOADING && !firstRun.current) { setLastReloadRequestTime(Date.now()); } + + firstRun.current = false; }, 100, [mainFetchStatus] From 801e6a371989a50819d6b57e26d254864a2e4dd6 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 9 Nov 2022 09:47:37 -0400 Subject: [PATCH 38/84] [Discover] Hide breakdown if undefined --- .../main/components/layout/use_discover_histogram.ts | 2 +- .../public/chart/breakdown_field_selector.tsx | 4 ++-- src/plugins/unified_histogram/public/chart/chart.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 4121084f1eb60..6268a6a9c0ddc 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -231,7 +231,7 @@ export const useDiscoverHistogram = ({ ); const breakdown = useMemo( - () => (isPlainRecord || !isTimeBased || !field ? undefined : { field }), + () => (isPlainRecord || !isTimeBased ? undefined : { field }), [field, isPlainRecord, isTimeBased] ); diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index b34192604d24e..9bcdf5c12e899 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -16,7 +16,7 @@ import { fieldSupportsBreakdown } from './field_supports_breakdown'; export interface BreakdownFieldSelectorProps { dataView: DataView; - breakdown?: UnifiedHistogramBreakdownContext; + breakdown: UnifiedHistogramBreakdownContext; onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; } @@ -30,7 +30,7 @@ export const BreakdownFieldSelector = ({ .map((field) => ({ label: field.displayName, value: field.name })) .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); - const selectedFields = breakdown?.field + const selectedFields = breakdown.field ? [{ label: breakdown.field.displayName, value: breakdown.field.name }] : []; diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 3d999b8c3505f..ad0ea40c30866 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -159,7 +159,7 @@ export function Chart({ justifyContent="flexEnd" css={breakdownFieldSelectorGroupCss} > - {chartVisible && ( + {chartVisible && breakdown && ( Date: Wed, 9 Nov 2022 09:52:01 -0400 Subject: [PATCH 39/84] [Discover] Use search session to determine when to fetch unified histogram --- .../layout/discover_main_content.tsx | 7 ++-- .../layout/use_discover_histogram.ts | 40 +++++-------------- .../services/discover_search_session.test.ts | 1 - .../main/services/discover_search_session.ts | 15 ++----- 4 files changed, 17 insertions(+), 46 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 81d59d2f8c249..815b6eb87b683 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -96,8 +96,8 @@ export const DiscoverMainContent = ({ ); const { + shouldRender, topPanelHeight, - lastReloadRequestTime, request, hits, chart, @@ -121,12 +121,11 @@ export const DiscoverMainContent = ({ searchSessionManager, }); - return ( + return shouldRender ? ( + ) : ( + <> ); }; diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 6268a6a9c0ddc..c7e3cffbe06dc 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -12,10 +12,10 @@ import { getVisualizeInformation, triggerVisualizeActions, } from '@kbn/unified-field-list-plugin/public'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; -import useDebounce from 'react-use/lib/useDebounce'; import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; +import useObservable from 'react-use/lib/useObservable'; import { getUiActions } from '../../../../kibana_services'; import { PLUGIN_ID } from '../../../../../common'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; @@ -121,7 +121,9 @@ export const useDiscoverHistogram = ({ * Request */ - const searchSessionId = searchSessionManager.getLastSearchSessionId(); + // The searchSessionId will be updated whenever a new search + // is started and will trigger a unified histogram refetch + const searchSessionId = useObservable(searchSessionManager.searchSessionId$); const request = useMemo( () => ({ searchSessionId, @@ -235,36 +237,12 @@ export const useDiscoverHistogram = ({ [field, isPlainRecord, isTimeBased] ); - /** - * Reload - */ - - const [lastReloadRequestTime, setLastReloadRequestTime] = useState(0); - const { fetchStatus: mainFetchStatus } = useDataState(savedSearchData$.main$); - const firstRun = useRef(true); - - // Reload unified histogram when a refetch is triggered, - // with a debounce to avoid multiple requests - const [, cancelDebounce] = useDebounce( - () => { - if (mainFetchStatus === FetchStatus.LOADING && !firstRun.current) { - setLastReloadRequestTime(Date.now()); - } - - firstRun.current = false; - }, - 100, - [mainFetchStatus] - ); - - // A refetch is triggered when the data view is changed, - // but we don't want to reload unified histogram in this case, - // so cancel the debounced effect on unmount - useEffect(() => cancelDebounce, [cancelDebounce]); - return { + // The histogram layout shouldn't render until the first search + // request has started, or an immediate refetch of the histogram + // will be triggered when the searchSessionId is set + shouldRender: Boolean(searchSessionId), topPanelHeight, - lastReloadRequestTime, request, hits, chart, diff --git a/src/plugins/discover/public/application/main/services/discover_search_session.test.ts b/src/plugins/discover/public/application/main/services/discover_search_session.test.ts index d1d1fb398727c..0f854438b6749 100644 --- a/src/plugins/discover/public/application/main/services/discover_search_session.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.test.ts @@ -28,7 +28,6 @@ describe('DiscoverSearchSessionManager', () => { const id = searchSessionManager.getNextSearchSessionId(); expect(id).toEqual(nextId); expect(session.start).toBeCalled(); - expect(searchSessionManager.getLastSearchSessionId()).toEqual(id); }); test('restores a session using query param from the URL', () => { diff --git a/src/plugins/discover/public/application/main/services/discover_search_session.ts b/src/plugins/discover/public/application/main/services/discover_search_session.ts index e91238b8a1c47..0cbaf74159a80 100644 --- a/src/plugins/discover/public/application/main/services/discover_search_session.ts +++ b/src/plugins/discover/public/application/main/services/discover_search_session.ts @@ -31,8 +31,9 @@ export class DiscoverSearchSessionManager { * skips if `searchSessionId` matches current search session id */ readonly newSearchSessionIdFromURL$: Rx.Observable; + readonly searchSessionId$: Rx.Observable; + private readonly deps: DiscoverSearchSessionManagerDeps; - private lastSearchSessionId?: string; constructor(deps: DiscoverSearchSessionManagerDeps) { this.deps = deps; @@ -45,6 +46,7 @@ export class DiscoverSearchSessionManager { return !this.deps.session.isCurrentSession(searchSessionId); }) ); + this.searchSessionId$ = this.deps.session.getSession$(); } /** @@ -66,16 +68,7 @@ export class DiscoverSearchSessionManager { } } - this.lastSearchSessionId = searchSessionIdFromURL ?? this.deps.session.start(); - - return this.lastSearchSessionId; - } - - /** - * Get the last returned session id by {@link getNextSearchSessionId} - */ - getLastSearchSessionId() { - return this.lastSearchSessionId; + return searchSessionIdFromURL ?? this.deps.session.start(); } /** From 0e6de80c58a19ccd8dd2c88271b1b5b71cbd3fdd Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 9 Nov 2022 10:24:54 -0400 Subject: [PATCH 40/84] [Discover] Prevent hits.total changes triggering a Lens refetch --- .../unified_histogram/public/chart/histogram.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 6eaefc40c49cc..e7141c749f784 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -70,13 +70,21 @@ export function Histogram({ timeInterval, }); + // Keep track of previous hits in a ref to avoid recreating the + // onLoad callback when the hits change, which triggers a Lens reload + const previousHits = useRef(hits?.total); + + useEffect(() => { + previousHits.current = hits?.total; + }, [hits?.total]); + const onLoad = useCallback( (isLoading: boolean, adapters: Partial | undefined) => { const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount; onTotalHitsChange?.( isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete, - totalHits ?? hits?.total + totalHits ?? previousHits.current ); const lensRequest = adapters?.requests?.getRequests()[0]; @@ -96,7 +104,7 @@ export function Histogram({ onChartLoad?.({ complete: !isLoading, adapters: adapters ?? {} }); }, - [data, dataView, hits?.total, onChartLoad, onTotalHitsChange, timeInterval] + [data, dataView, onChartLoad, onTotalHitsChange, timeInterval] ); const { euiTheme } = useEuiTheme(); From 4ca75e30d4d758cafaa0bfe737716cb16e33120c Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 9 Nov 2022 10:41:12 -0400 Subject: [PATCH 41/84] [Discover] Use refetchId to determine when unified histogram should refetch, and add request debouncing to dedupe requests --- .../public/chart/build_bucket_interval.ts | 5 +- .../unified_histogram/public/chart/chart.tsx | 32 ++++- .../unified_histogram/public/chart/consts.ts | 9 ++ .../public/chart/get_chart_agg_configs.ts | 5 +- .../public/chart/histogram.tsx | 70 +++++++--- .../public/chart/use_refetch_id.ts | 122 ++++++++++++++++++ .../public/chart/use_request_params.tsx | 11 +- .../public/chart/use_total_hits.ts | 110 ++++------------ 8 files changed, 253 insertions(+), 111 deletions(-) create mode 100644 src/plugins/unified_histogram/public/chart/consts.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_refetch_id.ts diff --git a/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts b/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts index 84e286cfc05ee..b3c9671662dbc 100644 --- a/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts +++ b/src/plugins/unified_histogram/public/chart/build_bucket_interval.ts @@ -9,6 +9,7 @@ import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { DataPublicPluginStart, search, tabifyAggResponse } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; +import type { TimeRange } from '@kbn/es-query'; import type { UnifiedHistogramBucketInterval } from '../types'; import { getChartAggConfigs } from './get_chart_agg_configs'; @@ -21,18 +22,20 @@ export const buildBucketInterval = ({ data, dataView, timeInterval, + timeRange, response, }: { data: DataPublicPluginStart; dataView: DataView; timeInterval?: string; + timeRange: TimeRange; response?: SearchResponse; }) => { if (!timeInterval || !response) { return {}; } - const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, data }); + const chartAggConfigs = getChartAggConfigs({ dataView, timeInterval, timeRange, data }); const bucketAggConfig = chartAggConfigs.aggs[1]; tabifyAggResponse(chartAggConfigs, response); diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index ad0ea40c30866..2df0669f0d09e 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { ReactElement } from 'react'; +import { ReactElement, useMemo } from 'react'; import React, { memo } from 'react'; import { EuiButtonIcon, @@ -35,6 +35,7 @@ import { useTotalHits } from './use_total_hits'; import { useRequestParams } from './use_request_params'; import { useChartStyles } from './use_chart_styles'; import { useChartActions } from './use_chart_actions'; +import { useRefetchId } from './use_refetch_id'; export interface ChartProps { className?: string; @@ -104,22 +105,45 @@ export function Chart({ dataView.isTimeBased() ); - const { filters, query, timeRange } = useRequestParams({ + const { filters, query, relativeTimeRange } = useRequestParams({ services, lastReloadRequestTime, request, }); + const refetchId = useRefetchId({ + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + relativeTimeRange, + }); + + // We need to update the absolute time range whenever the refetchId changes + const timeRange = useMemo( + () => services.data.query.timefilter.timefilter.getAbsoluteTime(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [services.data.query.timefilter.timefilter, refetchId] + ); + useTotalHits({ services, + dataView, lastReloadRequestTime, request, - chartVisible, hits, - dataView, + chart, + chartVisible, + breakdown, filters, query, timeRange, + refetchId, onTotalHitsChange, }); diff --git a/src/plugins/unified_histogram/public/chart/consts.ts b/src/plugins/unified_histogram/public/chart/consts.ts new file mode 100644 index 0000000000000..d2af2ed4ee33a --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/consts.ts @@ -0,0 +1,9 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export const REQUEST_DEBOUNCE_MS = 100; diff --git a/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts b/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts index 93ef7f3dd9188..d68330a22a45d 100644 --- a/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts +++ b/src/plugins/unified_histogram/public/chart/get_chart_agg_configs.ts @@ -8,6 +8,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; +import type { TimeRange } from '@kbn/es-query'; /** * Helper function to get the agg configs required for the unified histogram chart request @@ -15,10 +16,12 @@ import type { DataView } from '@kbn/data-views-plugin/common'; export function getChartAggConfigs({ dataView, timeInterval, + timeRange, data, }: { dataView: DataView; timeInterval: string; + timeRange: TimeRange; data: DataPublicPluginStart; }) { const visStateAggs = [ @@ -32,7 +35,7 @@ export function getChartAggConfigs({ params: { field: dataView.timeFieldName!, interval: timeInterval, - timeRange: data.query.timefilter.timefilter.getTime(), + timeRange, }, }, ]; diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index e7141c749f784..fb7d72e71683b 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -8,13 +8,15 @@ import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { useCallback, useMemo, useState } from 'react'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; import type { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import type { estypes } from '@elastic/elasticsearch'; import type { AggregateQuery, Query, Filter, TimeRange } from '@kbn/es-query'; +import useDebounce from 'react-use/lib/useDebounce'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { UnifiedHistogramBreakdownContext, UnifiedHistogramBucketInterval, @@ -28,6 +30,7 @@ import { import { getLensAttributes } from './get_lens_attributes'; import { buildBucketInterval } from './build_bucket_interval'; import { useTimeRange } from './use_time_range'; +import { REQUEST_DEBOUNCE_MS } from './consts'; export interface HistogramProps { services: UnifiedHistogramServices; @@ -96,6 +99,7 @@ export function Histogram({ data, dataView, timeInterval, + timeRange, response, }); @@ -104,7 +108,7 @@ export function Histogram({ onChartLoad?.({ complete: !isLoading, adapters: adapters ?? {} }); }, - [data, dataView, onChartLoad, onTotalHitsChange, timeInterval] + [data, dataView, onChartLoad, onTotalHitsChange, timeInterval, timeRange] ); const { euiTheme } = useEuiTheme(); @@ -128,24 +132,58 @@ export function Histogram({ } `; + const [debouncedProps, setDebouncedProps] = useState( + getLensProps({ + timeRange, + attributes, + request, + lastReloadRequestTime, + onLoad, + }) + ); + + useDebounce( + () => { + setDebouncedProps( + getLensProps({ timeRange, attributes, request, lastReloadRequestTime, onLoad }) + ); + }, + REQUEST_DEBOUNCE_MS, + [attributes, lastReloadRequestTime, onLoad, request?.searchSessionId, timeRange] + ); + return ( <>
- +
{timeRangeDisplay} ); } + +const getLensProps = ({ + timeRange, + attributes, + request, + lastReloadRequestTime, + onLoad, +}: { + timeRange: TimeRange; + attributes: TypedLensByValueInput['attributes']; + request: UnifiedHistogramRequestContext | undefined; + lastReloadRequestTime: number | undefined; + onLoad: (isLoading: boolean, adapters: Partial | undefined) => void; +}) => ({ + id: 'unifiedHistogramLensComponent', + viewMode: ViewMode.VIEW, + timeRange, + attributes, + noPadding: true, + searchSessionId: request?.searchSessionId, + executionContext: { + description: 'fetch chart data and total hits', + }, + lastReloadRequestTime, + onLoad, +}); diff --git a/src/plugins/unified_histogram/public/chart/use_refetch_id.ts b/src/plugins/unified_histogram/public/chart/use_refetch_id.ts new file mode 100644 index 0000000000000..357d6892c1745 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_refetch_id.ts @@ -0,0 +1,122 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { cloneDeep, isEqual } from 'lodash'; +import { useEffect, useRef, useState } from 'react'; +import type { + UnifiedHistogramBreakdownContext, + UnifiedHistogramChartContext, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, +} from '../types'; + +export const useRefetchId = ({ + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + relativeTimeRange: relativeTimeRange, +}: { + dataView: DataView; + lastReloadRequestTime: number | undefined; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + chart: UnifiedHistogramChartContext | undefined; + chartVisible: boolean; + breakdown: UnifiedHistogramBreakdownContext | undefined; + filters: Filter[]; + query: Query | AggregateQuery; + relativeTimeRange: TimeRange; +}) => { + const refetchDeps = useRef>(); + const [refetchId, setRefetchId] = useState(0); + + // When the unified histogram props change, we must compare the current subset + // that should trigger a histogram refetch against the previous subset. If they + // are different, we must refetch the histogram to ensure it's up to date. + useEffect(() => { + const newRefetchDeps = getRefetchDeps({ + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + relativeTimeRange, + }); + + if (!isEqual(refetchDeps.current, newRefetchDeps)) { + console.log(newRefetchDeps.filter((item, i) => !isEqual(item, refetchDeps.current?.[i]))); + if (refetchDeps.current) { + setRefetchId((id) => id + 1); + } + + refetchDeps.current = newRefetchDeps; + } + }, [ + breakdown, + chart, + chartVisible, + dataView, + filters, + hits, + lastReloadRequestTime, + query, + request, + relativeTimeRange, + ]); + + return refetchId; +}; + +const getRefetchDeps = ({ + dataView, + lastReloadRequestTime, + request, + hits, + chart, + chartVisible, + breakdown, + filters, + query, + relativeTimeRange, +}: { + dataView: DataView; + lastReloadRequestTime: number | undefined; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + chart: UnifiedHistogramChartContext | undefined; + chartVisible: boolean; + breakdown: UnifiedHistogramBreakdownContext | undefined; + filters: Filter[]; + query: Query | AggregateQuery; + relativeTimeRange: TimeRange; +}) => + cloneDeep([ + dataView.id, + lastReloadRequestTime, + request?.searchSessionId, + Boolean(hits), + chartVisible, + chart?.timeInterval, + Boolean(breakdown), + breakdown?.field, + filters, + query, + relativeTimeRange, + ]); diff --git a/src/plugins/unified_histogram/public/chart/use_request_params.tsx b/src/plugins/unified_histogram/public/chart/use_request_params.tsx index 533a92d1a3ec6..defa2bdd920d9 100644 --- a/src/plugins/unified_histogram/public/chart/use_request_params.tsx +++ b/src/plugins/unified_histogram/public/chart/use_request_params.tsx @@ -49,13 +49,10 @@ export const useRequestParams = ({ [data.query.queryString, queryState.query] ); - // We need to update the absolute time range whenever the relative - // time range changes, or when the lastReloadRequestTime changes - const timeRange = useMemo( - () => data.query.timefilter.timefilter.getAbsoluteTime(), - // eslint-disable-next-line react-hooks/exhaustive-deps - [data.query.timefilter.timefilter, queryState.time, lastReloadRequestTime] + const relativeTimeRange = useMemo( + () => queryState.time ?? data.query.timefilter.timefilter.getTimeDefaults(), + [data.query.timefilter.timefilter, queryState.time] ); - return { filters, query, timeRange }; + return { filters, query, relativeTimeRange }; }; diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.ts index af4fca24dd3d6..3f24b642c81bf 100644 --- a/src/plugins/unified_histogram/public/chart/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.ts @@ -10,132 +10,77 @@ import { isCompleteResponse } from '@kbn/data-plugin/public'; import { DataView, DataViewType } from '@kbn/data-views-plugin/public'; import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; -import { cloneDeep, isEqual } from 'lodash'; -import { MutableRefObject, useEffect, useRef } from 'react'; +import { MutableRefObject, useRef } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; import { catchError, filter, lastValueFrom, map, of } from 'rxjs'; import { + UnifiedHistogramBreakdownContext, + UnifiedHistogramChartContext, UnifiedHistogramFetchStatus, UnifiedHistogramHitsContext, UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; +import { REQUEST_DEBOUNCE_MS } from './consts'; export const useTotalHits = ({ services, + dataView, lastReloadRequestTime, request, - chartVisible, hits, - dataView, + chart, + chartVisible, + breakdown, filters, query, timeRange, + refetchId, onTotalHitsChange, }: { services: UnifiedHistogramServices; + dataView: DataView; lastReloadRequestTime: number | undefined; request: UnifiedHistogramRequestContext | undefined; - chartVisible: boolean; hits: UnifiedHistogramHitsContext | undefined; - dataView: DataView; + chart: UnifiedHistogramChartContext | undefined; + chartVisible: boolean; + breakdown: UnifiedHistogramBreakdownContext | undefined; filters: Filter[]; query: Query | AggregateQuery; timeRange: TimeRange; + refetchId: number; onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; }) => { const abortController = useRef(); - const totalHitsDeps = useRef>(); - - // When the unified histogram props change, we must compare the current subset - // that should trigger a total hits refetch against the previous subset. If they - // are different, we must refetch the total hits to ensure it's up to date with - // the chart. These are the props we care about: - // - chartVisible: - // We only need to fetch the total hits when the chart is hidden, - // otherwise Lens will be responsible for updating the display. - // - lastReloadRequestTime: A refetch has been manually triggered by the consumer. - // - hits: - // If the hits context is undefined, we don't need to fetch the - // total hits because the display will be hidden. - // - dataView: The current data view has changed. - // - filters: The current filters have changed. - // - query: The current query has been updated. - // - timeRange: The selected time range has changed. - useEffect(() => { - const newTotalHitsDeps = getTotalHitsDeps({ - chartVisible, - lastReloadRequestTime, - hits, - dataView, - filters, - query, - timeRange, - }); - - if (!isEqual(totalHitsDeps.current, newTotalHitsDeps)) { - totalHitsDeps.current = newTotalHitsDeps; + useDebounce( + () => { fetchTotalHits({ services, abortController, + dataView, request, - chartVisible, hits, - dataView, + chartVisible, filters, query, timeRange, onTotalHitsChange, }); - } - }, [ - chartVisible, - dataView, - filters, - hits, - lastReloadRequestTime, - onTotalHitsChange, - query, - request, - services, - timeRange, - ]); + }, + REQUEST_DEBOUNCE_MS, + [onTotalHitsChange, refetchId, services] + ); }; -const getTotalHitsDeps = ({ - chartVisible, - lastReloadRequestTime, - hits, - dataView, - filters, - query, - timeRange, -}: { - chartVisible: boolean; - lastReloadRequestTime: number | undefined; - hits: UnifiedHistogramHitsContext | undefined; - dataView: DataView; - filters: Filter[]; - query: Query | AggregateQuery; - timeRange: TimeRange; -}) => - cloneDeep([ - chartVisible, - Boolean(hits), - dataView.id, - filters, - query, - timeRange, - lastReloadRequestTime, - ]); - const fetchTotalHits = async ({ services: { data }, abortController, + dataView, request, - chartVisible, hits, - dataView, + chartVisible, filters: originalFilters, query, timeRange, @@ -143,10 +88,10 @@ const fetchTotalHits = async ({ }: { services: UnifiedHistogramServices; abortController: MutableRefObject; + dataView: DataView; request: UnifiedHistogramRequestContext | undefined; - chartVisible: boolean; hits: UnifiedHistogramHitsContext | undefined; - dataView: DataView; + chartVisible: boolean; filters: Filter[]; query: Query | AggregateQuery; timeRange: TimeRange; @@ -227,5 +172,6 @@ const fetchTotalHits = async ({ result instanceof Error ? UnifiedHistogramFetchStatus.error : UnifiedHistogramFetchStatus.complete; + onTotalHitsChange?.(resultStatus, result); }; From ff691954c43bc396e7e58f003bb05dcc99ee50a1 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 9 Nov 2022 18:52:18 +0300 Subject: [PATCH 42/84] [Discover] fix some tests --- .../application/main/utils/fetch_all.test.ts | 51 +++++++++++++++---- .../saved_searches_utils.test.ts | 2 + .../discover/group1/_discover_histogram.ts | 10 ++-- .../group1/_discover_histogram_breakdown.ts | 6 +-- .../apps/transform/creation_index_pattern.ts | 4 +- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 43a4fc0cafa71..a4f2a6085abb6 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -51,6 +51,8 @@ function subjectCollector(subject: Subject): () => Promise { }; } +const waitForNextTick = () => new Promise((resolve) => setTimeout(resolve, 0)); + describe('test fetchAll', () => { let subjects: SavedSearchData; let deps: Parameters[3]; @@ -90,7 +92,8 @@ describe('test fetchAll', () => { subjects.main$.subscribe((value) => stateArr.push(value.fetchStatus)); - await fetchAll(subjects, searchSource, false, deps); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); expect(stateArr).toEqual([ FetchStatus.UNINITIALIZED, @@ -107,7 +110,8 @@ describe('test fetchAll', () => { ]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchDocuments.mockResolvedValue(documents); - await fetchAll(subjects, searchSource, false, deps); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, @@ -129,12 +133,18 @@ describe('test fetchAll', () => { const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchDocuments.mockResolvedValue(documents); - await fetchAll(subjects, searchSource, false, deps); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.LOADING, + recordRawType: RecordRawType.DOCUMENT, + }); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); subjects.totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, recordRawType: RecordRawType.DOCUMENT, result: 42, }); + expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, @@ -146,8 +156,12 @@ describe('test fetchAll', () => { test('should use charts query to fetch total hit count when chart is visible', async () => { const collect = subjectCollector(subjects.totalHits$); searchSource.getField('index')!.isTimeBased = () => true; - await fetchAll(subjects, searchSource, false, deps); - + subjects.totalHits$.next({ + fetchStatus: FetchStatus.LOADING, + recordRawType: RecordRawType.DOCUMENT, + }); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); subjects.totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, recordRawType: RecordRawType.DOCUMENT, @@ -169,8 +183,12 @@ describe('test fetchAll', () => { const hits = [{ _id: '1', _index: 'logs' }]; const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); mockFetchDocuments.mockResolvedValue(documents); - await fetchAll(subjects, searchSource, false, deps); - + subjects.totalHits$.next({ + fetchStatus: FetchStatus.LOADING, + recordRawType: RecordRawType.DOCUMENT, + }); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); subjects.totalHits$.next({ fetchStatus: FetchStatus.ERROR, recordRawType: RecordRawType.DOCUMENT, @@ -200,11 +218,22 @@ describe('test fetchAll', () => { const collectMain = subjectCollector(subjects.main$); searchSource.getField('index')!.isTimeBased = () => false; mockFetchDocuments.mockRejectedValue({ msg: 'This query failed' }); - await fetchAll(subjects, searchSource, false, deps); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.LOADING, + recordRawType: RecordRawType.DOCUMENT, + }); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); + subjects.totalHits$.next({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: RecordRawType.DOCUMENT, + result: 5, + }); + expect(await collectMain()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, - { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document' }, // From totalHits query + // { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document' }, // There is no partial, since documents query failed { fetchStatus: FetchStatus.ERROR, error: { msg: 'This query failed' }, @@ -238,7 +267,9 @@ describe('test fetchAll', () => { savedSearch: savedSearchMock, services: discoverServiceMock, }; - await fetchAll(subjects, searchSource, false, deps); + fetchAll(subjects, searchSource, false, deps); + await waitForNextTick(); + expect(await collect()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'plain', query }, diff --git a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts index 55d2b7f99009d..877ea73b10097 100644 --- a/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/saved_search/public/services/saved_searches/saved_searches_utils.test.ts @@ -40,6 +40,7 @@ describe('saved_searches_utils', () => { ) ).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "a", "b", @@ -125,6 +126,7 @@ describe('saved_searches_utils', () => { expect(toSavedSearchAttributes(savedSearch, '{}')).toMatchInlineSnapshot(` Object { + "breakdownField": undefined, "columns": Array [ "c", "d", diff --git a/test/functional/apps/discover/group1/_discover_histogram.ts b/test/functional/apps/discover/group1/_discover_histogram.ts index 99775893c8b40..884dc665bf9db 100644 --- a/test/functional/apps/discover/group1/_discover_histogram.ts +++ b/test/functional/apps/discover/group1/_discover_histogram.ts @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); // this is the number of renderings of the histogram needed when new data is fetched - const renderingCountInc = 1; + let renderingCountInc = 1; const prevRenderingCount = await elasticChart.getVisualizationRenderingCount(); await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitUntilSearchingHasFinished(); @@ -79,11 +79,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.brushHistogram(); await PageObjects.discover.waitUntilSearchingHasFinished(); + renderingCountInc = 2; await retry.waitFor('chart rendering complete after being brushed', async () => { const actualCount = await elasticChart.getVisualizationRenderingCount(); const expectedCount = prevRenderingCount + renderingCountInc * 2; log.debug(`renderings after brushing - actual: ${actualCount} expected: ${expectedCount}`); - return actualCount === expectedCount; + return actualCount <= expectedCount; }); const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); expect(Math.round(newDurationHours)).to.be(26); @@ -102,10 +103,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); const initialTimeString = await PageObjects.discover.getChartTimespan(); - await queryBar.submitQuery(); + await queryBar.clickQuerySubmitButton(); + await PageObjects.discover.waitUntilSearchingHasFinished(); await retry.waitFor('chart timespan to have changed', async () => { const refreshedTimeString = await PageObjects.discover.getChartTimespan(); + await queryBar.clickQuerySubmitButton(); + await PageObjects.discover.waitUntilSearchingHasFinished(); log.debug( `Timestamp before: ${initialTimeString}, Timestamp after: ${refreshedTimeString}` ); diff --git a/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts b/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts index f83a76c45954b..805d59aee937d 100644 --- a/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts +++ b/test/functional/apps/discover/group1/_discover_histogram_breakdown.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); - describe('discover histogram breakdown', function describeIndexTests() { + describe('discover unified histogram breakdown', function describeIndexTests() { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.chooseBreakdownField('extension.raw'); await PageObjects.header.waitUntilLoadingHasFinished(); const list = await PageObjects.discover.getHistogramLegendList(); - expect(list).to.eql(['png', 'css', 'jpg']); + expect(list).to.eql(['Other', 'png', 'css', 'jpg']); }); it('should add filter using histogram legend values', async () => { @@ -55,7 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.loadSavedSearch('with breakdown'); await PageObjects.header.waitUntilLoadingHasFinished(); const list = await PageObjects.discover.getHistogramLegendList(); - expect(list).to.eql(['png', 'css', 'jpg']); + expect(list).to.eql(['Other', 'png', 'css', 'jpg']); }); }); } diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 4dee8e3af8262..5c240b2c0403c 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -16,10 +16,11 @@ import { PivotTransformTestData, } from '.'; -export default function ({ getService }: FtrProviderContext) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const canvasElement = getService('canvasElement'); const esArchiver = getService('esArchiver'); const transform = getService('transform'); + const PageObjects = getPageObjects(['discover']); describe('creation_index_pattern', function () { before(async () => { @@ -698,6 +699,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('should navigate to discover'); await transform.table.clickTransformRowAction(testData.transformId, 'Discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); if (testData.discoverAdjustSuperDatePicker) { await transform.discover.assertNoResults(testData.destinationIndex); From 192fcec51f21d8eb4ce3334f4359a06977f1c26f Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 9 Nov 2022 15:38:03 -0400 Subject: [PATCH 43/84] [Discover] cleanup --- .../main/components/layout/use_discover_histogram.ts | 4 ++-- .../public/chart/breakdown_field_selector.tsx | 2 +- .../unified_histogram/public/chart/histogram.tsx | 2 +- .../unified_histogram/public/chart/use_refetch_id.ts | 1 - .../unified_histogram/public/layout/layout.tsx | 11 ++++++++++- src/plugins/unified_histogram/public/types.ts | 5 ++--- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index c7e3cffbe06dc..4fda16eeb5cca 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -143,13 +143,13 @@ export const useDiscoverHistogram = ({ const onTotalHitsChange = useCallback( (status: UnifiedHistogramFetchStatus, result?: number | Error) => { - const { fetchStatus, recordRawType } = savedSearchData$.totalHits$.getValue(); - if (result instanceof Error) { sendTotalHitsError(result); return; } + const { fetchStatus, recordRawType } = savedSearchData$.totalHits$.getValue(); + // If we have a partial result already, we don't // want to update the total hits back to loading if (fetchStatus === FetchStatus.PARTIAL && status === UnifiedHistogramFetchStatus.loading) { diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index 9bcdf5c12e899..bc08ab71e217c 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -26,7 +26,7 @@ export const BreakdownFieldSelector = ({ onBreakdownFieldChange, }: BreakdownFieldSelectorProps) => { const fieldOptions = dataView.fields - .filter((field) => fieldSupportsBreakdown(field)) + .filter(fieldSupportsBreakdown) .map((field) => ({ label: field.displayName, value: field.name })) .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())); diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index fb7d72e71683b..59b9388d6c819 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -149,7 +149,7 @@ export function Histogram({ ); }, REQUEST_DEBOUNCE_MS, - [attributes, lastReloadRequestTime, onLoad, request?.searchSessionId, timeRange] + [attributes, lastReloadRequestTime, onLoad, request, timeRange] ); return ( diff --git a/src/plugins/unified_histogram/public/chart/use_refetch_id.ts b/src/plugins/unified_histogram/public/chart/use_refetch_id.ts index 357d6892c1745..4415be9ccd8b6 100644 --- a/src/plugins/unified_histogram/public/chart/use_refetch_id.ts +++ b/src/plugins/unified_histogram/public/chart/use_refetch_id.ts @@ -61,7 +61,6 @@ export const useRefetchId = ({ }); if (!isEqual(refetchDeps.current, newRefetchDeps)) { - console.log(newRefetchDeps.filter((item, i) => !isEqual(item, refetchDeps.current?.[i]))); if (refetchDeps.current) { setRefetchId((id) => id + 1); } diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index bc4ebb054b0f4..b9fabb62b0419 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -25,8 +25,17 @@ import type { } from '../types'; export interface UnifiedHistogramLayoutProps extends PropsWithChildren { + /** + * Optional class name to add to the layout container + */ className?: string; + /** + * Required services + */ services: UnifiedHistogramServices; + /** + * The current data view + */ dataView: DataView; /** * Can be updated to `Date.now()` to force a refresh @@ -82,7 +91,7 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void; /** * Callback to update the total hits -- should set {@link UnifiedHistogramHitsContext.status} to status - * and {@link UnifiedHistogramHitsContext.total} to totalHits + * and {@link UnifiedHistogramHitsContext.total} to result */ onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; /** diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index 1fda1e45cb78e..c4579812216a4 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -38,8 +38,7 @@ export interface UnifiedHistogramServices { } /** - * The bucketInterval object returned by {@link buildChartData} that - * should be used to set {@link UnifiedHistogramChartContext.bucketInterval} + * The bucketInterval object returned by {@link buildBucketInterval} */ export interface UnifiedHistogramBucketInterval { scaled?: boolean; @@ -72,7 +71,7 @@ export interface UnifiedHistogramRequestContext { */ searchSessionId?: string; /** - * The adapter to use for requests + * The adapter to use for requests (does not apply to Lens requests) */ adapter?: RequestAdapter; } From c40eda8eb744ddd174707ae644ec98e1da99353e Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 9 Nov 2022 17:29:52 -0400 Subject: [PATCH 44/84] [Discover] Refetch data when chart visibility is toggled or breakdown field is changed to keep total hits, chart, and documents in sync --- .../application/main/hooks/use_discover_state.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts index 7d17279af2d4f..74d5159897153 100644 --- a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts @@ -178,10 +178,10 @@ export function useDiscoverState({ */ useEffect(() => { const unsubscribe = appStateContainer.subscribe(async (nextState) => { - const { hideChart, interval, sort, index } = state; - // chart was hidden, now it should be displayed, so data is needed - const chartDisplayChanged = nextState.hideChart !== hideChart && hideChart; + const { hideChart, interval, breakdownField, sort, index } = state; + const chartDisplayChanged = nextState.hideChart !== hideChart; const chartIntervalChanged = nextState.interval !== interval; + const breakdownFieldChanged = nextState.breakdownField !== breakdownField; const docTableSortChanged = !isEqual(nextState.sort, sort); const dataViewChanged = !isEqual(nextState.index, index); // NOTE: this is also called when navigating from discover app to context app @@ -214,9 +214,15 @@ export function useDiscoverState({ reset(); } - if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged) { + if ( + chartDisplayChanged || + chartIntervalChanged || + breakdownFieldChanged || + docTableSortChanged + ) { refetch$.next(undefined); } + setState(nextState); }); return () => unsubscribe(); From 6bce46e71129819b5971982835654aa2c689f8e4 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 11 Nov 2022 16:25:24 +0300 Subject: [PATCH 45/84] [Discover] fix include typings --- src/plugins/unified_histogram/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/unified_histogram/tsconfig.json b/src/plugins/unified_histogram/tsconfig.json index 444370fe54d94..9c6213783980c 100644 --- a/src/plugins/unified_histogram/tsconfig.json +++ b/src/plugins/unified_histogram/tsconfig.json @@ -5,7 +5,7 @@ "emitDeclarationOnly": true, "declaration": true, }, - "include": ["common/**/*", "public/**/*", "server/**/*"], + "include": [ "../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*"], "kbn_references": [ { "path": "../../core/tsconfig.json" }, { "path": "../charts/tsconfig.json" }, From b582bc7748eb1fb5a57ca6136db9434bcdbb3467 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 11 Nov 2022 17:36:35 +0300 Subject: [PATCH 46/84] [Discover] try to fix type check failure --- x-pack/plugins/data_visualizer/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 82484dc83b6cd..8d466474be5d8 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -28,6 +28,7 @@ { "path": "../../../src/plugins/unified_search/tsconfig.json" }, { "path": "../cloud/tsconfig.json" }, { "path": "../cloud_integrations/cloud_chat/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" } + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../unified_histogram/tsconfig.json" } ] } From 26b2feb11c9ee643d9592226715517d84053c922 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 11 Nov 2022 18:29:09 +0300 Subject: [PATCH 47/84] [Discover] fix discover layout test --- .../layout/discover_layout.test.tsx | 31 ------------------- .../public/chart/chart.test.tsx | 16 +++++++++- .../unified_histogram/public/chart/chart.tsx | 2 +- 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 9c47537849f2b..60c443c61d7af 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -13,7 +13,6 @@ import type { Query, AggregateQuery } from '@kbn/es-query'; import { setHeaderActionMenuMounter } from '../../../../kibana_services'; import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout'; import { esHits } from '../../../../__mocks__/es_hits'; -import { dataViewMock } from '../../../../__mocks__/data_view'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { createSearchSourceMock, @@ -158,36 +157,6 @@ async function mountComponent( } describe('Discover component', () => { - test('selected data view without time field displays no chart toggle', async () => { - const container = document.createElement('div'); - await mountComponent(dataViewMock, undefined, { attachTo: container }); - expect( - container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') - ).toBeNull(); - }); - - test('selected data view with time field displays chart toggle', async () => { - const container = document.createElement('div'); - await mountComponent(dataViewWithTimefieldMock, undefined, { attachTo: container }); - expect( - container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') - ).not.toBeNull(); - }); - - test('sql query displays no chart toggle', async () => { - const container = document.createElement('div'); - await mountComponent( - dataViewWithTimefieldMock, - false, - { attachTo: container }, - { sql: 'SELECT * FROM test' }, - true - ); - expect( - container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') - ).toBeNull(); - }); - test('the saved search title h1 gains focus on navigate', async () => { const container = document.createElement('div'); document.body.appendChild(container); diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index 80d3a37d0c27b..d194f4c044fa6 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -9,6 +9,7 @@ import React, { ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test-jest-helpers'; +import type { DataView } from '@kbn/data-views-plugin/public'; import type { UnifiedHistogramFetchStatus } from '../types'; import { Chart } from './chart'; import type { ReactWrapper } from 'enzyme'; @@ -17,6 +18,7 @@ import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_ import { of } from 'rxjs'; import { HitsCounter } from '../hits_counter'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { dataViewMock } from '../__mocks__/data_view'; async function mountComponent({ noChart, @@ -24,11 +26,13 @@ async function mountComponent({ chartHidden = false, appendHistogram, onEditVisualization = jest.fn(), + dataView = dataViewWithTimefieldMock, }: { noChart?: boolean; noHits?: boolean; chartHidden?: boolean; appendHistogram?: ReactElement; + dataView?: DataView; onEditVisualization?: null | (() => void); } = {}) { const services = unifiedHistogramServicesMock; @@ -44,7 +48,7 @@ async function mountComponent({ ); const props = { - dataView: dataViewWithTimefieldMock, + dataView, services: unifiedHistogramServicesMock, hits: noHits ? undefined @@ -153,4 +157,14 @@ describe('Chart', () => { 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 () => { + const component = await mountComponent({ dataView: dataViewMock }); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeFalsy(); + }); + + it('should render chart if data view is not time based', async () => { + const component = await mountComponent(); + expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); + }); }); diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 2df0669f0d09e..4cccbd32ff2b0 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -41,7 +41,7 @@ export interface ChartProps { className?: string; services: UnifiedHistogramServices; dataView: DataView; - lastReloadRequestTime: number | undefined; + lastReloadRequestTime?: number; request?: UnifiedHistogramRequestContext; hits?: UnifiedHistogramHitsContext; chart?: UnifiedHistogramChartContext; From b59058ddd1d16a20df6166cc32dae0784932d21f Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 11 Nov 2022 18:30:52 +0300 Subject: [PATCH 48/84] [Discover] fix ts config --- x-pack/plugins/data_visualizer/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 8d466474be5d8..6f7e80c713467 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -29,6 +29,6 @@ { "path": "../cloud/tsconfig.json" }, { "path": "../cloud_integrations/cloud_chat/tsconfig.json" }, { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../unified_histogram/tsconfig.json" } + { "path": "../../../src/plugins/unified_histogram/tsconfig.json" } ] } From 4a992b1a3a2f23623fafa44fa490ada4d33574e3 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 14 Nov 2022 13:22:31 +0300 Subject: [PATCH 49/84] [Discover] fix tests --- .../discover_histogram_content.test.tsx | 137 ++++++++++++ .../layout/discover_histogram_content.tsx | 127 +++++++++++ ...tsx => discover_histogram_layout.test.tsx} | 81 ++----- .../layout/discover_histogram_layout.tsx | 83 +++++++ .../components/layout/discover_layout.tsx | 4 +- .../layout/discover_main_content.tsx | 211 ------------------ .../components/layout/reset_search_button.tsx | 35 +++ x-pack/plugins/observability/tsconfig.json | 1 + 8 files changed, 398 insertions(+), 281 deletions(-) create mode 100644 src/plugins/discover/public/application/main/components/layout/discover_histogram_content.test.tsx create mode 100644 src/plugins/discover/public/application/main/components/layout/discover_histogram_content.tsx rename src/plugins/discover/public/application/main/components/layout/{discover_main_content.test.tsx => discover_histogram_layout.test.tsx} (65%) create mode 100644 src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx delete mode 100644 src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx create mode 100644 src/plugins/discover/public/application/main/components/layout/reset_search_button.tsx diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.test.tsx new file mode 100644 index 0000000000000..1165669f449bb --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { Subject, BehaviorSubject, of } from 'rxjs'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { esHits } from '../../../../__mocks__/es_hits'; +import { dataViewMock } from '../../../../__mocks__/data_view'; +import { GetStateReturn } from '../../services/discover_state'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { + AvailableFields$, + DataDocuments$, + DataMain$, + DataTotalHits$, + RecordRawType, +} from '../../hooks/use_saved_search'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { FetchStatus } from '../../../types'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { buildDataTableRecord } from '../../../../utils/build_data_record'; +import { + DiscoverHistogramContent, + DiscoverHistogramContentProps, +} from './discover_histogram_content'; +import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; +import { CoreTheme } from '@kbn/core/public'; +import { act } from 'react-dom/test-utils'; +import { setTimeout } from 'timers/promises'; +import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; + +const mountComponent = async ({ + isPlainRecord = false, +}: { + isPlainRecord?: boolean; +} = {}) => { + const services = discoverServiceMock; + services.data.query.timefilter.timefilter.getAbsoluteTime = () => { + return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; + }; + + (services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({ + language: 'kuery', + query: '', + }); + (searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation( + jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } })) + ); + + const main$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT, + foundDocuments: true, + }) as DataMain$; + + const documents$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewMock)), + }) as DataDocuments$; + + const availableFields$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + fields: [] as string[], + }) as AvailableFields$; + + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: Number(esHits.length), + }) as DataTotalHits$; + + const savedSearchData$ = { + main$, + documents$, + totalHits$, + availableFields$, + }; + + const props: DiscoverHistogramContentProps = { + isPlainRecord, + dataView: dataViewMock, + navigateTo: jest.fn(), + setExpandedDoc: jest.fn(), + savedSearch: savedSearchMock, + savedSearchData$, + savedSearchRefetch$: new Subject(), + state: { columns: [], hideChart: false }, + stateContainer: { + setAppState: () => {}, + appStateContainer: { + getState: () => ({ + interval: 'auto', + }), + }, + } as unknown as GetStateReturn, + onFieldEdited: jest.fn(), + columns: [], + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + onAddFilter: jest.fn(), + }; + + const coreTheme$ = new BehaviorSubject({ darkMode: false }); + + const component = mountWithIntl( + + + + + + ); + + // DiscoverMainContent uses UnifiedHistogramLayout which + // is lazy loaded, so we need to wait for it to be loaded + await act(() => setTimeout(0)); + component.update(); + + return component; +}; + +describe('Discover histogram content component', () => { + describe('DocumentViewModeToggle', () => { + it('should show DocumentViewModeToggle when isPlainRecord is false', async () => { + const component = await mountComponent(); + expect(component.find(DocumentViewModeToggle).exists()).toBe(true); + }); + + it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => { + const component = await mountComponent({ isPlainRecord: true }); + expect(component.find(DocumentViewModeToggle).exists()).toBe(false); + }); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.tsx new file mode 100644 index 0000000000000..e1785f37362d1 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.tsx @@ -0,0 +1,127 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import React, { useCallback } from 'react'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { DataTableRecord } from '../../../../types'; +import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle'; +import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; +import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search'; +import { AppState, GetStateReturn } from '../../services/discover_state'; +import { FieldStatisticsTable } from '../field_stats_table'; +import { DiscoverDocuments } from './discover_documents'; +import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; + +const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); + +export interface CommonDiscoverHistogramProps { + dataView: DataView; + savedSearch: SavedSearch; + isPlainRecord: boolean; + navigateTo: (url: string) => void; + savedSearchData$: SavedSearchData; + savedSearchRefetch$: DataRefetch$; + expandedDoc?: DataTableRecord; + setExpandedDoc: (doc?: DataTableRecord) => void; + viewMode: VIEW_MODE; + onAddFilter: DocViewFilterFn | undefined; + onFieldEdited: () => Promise; + columns: string[]; + state: AppState; + stateContainer: GetStateReturn; +} + +export interface DiscoverHistogramContentProps extends CommonDiscoverHistogramProps { + chartHidden?: boolean; +} + +export const DiscoverHistogramContent = ({ + dataView, + isPlainRecord, + navigateTo, + savedSearchData$, + savedSearchRefetch$, + expandedDoc, + setExpandedDoc, + viewMode, + onAddFilter, + onFieldEdited, + columns, + state, + stateContainer, + savedSearch, + chartHidden, +}: DiscoverHistogramContentProps) => { + const { trackUiMetric } = useDiscoverServices(); + + const setDiscoverViewMode = useCallback( + (mode: VIEW_MODE) => { + stateContainer.setAppState({ viewMode: mode }); + + if (trackUiMetric) { + if (mode === VIEW_MODE.AGGREGATED_LEVEL) { + trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK); + } else { + trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK); + } + } + }, + [trackUiMetric, stateContainer] + ); + + return ( + + {!isPlainRecord && ( + + + + + )} + {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx similarity index 65% rename from src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx rename to src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx index 7d94bd86ec842..9a21f7b419e4a 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Subject, BehaviorSubject, of } from 'rxjs'; -import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; import { esHits } from '../../../../__mocks__/es_hits'; import { dataViewMock } from '../../../../__mocks__/data_view'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; @@ -24,15 +24,13 @@ import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; -import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content'; +import { DiscoverHistogramLayout, DiscoverHistogramLayoutProps } from './discover_histogram_layout'; import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { CoreTheme } from '@kbn/core/public'; import { act } from 'react-dom/test-utils'; import { setTimeout } from 'timers/promises'; -import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; -import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; import { HISTOGRAM_HEIGHT_KEY } from './use_discover_histogram'; import { createSearchSessionMock } from '../../../../__mocks__/search_session'; import { RequestAdapter } from '@kbn/inspector-plugin/public'; @@ -51,7 +49,7 @@ const mountComponent = async ({ isTimeBased?: boolean; storage?: Storage; savedSearch?: SavedSearch; - resetSavedSearch?: () => void; + resetSavedSearch?(): void; } = {}) => { let services = discoverServiceMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { @@ -98,11 +96,10 @@ const mountComponent = async ({ availableFields$, }; - const props: DiscoverMainContentProps = { + const props: DiscoverHistogramLayoutProps = { isPlainRecord, dataView: dataViewMock, navigateTo: jest.fn(), - resetSavedSearch, setExpandedDoc: jest.fn(), savedSearch, savedSearchData$, @@ -116,11 +113,12 @@ const mountComponent = async ({ }), }, } as unknown as GetStateReturn, - isTimeBased, - viewMode: VIEW_MODE.DOCUMENT_LEVEL, - onAddFilter: jest.fn(), onFieldEdited: jest.fn(), columns: [], + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + onAddFilter: jest.fn(), + resetSavedSearch, + isTimeBased, resizeRef: { current: null }, searchSessionManager: createSearchSessionMock().searchSessionManager, inspectorAdapters: { requests: new RequestAdapter() }, @@ -131,7 +129,7 @@ const mountComponent = async ({ const component = mountWithIntl( - + ); @@ -139,25 +137,12 @@ const mountComponent = async ({ // DiscoverMainContent uses UnifiedHistogramLayout which // is lazy loaded, so we need to wait for it to be loaded await act(() => setTimeout(0)); + component.update(); return component; }; -describe('Discover main content component', () => { - describe('DocumentViewModeToggle', () => { - it('should show DocumentViewModeToggle when isPlainRecord is false', async () => { - const component = await mountComponent(); - component.update(); - expect(component.find(DocumentViewModeToggle).exists()).toBe(true); - }); - - it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => { - const component = await mountComponent({ isPlainRecord: true }); - component.update(); - expect(component.find(DocumentViewModeToggle).exists()).toBe(false); - }); - }); - +describe('Discover histogram layout component', () => { describe('topPanelHeight persistence', () => { it('should try to get the initial topPanelHeight for UnifiedHistogramLayout from storage', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; @@ -171,58 +156,18 @@ describe('Discover main content component', () => { const storage = new LocalStorageMock({}) as unknown as Storage; const originalGet = storage.get; storage.get = jest.fn().mockImplementation(originalGet); - const component = await mountComponent({ storage }); + await mountComponent({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); expect(storage.get).toHaveReturnedWith(null); - expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(undefined); }); it('should pass the stored topPanelHeight to UnifiedHistogramLayout if a value is found in storage', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; const topPanelHeight = 123; storage.get = jest.fn().mockImplementation(() => topPanelHeight); - const component = await mountComponent({ storage }); + await mountComponent({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); expect(storage.get).toHaveReturnedWith(topPanelHeight); - expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(topPanelHeight); - }); - - it('should update the topPanelHeight in storage and pass the new value to UnifiedHistogramLayout when the topPanelHeight changes', async () => { - const storage = new LocalStorageMock({}) as unknown as Storage; - const originalSet = storage.set; - storage.set = jest.fn().mockImplementation(originalSet); - const component = await mountComponent({ storage }); - const newTopPanelHeight = 123; - expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).not.toBe( - newTopPanelHeight - ); - act(() => { - component.find(UnifiedHistogramLayout).prop('onTopPanelHeightChange')!(newTopPanelHeight); - }); - component.update(); - expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, newTopPanelHeight); - expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(newTopPanelHeight); - }); - }); - - describe('reset search button', () => { - it('renders the button when there is a saved search', async () => { - const component = await mountComponent(); - expect(findTestSubject(component, 'resetSavedSearch').length).toBe(1); - }); - - it('does not render the button when there is no saved search', async () => { - const component = await mountComponent({ - savedSearch: { ...savedSearchMock, id: undefined }, - }); - expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0); - }); - - it('should call resetSavedSearch when clicked', async () => { - const resetSavedSearch = jest.fn(); - const component = await mountComponent({ resetSavedSearch }); - findTestSubject(component, 'resetSavedSearch').simulate('click'); - expect(resetSavedSearch).toHaveBeenCalled(); }); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx new file mode 100644 index 0000000000000..a3678ff4a7d34 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx @@ -0,0 +1,83 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { RefObject } from 'react'; +import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { useDiscoverHistogram } from './use_discover_histogram'; +import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; +import type { InspectorAdapters } from '../../hooks/use_inspector'; +import { + type CommonDiscoverHistogramProps, + DiscoverHistogramContent, +} from './discover_histogram_content'; +import { ResetSearchButton } from './reset_search_button'; + +export interface DiscoverHistogramLayoutProps extends CommonDiscoverHistogramProps { + resetSavedSearch: () => void; + isTimeBased: boolean; + resizeRef: RefObject; + inspectorAdapters: InspectorAdapters; + searchSessionManager: DiscoverSearchSessionManager; +} + +export const DiscoverHistogramLayout = ({ + isPlainRecord, + dataView, + resetSavedSearch, + savedSearch, + savedSearchData$, + state, + stateContainer, + isTimeBased, + resizeRef, + inspectorAdapters, + searchSessionManager, + ...histogramContentProps +}: DiscoverHistogramLayoutProps) => { + const services = useDiscoverServices(); + + const commonInputProps = { + dataView, + isPlainRecord, + stateContainer, + savedSearch, + state, + savedSearchData$, + }; + + const { shouldRender, ...histogramProps } = useDiscoverHistogram({ + isTimeBased, + inspectorAdapters, + searchSessionManager, + ...commonInputProps, + }); + + if (!shouldRender) { + return null; + } + + return ( + : undefined + } + {...histogramProps} + > + + + ); +}; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 0438572648c95..8bba4008a231e 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -42,7 +42,7 @@ import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { hasActiveFilter } from './utils'; import { getRawRecordType } from '../../utils/get_raw_record_type'; import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout'; -import { DiscoverMainContent } from './discover_main_content'; +import { DiscoverHistogramLayout } from './discover_histogram_layout'; /** * Local storage key for sidebar persistence state @@ -216,7 +216,7 @@ export function DiscoverLayout({ } return ( - void; - resetSavedSearch: () => void; - expandedDoc?: DataTableRecord; - setExpandedDoc: (doc?: DataTableRecord) => void; - savedSearch: SavedSearch; - savedSearchData$: SavedSearchData; - savedSearchRefetch$: DataRefetch$; - state: AppState; - stateContainer: GetStateReturn; - isTimeBased: boolean; - viewMode: VIEW_MODE; - onAddFilter: DocViewFilterFn | undefined; - onFieldEdited: () => Promise; - columns: string[]; - resizeRef: RefObject; - inspectorAdapters: InspectorAdapters; - searchSessionManager: DiscoverSearchSessionManager; -} - -const resetSearchButtonWrapper = css` - overflow: hidden; -`; - -export const DiscoverMainContent = ({ - isPlainRecord, - dataView, - navigateTo, - resetSavedSearch, - expandedDoc, - setExpandedDoc, - savedSearch, - savedSearchData$, - savedSearchRefetch$, - state, - stateContainer, - isTimeBased, - viewMode, - onAddFilter, - onFieldEdited, - columns, - resizeRef, - inspectorAdapters, - searchSessionManager, -}: DiscoverMainContentProps) => { - const services = useDiscoverServices(); - const { trackUiMetric } = services; - - const setDiscoverViewMode = useCallback( - (mode: VIEW_MODE) => { - stateContainer.setAppState({ viewMode: mode }); - - if (trackUiMetric) { - if (mode === VIEW_MODE.AGGREGATED_LEVEL) { - trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK); - } else { - trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK); - } - } - }, - [trackUiMetric, stateContainer] - ); - - const { - shouldRender, - topPanelHeight, - request, - hits, - chart, - breakdown, - onEditVisualization, - onTopPanelHeightChange, - onChartHiddenChange, - onTimeIntervalChange, - onBreakdownFieldChange, - onTotalHitsChange, - onChartLoad, - } = useDiscoverHistogram({ - stateContainer, - state, - savedSearchData$, - dataView, - savedSearch, - isTimeBased, - isPlainRecord, - inspectorAdapters, - searchSessionManager, - }); - - return shouldRender ? ( - - - - -
- ) : undefined - } - onTopPanelHeightChange={onTopPanelHeightChange} - onEditVisualization={onEditVisualization} - onChartHiddenChange={onChartHiddenChange} - onTimeIntervalChange={onTimeIntervalChange} - onBreakdownFieldChange={onBreakdownFieldChange} - onTotalHitsChange={onTotalHitsChange} - onChartLoad={onChartLoad} - > - - {!isPlainRecord && ( - - - - - )} - {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( - - ) : ( - - )} - - - ) : ( - <> - ); -}; diff --git a/src/plugins/discover/public/application/main/components/layout/reset_search_button.tsx b/src/plugins/discover/public/application/main/components/layout/reset_search_button.tsx new file mode 100644 index 0000000000000..e9b0cc2417d97 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/reset_search_button.tsx @@ -0,0 +1,35 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; + +const resetSearchButtonWrapper = css` + overflow: hidden; +`; + +export const ResetSearchButton = ({ resetSavedSearch }: { resetSavedSearch?: () => void }) => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 4f9d89cd2b3cd..07e7a45fe3be1 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -30,5 +30,6 @@ { "path": "../translations/tsconfig.json" }, { "path": "../../../src/plugins/unified_search/tsconfig.json"}, { "path": "../../../src/plugins/guided_onboarding/tsconfig.json"}, + { "path": "../../../src/plugins/unified_histogram/tsconfig.json" } ] } From 6121d61fa0b0176d0c9688351f99146a0f717e67 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 14 Nov 2022 15:27:44 +0300 Subject: [PATCH 50/84] [Discover] fix ci --- .../migrations/check_registered_types.test.ts | 2 +- .../public/chart/build_bucket_interval.test.ts | 4 ++++ .../public/chart/get_chart_agg_config.test.ts | 14 +++++++++++++- .../public/chart/histogram.test.tsx | 6 +++--- .../public/hits_counter/hits_counter.test.tsx | 16 ++++++++++++---- .../public/layout/layout.test.tsx | 8 ++++++-- .../apps/discover/group1/_inspector.ts | 10 ++++------ 7 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index a53049a26106e..9a70f79fbbf26 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -118,7 +118,7 @@ describe('checking migration metadata changes on all registered SO types', () => "osquery-saved-query": "7b213b4b7a3e59350e99c50e8df9948662ed493a", "query": "4640ef356321500a678869f24117b7091a911cb6", "sample-data-telemetry": "8b10336d9efae6f3d5593c4cc89fb4abcdf84e04", - "search": "e7ba25ea37cb36b622db42c9590c6d8dfc838801", + "search": "963f2eb44b401e48c8c7cbe980e85572c2f52506", "search-session": "ba383309da68a15be3765977f7a44c84f0ec7964", "search-telemetry": "beb3fc25488c753f2a6dcff1845d667558712b66", "security-rule": "e0dfdba5d66139d0300723b2e6672993cd4a11f3", diff --git a/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts b/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts index 737d77ced1c02..072f7a811babe 100644 --- a/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts +++ b/src/plugins/unified_histogram/public/chart/build_bucket_interval.test.ts @@ -72,6 +72,10 @@ describe('buildBucketInterval', () => { dataView, timeInterval: 'auto', response, + timeRange: { + from: '1991-03-29T08:04:00.694Z', + to: '2021-03-29T07:04:00.695Z', + }, }; }; diff --git a/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts b/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts index 3b4f470ba6119..ef5ce1b677153 100644 --- a/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts +++ b/src/plugins/unified_histogram/public/chart/get_chart_agg_config.test.ts @@ -14,7 +14,15 @@ describe('getChartAggConfigs', () => { test('is working', () => { const dataView = dataViewWithTimefieldMock; const dataMock = dataPluginMock.createStartContract(); - const aggsConfig = getChartAggConfigs({ dataView, timeInterval: 'auto', data: dataMock }); + const aggsConfig = getChartAggConfigs({ + dataView, + timeInterval: 'auto', + data: dataMock, + timeRange: { + from: '2022-10-05T16:00:00.000-03:00', + to: '2022-10-05T18:00:00.000-03:00', + }, + }); expect(aggsConfig!.aggs).toMatchInlineSnapshot(` Array [ @@ -38,6 +46,10 @@ describe('getChartAggConfigs', () => { "interval": "auto", "min_doc_count": 1, "scaleMetricValues": false, + "timeRange": Object { + "from": "2022-10-05T16:00:00.000-03:00", + "to": "2022-10-05T18:00:00.000-03:00", + }, "useNormalizedEsInterval": true, "used_interval": "0ms", }, diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index b684988c6d54c..c46a042d1334e 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -6,13 +6,12 @@ * Side Public License, v 1. */ import { mountWithIntl } from '@kbn/test-jest-helpers'; -import type { UnifiedHistogramFetchStatus } from '../types'; import { Histogram } from './histogram'; import React from 'react'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; -function mountComponent(status: UnifiedHistogramFetchStatus, error?: Error) { +function mountComponent() { const services = unifiedHistogramServicesMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; @@ -37,6 +36,7 @@ function mountComponent(status: UnifiedHistogramFetchStatus, error?: Error) { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590', }, + lastReloadRequestTime: 0, }; return mountWithIntl(); @@ -44,7 +44,7 @@ function mountComponent(status: UnifiedHistogramFetchStatus, error?: Error) { describe('Histogram', () => { it('renders correctly', () => { - const component = mountComponent('complete'); + const component = mountComponent(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true); }); }); diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx index d094fef953af8..03b350448e9c2 100644 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.test.tsx @@ -13,6 +13,7 @@ import type { HitsCounterProps } from './hits_counter'; import { HitsCounter } from './hits_counter'; import { findTestSubject } from '@elastic/eui/lib/test'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { UnifiedHistogramFetchStatus } from '../types'; describe('hits counter', function () { let props: HitsCounterProps; @@ -21,7 +22,7 @@ describe('hits counter', function () { beforeAll(() => { props = { hits: { - status: 'complete', + status: UnifiedHistogramFetchStatus.complete, total: 2, }, }; @@ -35,7 +36,10 @@ describe('hits counter', function () { it('expect to render 1,899 hits if 1899 hits given', function () { component = mountWithIntl( - + ); const hits = findTestSubject(component, 'unifiedHistogramQueryHits'); expect(hits.text()).toBe('1,899'); @@ -48,12 +52,16 @@ describe('hits counter', function () { }); it('should render a EuiLoadingSpinner when status is partial', () => { - component = mountWithIntl(); + component = mountWithIntl( + + ); expect(component.find(EuiLoadingSpinner).length).toBe(1); }); it('should render unifiedHistogramQueryHitsPartial when status is partial', () => { - component = mountWithIntl(); + component = mountWithIntl( + + ); expect(component.find('[data-test-subj="unifiedHistogramQueryHitsPartial"]').length).toBe(1); }); diff --git a/src/plugins/unified_histogram/public/layout/layout.test.tsx b/src/plugins/unified_histogram/public/layout/layout.test.tsx index 0673d255362c4..85862b609a19c 100644 --- a/src/plugins/unified_histogram/public/layout/layout.test.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.test.tsx @@ -14,7 +14,11 @@ import { act } from 'react-dom/test-utils'; import { of } from 'rxjs'; import { Chart } from '../chart'; import { Panels, PANELS_MODE } from '../panels'; -import type { UnifiedHistogramChartContext, UnifiedHistogramHitsContext } from '../types'; +import { + UnifiedHistogramChartContext, + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, +} from '../types'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from './layout'; @@ -33,7 +37,7 @@ jest.mock('@elastic/eui', () => { describe('Layout', () => { const createHits = (): UnifiedHistogramHitsContext => ({ - status: 'complete', + status: UnifiedHistogramFetchStatus.complete, total: 10, }); diff --git a/test/functional/apps/discover/group1/_inspector.ts b/test/functional/apps/discover/group1/_inspector.ts index 10451adc98e4f..851f992f42b20 100644 --- a/test/functional/apps/discover/group1/_inspector.ts +++ b/test/functional/apps/discover/group1/_inspector.ts @@ -38,10 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); // delete .kibana index and update configDoc - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - }); - + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); @@ -51,9 +49,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should display request stats with no results', async () => { await inspector.open(); - await testSubjects.click('inspectorRequestChooser'); let foundZero = false; - for (const subj of ['Documents', 'Chart_data']) { + for (const subj of ['Documents', 'Data']) { + await testSubjects.click('inspectorRequestChooser'); await testSubjects.click(`inspectorRequestChooser${subj}`); if (await testSubjects.exists('inspectorRequestDetailStatistics', { timeout: 500 })) { await testSubjects.click(`inspectorRequestDetailStatistics`); From b0bff110b9b1595b7a38ddcc0f665874a633e361 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 15 Nov 2022 15:07:35 +0300 Subject: [PATCH 51/84] [Discover] add navigation to Lens using breakdown by field --- .../layout/use_discover_histogram.ts | 47 ++++++++++--------- src/plugins/ui_actions/public/types.ts | 1 + .../visualize_trigger_utils.ts | 4 +- .../form_based/form_based_suggestions.ts | 47 +++++++++++++++---- .../public/datasources/form_based/types.ts | 1 + .../editor_frame/suggestion_helpers.ts | 3 +- x-pack/plugins/lens/public/types.ts | 3 +- .../apps/discover/visualize_field.ts | 22 +++++++++ 8 files changed, 94 insertions(+), 34 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 4fda16eeb5cca..f752fd3859326 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -54,6 +54,27 @@ export const useDiscoverHistogram = ({ }) => { const { storage, data } = useDiscoverServices(); + /** + * Breakdown + */ + + const onBreakdownFieldChange = useCallback( + (breakdownField: DataViewField | undefined) => { + stateContainer.setAppState({ breakdownField: breakdownField?.name }); + }, + [stateContainer] + ); + + const field = useMemo( + () => (state.breakdownField ? dataView.getFieldByName(state.breakdownField) : undefined), + [dataView, state.breakdownField] + ); + + const breakdown = useMemo( + () => (isPlainRecord || !isTimeBased ? undefined : { field }), + [field, isPlainRecord, isTimeBased] + ); + /** * Visualize */ @@ -85,9 +106,10 @@ export const useDiscoverHistogram = ({ timeField, savedSearch.columns || [], PLUGIN_ID, - dataView + dataView, + breakdown?.field ); - }, [dataView, savedSearch.columns, timeField]); + }, [breakdown?.field, dataView, savedSearch.columns, timeField]); /** * Height @@ -216,27 +238,6 @@ export const useDiscoverHistogram = ({ [isPlainRecord, isTimeBased, state.hideChart, state.interval] ); - /** - * Breakdown - */ - - const onBreakdownFieldChange = useCallback( - (breakdownField: DataViewField | undefined) => { - stateContainer.setAppState({ breakdownField: breakdownField?.name }); - }, - [stateContainer] - ); - - const field = useMemo( - () => (state.breakdownField ? dataView.getFieldByName(state.breakdownField) : undefined), - [dataView, state.breakdownField] - ); - - const breakdown = useMemo( - () => (isPlainRecord || !isTimeBased ? undefined : { field }), - [field, isPlainRecord, isTimeBased] - ); - return { // The histogram layout shouldn't render until the first search // request has started, or an immediate refetch of the histogram diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index fb2d9869e21c7..ee54d4daf7365 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -18,6 +18,7 @@ export type TriggerToActionsRegistry = Map; export interface VisualizeFieldContext { fieldName: string; dataViewSpec: DataViewSpec; + breakdownField?: string; contextualFields?: string[]; originatingApp?: string; query?: AggregateQuery; diff --git a/src/plugins/unified_field_list/public/components/field_visualize_button/visualize_trigger_utils.ts b/src/plugins/unified_field_list/public/components/field_visualize_button/visualize_trigger_utils.ts index babb7f40ff92b..75d2b5846119f 100644 --- a/src/plugins/unified_field_list/public/components/field_visualize_button/visualize_trigger_utils.ts +++ b/src/plugins/unified_field_list/public/components/field_visualize_button/visualize_trigger_utils.ts @@ -49,12 +49,14 @@ export function triggerVisualizeActions( field: DataViewField, contextualFields: string[] = [], originatingApp: string, - dataView?: DataView + dataView?: DataView, + breakdownField?: DataViewField ) { if (!dataView) return; const trigger = getTriggerConstant(field.type); const triggerOptions = { dataViewSpec: dataView.toSpec(false), + breakdownField: breakdownField?.name, fieldName: field.name, contextualFields, originatingApp, diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts index 81ce81bb49053..222b9cdbd45bf 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts @@ -363,13 +363,15 @@ export function getDatasourceSuggestionsForVisualizeField( state: FormBasedPrivateState, indexPatternId: string, fieldName: string, - indexPatterns: IndexPatternMap + indexPatterns: IndexPatternMap, + breakdownField?: string ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); // Identify the field by the indexPatternId and the fieldName const indexPattern = indexPatterns[indexPatternId]; const field = indexPattern?.getFieldByName(fieldName); + const breakdown = breakdownField ? indexPattern?.getFieldByName(breakdownField) : undefined; if (layerIds.length !== 0 || !field) return []; const newId = generateId(); @@ -378,14 +380,16 @@ export function getDatasourceSuggestionsForVisualizeField( newId, indexPatternId, field, - indexPatterns + indexPatterns, + breakdown ).concat( getEmptyLayerSuggestionsForField( { ...state, layers: {} }, newId, indexPatternId, field, - indexPatterns + indexPatterns, + breakdown ) ); } @@ -518,13 +522,19 @@ function getEmptyLayerSuggestionsForField( layerId: string, indexPatternId: string, field: IndexPatternField, - indexPatterns: IndexPatternMap + indexPatterns: IndexPatternMap, + breakdownField?: IndexPatternField ): IndexPatternSuggestion[] { const indexPattern = indexPatterns[indexPatternId]; let newLayer: FormBasedLayer | undefined; const bucketOperation = getBucketOperation(field); if (bucketOperation) { - newLayer = createNewLayerWithBucketAggregation(indexPattern, field, bucketOperation); + newLayer = createNewLayerWithBucketAggregation( + indexPattern, + field, + bucketOperation, + breakdownField + ); } else if (indexPattern.timeFieldName && getOperationTypesForField(field).length > 0) { newLayer = createNewLayerWithMetricAggregation(indexPattern, field); } @@ -554,14 +564,35 @@ function getEmptyLayerSuggestionsForField( function createNewLayerWithBucketAggregation( indexPattern: IndexPattern, field: IndexPatternField, - operation: OperationType + operation: OperationType, + breakdownField?: IndexPatternField ): FormBasedLayer { + const countColumnId = generateId(); + const splitColumnId = generateId(); + + const getBreakdownColumn = () => + insertNewColumn({ + op: 'terms', + layer: { + indexPatternId: indexPattern.id, + columns: {}, + columnOrder: [], + splitAccessor: splitColumnId, + }, + columnId: splitColumnId, + field: breakdownField, + indexPattern, + visualizationGroups: [], + }); + return insertNewColumn({ op: operation, layer: insertNewColumn({ op: 'count', - layer: { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] }, - columnId: generateId(), + layer: breakdownField + ? getBreakdownColumn() + : { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] }, + columnId: countColumnId, field: documentField, indexPattern, visualizationGroups: [], diff --git a/x-pack/plugins/lens/public/datasources/form_based/types.ts b/x-pack/plugins/lens/public/datasources/form_based/types.ts index 0846c96d76dc8..4a89fddb46620 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/types.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/types.ts @@ -55,6 +55,7 @@ export interface FormBasedLayer { // Partial columns represent the temporary invalid states incompleteColumns?: Record; sampling?: number; + splitAccessor?: string; } export interface FormBasedPersistedState { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 81298a97f650e..57edaff6c1f5a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -106,7 +106,8 @@ export function getSuggestions({ datasourceState, visualizeTriggerFieldContext.dataViewSpec.id!, visualizeTriggerFieldContext.fieldName, - dataViews.indexPatterns + dataViews.indexPatterns, + visualizeTriggerFieldContext.breakdownField ); } } else if (field) { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index b42f3c4ae0411..b89fdd5249e33 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -385,7 +385,8 @@ export interface Datasource { state: T, indexPatternId: string, fieldName: string, - indexPatterns: IndexPatternMap + indexPatterns: IndexPatternMap, + breakdownField?: string ) => Array>; getDatasourceSuggestionsFromCurrentState: ( state: T, diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts index 453c3467c923a..7d77d5d9c4480 100644 --- a/x-pack/test/functional/apps/discover/visualize_field.ts +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -91,6 +92,27 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await queryBar.getQueryString()).to.equal('machine.os : ios'); }); + it('should visualize correctly using breakdown field', async () => { + await PageObjects.discover.chooseBreakdownField('extension.raw'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('unifiedHistogramEditVisualization'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async () => { + const breakdownLabel = await testSubjects.find( + 'lnsDragDrop_draggable-Top 5 values of extension.raw' + ); + + const lnsWorkspace = await testSubjects.find('lnsWorkspace'); + const list = await lnsWorkspace.findAllByClassName('echLegendItem__label'); + const values = await Promise.all( + list.map((elem: WebElementWrapper) => elem.getVisibleText()) + ); + + expect(await breakdownLabel.getVisibleText()).to.eql('Top 5 values of extension.raw'); + expect(values).to.eql(['php', 'gif', 'png', 'css', 'jpg']); + }); + }); + it('should visualize correctly using adhoc data view', async () => { await PageObjects.discover.createAdHocDataView('logst', true); await PageObjects.header.waitUntilLoadingHasFinished(); From 76d8e355e2cf728d9f3c771d1476359707687584 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 15 Nov 2022 16:12:25 +0300 Subject: [PATCH 52/84] [Discover] fix tests --- x-pack/plugins/data_visualizer/tsconfig.json | 3 +- .../form_based_suggestions.test.tsx | 52 +++++++++++++++++++ .../form_based/form_based_suggestions.ts | 7 +-- x-pack/plugins/observability/tsconfig.json | 3 +- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 6f7e80c713467..aef3ee72ababb 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -29,6 +29,7 @@ { "path": "../cloud/tsconfig.json" }, { "path": "../cloud_integrations/cloud_chat/tsconfig.json" }, { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/unified_histogram/tsconfig.json" } + { "path": "../../../src/plugins/unified_histogram/tsconfig.json" }, + { "path": "../../../src/plugins/discover/tsconfig.json" } ] } diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx index eca0c032ee224..8e2e19e159ed7 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx @@ -2150,6 +2150,58 @@ describe('IndexPattern Data Source suggestions', () => { }) ); }); + + it('should apply a bucketed aggregation for a date field, using breakdown', () => { + const suggestions = getDatasourceSuggestionsForVisualizeField( + stateWithoutLayer(), + '1', + 'timestamp', + expectedIndexPatterns, + 'dest' + ); + + expect(suggestions).toContainEqual( + expect.objectContaining({ + state: expect.objectContaining({ + layers: { + id1: expect.objectContaining({ + columnOrder: ['id7', 'id8', 'id6'], + columns: { + id6: expect.objectContaining({ + operationType: 'count', + sourceField: '___records___', + }), + id7: expect.objectContaining({ + operationType: 'terms', + label: 'Top 5 values of dest', + }), + id8: expect.objectContaining({ + operationType: 'date_histogram', + }), + }, + }), + }, + }), + table: { + changeType: 'initial', + label: undefined, + isMultiRow: true, + columns: [ + expect.objectContaining({ + columnId: 'id7', + }), + expect.objectContaining({ + columnId: 'id8', + }), + expect.objectContaining({ + columnId: 'id6', + }), + ], + layerId: 'id1', + }, + }) + ); + }); }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts index 222b9cdbd45bf..af167807d060c 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts @@ -568,10 +568,10 @@ function createNewLayerWithBucketAggregation( breakdownField?: IndexPatternField ): FormBasedLayer { const countColumnId = generateId(); - const splitColumnId = generateId(); - const getBreakdownColumn = () => - insertNewColumn({ + const getBreakdownColumn = () => { + const splitColumnId = generateId(); + return insertNewColumn({ op: 'terms', layer: { indexPatternId: indexPattern.id, @@ -584,6 +584,7 @@ function createNewLayerWithBucketAggregation( indexPattern, visualizationGroups: [], }); + }; return insertNewColumn({ op: operation, diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 07e7a45fe3be1..4e701c10cb904 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -30,6 +30,7 @@ { "path": "../translations/tsconfig.json" }, { "path": "../../../src/plugins/unified_search/tsconfig.json"}, { "path": "../../../src/plugins/guided_onboarding/tsconfig.json"}, - { "path": "../../../src/plugins/unified_histogram/tsconfig.json" } + { "path": "../../../src/plugins/unified_histogram/tsconfig.json" }, + { "path": "../../../src/plugins/discover/tsconfig.json" } ] } From 274f0df7079f9858d556053995da102522e1a9f6 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 15 Nov 2022 17:02:25 +0300 Subject: [PATCH 53/84] [Discover] fix remaining tests --- .../editor_frame/suggestion_helpers.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index e86f602465584..f973fceb6c6d9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -217,7 +217,8 @@ describe('suggestion helpers', () => { datasourceStates.mock.state, '1', 'test', - dataViews.indexPatterns + dataViews.indexPatterns, + undefined ); }); @@ -258,13 +259,15 @@ describe('suggestion helpers', () => { multiDatasourceStates.mock.state, '1', 'test', - dataViews.indexPatterns + dataViews.indexPatterns, + undefined ); expect(multiDatasourceMap.mock2.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( multiDatasourceStates.mock2.state, '1', 'test', - dataViews.indexPatterns + dataViews.indexPatterns, + undefined ); expect( multiDatasourceMap.mock3.getDatasourceSuggestionsForVisualizeField From a2b6221779bdd425085661abb02d1095c27161c5 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 16 Nov 2022 16:11:42 +0300 Subject: [PATCH 54/84] [Discover] enable navigation from Lens with breakdown field --- src/plugins/discover/public/locator.ts | 7 ++++++ .../lens/public/app_plugin/lens_top_nav.tsx | 25 +++++++++++++++++++ .../apps/lens/group2/show_underlying_data.ts | 24 ++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts index e59c61c98412c..e803433c4d70c 100644 --- a/src/plugins/discover/public/locator.ts +++ b/src/plugins/discover/public/locator.ts @@ -91,6 +91,10 @@ export interface DiscoverAppLocatorParams extends SerializableRecord { * Hide mini distribution/preview charts when in Field Statistics mode */ hideAggregatedPreview?: boolean; + /** + * Breakdown field + */ + breakdownField?: string; } export type DiscoverAppLocator = LocatorPublic; @@ -129,6 +133,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition operationType === 'terms' + ); + if (termsColumn) { + breakdownField = (termsColumn as FieldBasedIndexPatternColumn | undefined) + ?.sourceField; + } + } + return discoverLocator.getRedirectUrl({ dataViewSpec: dataViews.indexPatterns[meta.id]?.spec, timeRange: data.query.timefilter.timefilter.getTime(), filters: newFilters, query: isOnTextBasedMode ? query : newQuery, columns: meta.columns, + breakdownField, }); }, openSettings: (anchorElement: HTMLElement) => @@ -617,9 +639,12 @@ export const LensTopNavMenu = ({ query, filters, indexPatterns, + activeDatasourceId, dataViews.indexPatterns, data.query.timefilter.timefilter, isOnTextBasedMode, + datasourceMap, + datasourceStates, lensStore, theme$, ]); diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts index 29f684c1ebf37..a8c5e415864c2 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts @@ -42,6 +42,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); + + // check breakdown values + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['png', 'css', 'jpg']); + // check the table columns const columns = await PageObjects.discover.getColumnHeaders(); expect(columns).to.eql(['extension.raw', '@timestamp', 'bytes']); @@ -68,6 +73,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); + + // check breakdown values + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['Other', 'png', 'css', 'jpg']); + expect(await queryBar.getQueryString()).be.eql(''); await browser.closeCurrentWindow(); await browser.switchToWindow(lensWindowHandler); @@ -104,6 +114,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); + + // check breakdown values + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['png', 'css', 'jpg']); + // check the query expect(await queryBar.getQueryString()).be.eql( '( ( extension.raw: "png" ) OR ( extension.raw: "css" ) OR ( extension.raw: "jpg" ) )' @@ -140,6 +155,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); + + // check breakdown values + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['css', 'jpg']); + // check the columns const columns = await PageObjects.discover.getColumnHeaders(); expect(columns).to.eql(['extension.raw', '@timestamp', 'memory']); @@ -176,6 +196,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); + // check breakdown values + const list = await PageObjects.discover.getHistogramLegendList(); + expect(list).to.eql(['css', 'jpg']); + // check the query expect(await queryBar.getQueryString()).be.eql( '( ( bytes > 4000 ) AND ( ( extension.raw: "css" ) OR ( extension.raw: "gif" ) OR ( extension.raw: "jpg" ) ) )' From f3f3f88696535194fecc0ebe631d70f3ac0f784d Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 09:45:16 -0400 Subject: [PATCH 55/84] [Discover] Cleaning up some component and prop names, and fixing duplicate requests on chart hidden/shown --- .../layout/discover_histogram_layout.tsx | 27 +++++----- ...est.tsx => discover_main_content.test.tsx} | 11 ++--- ..._content.tsx => discover_main_content.tsx} | 14 ++---- .../layout/use_discover_histogram.ts | 49 +++++++++++-------- 4 files changed, 48 insertions(+), 53 deletions(-) rename src/plugins/discover/public/application/main/components/layout/{discover_histogram_content.test.tsx => discover_main_content.test.tsx} (94%) rename src/plugins/discover/public/application/main/components/layout/{discover_histogram_content.tsx => discover_main_content.tsx} (89%) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx index a3678ff4a7d34..7f7e573e000a9 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx @@ -12,13 +12,10 @@ import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverHistogram } from './use_discover_histogram'; import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; import type { InspectorAdapters } from '../../hooks/use_inspector'; -import { - type CommonDiscoverHistogramProps, - DiscoverHistogramContent, -} from './discover_histogram_content'; +import { type DiscoverMainContentProps, DiscoverMainContent } from './discover_main_content'; import { ResetSearchButton } from './reset_search_button'; -export interface DiscoverHistogramLayoutProps extends CommonDiscoverHistogramProps { +export interface DiscoverHistogramLayoutProps extends DiscoverMainContentProps { resetSavedSearch: () => void; isTimeBased: boolean; resizeRef: RefObject; @@ -38,11 +35,11 @@ export const DiscoverHistogramLayout = ({ resizeRef, inspectorAdapters, searchSessionManager, - ...histogramContentProps + ...mainContentProps }: DiscoverHistogramLayoutProps) => { const services = useDiscoverServices(); - const commonInputProps = { + const commonProps = { dataView, isPlainRecord, stateContainer, @@ -51,14 +48,14 @@ export const DiscoverHistogramLayout = ({ savedSearchData$, }; - const { shouldRender, ...histogramProps } = useDiscoverHistogram({ + const histogramProps = useDiscoverHistogram({ isTimeBased, inspectorAdapters, searchSessionManager, - ...commonInputProps, + ...commonProps, }); - if (!shouldRender) { + if (!histogramProps) { return null; } @@ -73,10 +70,12 @@ export const DiscoverHistogramLayout = ({ } {...histogramProps} > - ); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx similarity index 94% rename from src/plugins/discover/public/application/main/components/layout/discover_histogram_content.test.tsx rename to src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx index 1165669f449bb..25ba2bb70027a 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx @@ -24,10 +24,7 @@ import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; -import { - DiscoverHistogramContent, - DiscoverHistogramContentProps, -} from './discover_histogram_content'; +import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content'; import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { CoreTheme } from '@kbn/core/public'; import { act } from 'react-dom/test-utils'; @@ -81,7 +78,7 @@ const mountComponent = async ({ availableFields$, }; - const props: DiscoverHistogramContentProps = { + const props: DiscoverMainContentProps = { isPlainRecord, dataView: dataViewMock, navigateTo: jest.fn(), @@ -109,7 +106,7 @@ const mountComponent = async ({ const component = mountWithIntl( - + ); @@ -122,7 +119,7 @@ const mountComponent = async ({ return component; }; -describe('Discover histogram content component', () => { +describe('Discover main content component', () => { describe('DocumentViewModeToggle', () => { it('should show DocumentViewModeToggle when isPlainRecord is false', async () => { const component = await mountComponent(); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx similarity index 89% rename from src/plugins/discover/public/application/main/components/layout/discover_histogram_content.tsx rename to src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index e1785f37362d1..78252702dccc7 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_histogram_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -23,7 +23,7 @@ import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stat const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); -export interface CommonDiscoverHistogramProps { +export interface DiscoverMainContentProps { dataView: DataView; savedSearch: SavedSearch; isPlainRecord: boolean; @@ -40,11 +40,7 @@ export interface CommonDiscoverHistogramProps { stateContainer: GetStateReturn; } -export interface DiscoverHistogramContentProps extends CommonDiscoverHistogramProps { - chartHidden?: boolean; -} - -export const DiscoverHistogramContent = ({ +export const DiscoverMainContent = ({ dataView, isPlainRecord, navigateTo, @@ -59,8 +55,7 @@ export const DiscoverHistogramContent = ({ state, stateContainer, savedSearch, - chartHidden, -}: DiscoverHistogramContentProps) => { +}: DiscoverMainContentProps) => { const { trackUiMetric } = useDiscoverServices(); const setDiscoverViewMode = useCallback( @@ -93,9 +88,6 @@ export const DiscoverHistogramContent = ({ )} {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( isPlainRecord || !isTimeBased ? undefined : { - hidden: state.hideChart, + hidden: chartHidden, timeInterval: state.interval, }, - [isPlainRecord, isTimeBased, state.hideChart, state.interval] + [chartHidden, isPlainRecord, isTimeBased, state.interval] ); - return { - // The histogram layout shouldn't render until the first search - // request has started, or an immediate refetch of the histogram - // will be triggered when the searchSessionId is set - shouldRender: Boolean(searchSessionId), - topPanelHeight, - request, - hits, - chart, - breakdown, - onEditVisualization: canVisualize ? onEditVisualization : undefined, - onTopPanelHeightChange, - onChartHiddenChange, - onTimeIntervalChange, - onBreakdownFieldChange, - onTotalHitsChange, - onChartLoad, - }; + // state.chartHidden is updated before searchSessionId, which can trigger duplicate + // requests, so instead of using state.chartHidden directly, we update chartHidden + // when searchSessionId changes + useEffect(() => { + setChartHidden(state.hideChart); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchSessionId]); + + return searchSessionId + ? { + topPanelHeight, + request, + hits, + chart, + breakdown, + onEditVisualization: canVisualize ? onEditVisualization : undefined, + onTopPanelHeightChange, + onChartHiddenChange, + onTimeIntervalChange, + onBreakdownFieldChange, + onTotalHitsChange, + onChartLoad, + } + : undefined; }; From 1ddef1658694e322c07df17426be5216c33d4ef1 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 10:29:23 -0400 Subject: [PATCH 56/84] [Discover] Fix use_discover_histogram.test.ts types --- .../layout/use_discover_histogram.test.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts index 59b2e0492f62b..197fac9c58569 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts @@ -129,38 +129,38 @@ describe('useDiscoverHistogram', () => { describe('contexts', () => { it('should output the correct hits context', async () => { const { result } = await renderUseDiscoverHistogram(); - expect(result.current.hits?.status).toBe(FetchStatus.COMPLETE); - expect(result.current.hits?.total).toEqual(esHits.length); + expect(result.current?.hits?.status).toBe(FetchStatus.COMPLETE); + expect(result.current?.hits?.total).toEqual(esHits.length); }); it('should output the correct chart context', async () => { const { result } = await renderUseDiscoverHistogram(); - expect(result.current.chart?.hidden).toBe(false); - expect(result.current.chart?.timeInterval).toBe('auto'); + expect(result.current?.chart?.hidden).toBe(false); + expect(result.current?.chart?.timeInterval).toBe('auto'); }); it('should output undefined for hits and chart if isPlainRecord is true', async () => { const { result } = await renderUseDiscoverHistogram({ isPlainRecord: true }); - expect(result.current.hits).toBeUndefined(); - expect(result.current.chart).toBeUndefined(); + expect(result.current?.hits).toBeUndefined(); + expect(result.current?.chart).toBeUndefined(); }); it('should output undefined for chart if isTimeBased is false', async () => { const { result } = await renderUseDiscoverHistogram({ isTimeBased: false }); - expect(result.current.hits).not.toBeUndefined(); - expect(result.current.chart).toBeUndefined(); + expect(result.current?.hits).not.toBeUndefined(); + expect(result.current?.chart).toBeUndefined(); }); }); describe('onEditVisualization', () => { it('returns a callback for onEditVisualization when the data view can be visualized', async () => { const { result } = await renderUseDiscoverHistogram(); - expect(result.current.onEditVisualization).toBeDefined(); + expect(result.current?.onEditVisualization).toBeDefined(); }); it('returns undefined for onEditVisualization when the data view cannot be visualized', async () => { const { result } = await renderUseDiscoverHistogram({ canVisualize: false }); - expect(result.current.onEditVisualization).toBeUndefined(); + expect(result.current?.onEditVisualization).toBeUndefined(); }); }); @@ -170,7 +170,7 @@ describe('useDiscoverHistogram', () => { storage.get = jest.fn(() => 100); const { result } = await renderUseDiscoverHistogram({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); - expect(result.current.topPanelHeight).toBe(100); + expect(result.current?.topPanelHeight).toBe(100); }); it('should update topPanelHeight when onTopPanelHeightChange is called', async () => { @@ -178,12 +178,12 @@ describe('useDiscoverHistogram', () => { storage.get = jest.fn(() => 100); storage.set = jest.fn(); const { result } = await renderUseDiscoverHistogram({ storage }); - expect(result.current.topPanelHeight).toBe(100); + expect(result.current?.topPanelHeight).toBe(100); act(() => { - result.current.onTopPanelHeightChange(200); + result.current?.onTopPanelHeightChange(200); }); expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, 200); - expect(result.current.topPanelHeight).toBe(200); + expect(result.current?.topPanelHeight).toBe(200); }); }); @@ -199,7 +199,7 @@ describe('useDiscoverHistogram', () => { stateContainer, }); act(() => { - result.current.onChartHiddenChange(true); + result.current?.onChartHiddenChange(true); }); expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, true); expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: true }); @@ -213,7 +213,7 @@ describe('useDiscoverHistogram', () => { stateContainer, }); act(() => { - result.current.onTimeIntervalChange('auto'); + result.current?.onTimeIntervalChange('auto'); }); expect(stateContainer.setAppState).toHaveBeenCalledWith({ interval: 'auto' }); }); From f2eda4a589242b5f340333d14c83a6e418a8a6d1 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 10:41:22 -0400 Subject: [PATCH 57/84] [Discover] Show current time marker --- .../unified_histogram/public/chart/get_lens_attributes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts index d1eb7313ab5a5..ab443ed7b4f82 100644 --- a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -151,6 +151,7 @@ export const getLensAttributes = ({ preferredSeriesType: 'bar_stacked', valueLabels: 'hide', fittingFunction: 'None', + showCurrentTimeMarker: true, axisTitlesVisibilitySettings: { x: false, yLeft: false, From 6c660d03070b5cc72c2bc9d1f63535f1eee4006d Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 13:17:27 -0400 Subject: [PATCH 58/84] [Discover] Fixed issue with total hits loading flicker on refetch --- .../layout/use_discover_histogram.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index dd9ab9124a7d1..eda747ff04c39 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -13,7 +13,10 @@ import { triggerVisualizeActions, } from '@kbn/unified-field-list-plugin/public'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; +import { + UnifiedHistogramFetchStatus, + UnifiedHistogramHitsContext, +} from '@kbn/unified-histogram-plugin/public'; import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; import useObservable from 'react-use/lib/useObservable'; import { getUiActions } from '../../../../kibana_services'; @@ -158,35 +161,36 @@ export const useDiscoverHistogram = ({ * Total hits */ - const sendTotalHitsError = useMemo( - () => sendErrorTo(data, savedSearchData$.totalHits$), - [data, savedSearchData$.totalHits$] - ); + const [localHitsContext, setLocalHitsContext] = useState(); const onTotalHitsChange = useCallback( (status: UnifiedHistogramFetchStatus, result?: number | Error) => { if (result instanceof Error) { - sendTotalHitsError(result); + sendErrorTo(data, savedSearchData$.totalHits$); return; } const { fetchStatus, recordRawType } = savedSearchData$.totalHits$.getValue(); - // If we have a partial result already, we don't - // want to update the total hits back to loading + // If we have a partial result already, we don't want to update the total hits back to loading if (fetchStatus === FetchStatus.PARTIAL && status === UnifiedHistogramFetchStatus.loading) { return; } + setLocalHitsContext({ status, total: result }); + savedSearchData$.totalHits$.next({ fetchStatus: status.toString() as FetchStatus, result, recordRawType, }); }, - [savedSearchData$.totalHits$, sendTotalHitsError] + [data, savedSearchData$.totalHits$] ); + // We only rely on the totalHits$ observable if we don't have a local hits context yet, + // since we only want to show the partial results on the first load, or there will be + // a flickering effect as the loading spinner is quickly shown and hidden again on fetches const { fetchStatus: hitsFetchStatus, result: hitsTotal } = useDataState( savedSearchData$.totalHits$ ); @@ -195,11 +199,11 @@ export const useDiscoverHistogram = ({ () => isPlainRecord ? undefined - : { + : localHitsContext ?? { status: hitsFetchStatus.toString() as UnifiedHistogramFetchStatus, total: hitsTotal, }, - [hitsFetchStatus, hitsTotal, isPlainRecord] + [hitsFetchStatus, hitsTotal, isPlainRecord, localHitsContext] ); /** From e66de997528ef12bd2ca4a6b0d08e41b14139199 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 14:37:32 -0400 Subject: [PATCH 59/84] [Discover] Fix and add new tests for use_discover_histogram.test.tsx --- .../discover/public/__mocks__/search_session.ts | 8 +++----- .../layout/use_discover_histogram.test.ts | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/search_session.ts b/src/plugins/discover/public/__mocks__/search_session.ts index 9763ff7089e0a..8dd6d8e05e8f8 100644 --- a/src/plugins/discover/public/__mocks__/search_session.ts +++ b/src/plugins/discover/public/__mocks__/search_session.ts @@ -7,14 +7,12 @@ */ import { createMemoryHistory } from 'history'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DiscoverSearchSessionManager } from '../application/main/services/discover_search_session'; -export function createSearchSessionMock() { +export function createSearchSessionMock( + session = dataPluginMock.createStartContract().search.session +) { const history = createMemoryHistory(); - const session = dataPluginMock.createStartContract().search.session as jest.Mocked< - DataPublicPluginStart['search']['session'] - >; const searchSessionManager = new DiscoverSearchSessionManager({ history, session, diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts index 197fac9c58569..5384c698c1468 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts @@ -33,6 +33,7 @@ import { setTimeout } from 'timers/promises'; import { calculateBounds } from '@kbn/data-plugin/public'; import { createSearchSessionMock } from '../../../../__mocks__/search_session'; import { RequestAdapter } from '@kbn/inspector-plugin/public'; +import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks'; const mockData = dataPluginMock.createStartContract(); @@ -69,12 +70,14 @@ describe('useDiscoverHistogram', () => { canVisualize = true, storage = new LocalStorageMock({}) as unknown as Storage, stateContainer = {}, + searchSessionId = '123', }: { isPlainRecord?: boolean; isTimeBased?: boolean; canVisualize?: boolean; storage?: Storage; stateContainer?: unknown; + searchSessionId?: string | null; } = {}) => { mockStorage = storage; mockCanVisualize = canVisualize; @@ -107,6 +110,11 @@ describe('useDiscoverHistogram', () => { availableFields$, }; + const searchSessionService = { + ...getSessionServiceMock(), + getSession$: () => new BehaviorSubject(searchSessionId ?? undefined), + }; + const hook = renderHook(() => { return useDiscoverHistogram({ stateContainer: stateContainer as GetStateReturn, @@ -117,7 +125,7 @@ describe('useDiscoverHistogram', () => { isTimeBased, isPlainRecord, inspectorAdapters: { requests: new RequestAdapter() }, - searchSessionManager: createSearchSessionMock().searchSessionManager, + searchSessionManager: createSearchSessionMock(searchSessionService).searchSessionManager, }); }); @@ -126,6 +134,11 @@ describe('useDiscoverHistogram', () => { return hook; }; + it('should return undefined if there is no search session', async () => { + const { result } = await renderUseDiscoverHistogram({ searchSessionId: null }); + expect(result.current).toBeUndefined(); + }); + describe('contexts', () => { it('should output the correct hits context', async () => { const { result } = await renderUseDiscoverHistogram(); From c3b315acf316daabf314d6faeec1e13bc24c9ac0 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 16:22:11 -0400 Subject: [PATCH 60/84] [Discover] Make sure the breakdown field is accounted for when saving a search session, otherwise restored search sessions using the breakdown field will fail to load properly --- .../discover/public/application/main/services/discover_state.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 23042a4b955f6..77e870dfc75a5 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -451,5 +451,6 @@ function createUrlGeneratorState({ useHash: false, viewMode: appState.viewMode, hideAggregatedPreview: appState.hideAggregatedPreview, + breakdownField: appState.breakdownField, }; } From 932d2762c8ea206d157a8db9c1fc32b965867192 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 16:38:31 -0400 Subject: [PATCH 61/84] [Discover] Fix broken types from Jest test fix --- src/plugins/discover/public/__mocks__/search_session.ts | 5 ++++- .../components/layout/use_discover_histogram.test.ts | 9 ++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/search_session.ts b/src/plugins/discover/public/__mocks__/search_session.ts index 8dd6d8e05e8f8..abcd5e92a1cbd 100644 --- a/src/plugins/discover/public/__mocks__/search_session.ts +++ b/src/plugins/discover/public/__mocks__/search_session.ts @@ -7,10 +7,13 @@ */ import { createMemoryHistory } from 'history'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DiscoverSearchSessionManager } from '../application/main/services/discover_search_session'; export function createSearchSessionMock( - session = dataPluginMock.createStartContract().search.session + session = dataPluginMock.createStartContract().search.session as jest.Mocked< + DataPublicPluginStart['search']['session'] + > ) { const history = createMemoryHistory(); const searchSessionManager = new DiscoverSearchSessionManager({ diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts index 5384c698c1468..616e5ee2b0148 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts @@ -110,10 +110,9 @@ describe('useDiscoverHistogram', () => { availableFields$, }; - const searchSessionService = { - ...getSessionServiceMock(), - getSession$: () => new BehaviorSubject(searchSessionId ?? undefined), - }; + const session = getSessionServiceMock(); + + session.getSession$.mockReturnValue(new BehaviorSubject(searchSessionId ?? undefined)); const hook = renderHook(() => { return useDiscoverHistogram({ @@ -125,7 +124,7 @@ describe('useDiscoverHistogram', () => { isTimeBased, isPlainRecord, inspectorAdapters: { requests: new RequestAdapter() }, - searchSessionManager: createSearchSessionMock(searchSessionService).searchSessionManager, + searchSessionManager: createSearchSessionMock(session).searchSessionManager, }); }); From 9fd9c5b0e0845b9b60cca0fa14d78bb6dbef8209 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 21:31:41 -0400 Subject: [PATCH 62/84] [Discover] Updating and fixing Jest tests --- .../layout/discover_histogram_layout.test.tsx | 53 +++++++++++++++++-- .../layout/discover_layout.test.tsx | 38 ++++++++++++- .../layout/discover_main_content.test.tsx | 25 ++++++++- .../layout/discover_main_content.tsx | 2 +- .../application/main/utils/fetch_all.test.ts | 1 - .../public/chart/chart.test.tsx | 2 +- 6 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx index 9a21f7b419e4a..5f2b0f9e434d7 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx @@ -35,6 +35,9 @@ import { HISTOGRAM_HEIGHT_KEY } from './use_discover_histogram'; import { createSearchSessionMock } from '../../../../__mocks__/search_session'; import { RequestAdapter } from '@kbn/inspector-plugin/public'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; +import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks'; +import { ResetSearchButton } from './reset_search_button'; const mountComponent = async ({ isPlainRecord = false, @@ -96,6 +99,10 @@ const mountComponent = async ({ availableFields$, }; + const session = getSessionServiceMock(); + + session.getSession$.mockReturnValue(new BehaviorSubject('123')); + const props: DiscoverHistogramLayoutProps = { isPlainRecord, dataView: dataViewMock, @@ -120,7 +127,7 @@ const mountComponent = async ({ resetSavedSearch, isTimeBased, resizeRef: { current: null }, - searchSessionManager: createSearchSessionMock().searchSessionManager, + searchSessionManager: createSearchSessionMock(session).searchSessionManager, inspectorAdapters: { requests: new RequestAdapter() }, }; @@ -156,18 +163,58 @@ describe('Discover histogram layout component', () => { const storage = new LocalStorageMock({}) as unknown as Storage; const originalGet = storage.get; storage.get = jest.fn().mockImplementation(originalGet); - await mountComponent({ storage }); + const component = await mountComponent({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); expect(storage.get).toHaveReturnedWith(null); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(undefined); }); it('should pass the stored topPanelHeight to UnifiedHistogramLayout if a value is found in storage', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; const topPanelHeight = 123; storage.get = jest.fn().mockImplementation(() => topPanelHeight); - await mountComponent({ storage }); + const component = await mountComponent({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); expect(storage.get).toHaveReturnedWith(topPanelHeight); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(topPanelHeight); + }); + + it('should update the topPanelHeight in storage and pass the new value to UnifiedHistogramLayout when the topPanelHeight changes', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + const originalSet = storage.set; + storage.set = jest.fn().mockImplementation(originalSet); + const component = await mountComponent({ storage }); + const newTopPanelHeight = 123; + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).not.toBe( + newTopPanelHeight + ); + act(() => { + component.find(UnifiedHistogramLayout).prop('onTopPanelHeightChange')!(newTopPanelHeight); + }); + component.update(); + expect(storage.set).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY, newTopPanelHeight); + expect(component.find(UnifiedHistogramLayout).prop('topPanelHeight')).toBe(newTopPanelHeight); + }); + }); + + describe('reset search button', () => { + it('renders the button when there is a saved search', async () => { + const component = await mountComponent(); + expect(component.find(ResetSearchButton).exists()).toBe(true); + }); + + it('does not render the button when there is no saved search', async () => { + const component = await mountComponent({ + savedSearch: { ...savedSearchMock, id: undefined }, + }); + expect(component.find(ResetSearchButton).exists()).toBe(false); + }); + + it('should call resetSavedSearch when clicked', async () => { + const resetSavedSearch = jest.fn(); + const component = await mountComponent({ resetSavedSearch }); + component.find(ResetSearchButton).find('button').simulate('click'); + expect(resetSavedSearch).toHaveBeenCalled(); }); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 60c443c61d7af..ccdba3d0604cb 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -13,6 +13,7 @@ import type { Query, AggregateQuery } from '@kbn/es-query'; import { setHeaderActionMenuMounter } from '../../../../kibana_services'; import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout'; import { esHits } from '../../../../__mocks__/es_hits'; +import { dataViewMock } from '../../../../__mocks__/data_view'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { createSearchSourceMock, @@ -42,6 +43,7 @@ import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock' import { setTimeout } from 'timers/promises'; import { act } from 'react-dom/test-utils'; import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks'; function getAppStateContainer() { const appStateContainer = getDiscoverStateMock({ isTimeBased: true }).appStateContainer; @@ -110,6 +112,10 @@ async function mountComponent( availableFields$, }; + const session = getSessionServiceMock(); + + session.getSession$.mockReturnValue(new BehaviorSubject('123')); + const props = { dataView, dataViewList, @@ -135,7 +141,7 @@ async function mountComponent( persistDataView: jest.fn(), updateAdHocDataViewId: jest.fn(), adHocDataViewList: [], - searchSessionManager: createSearchSessionMock().searchSessionManager, + searchSessionManager: createSearchSessionMock(session).searchSessionManager, savedDataViewList: [], updateDataViewList: jest.fn(), }; @@ -157,6 +163,36 @@ async function mountComponent( } describe('Discover component', () => { + test('selected data view without time field displays no chart toggle', async () => { + const container = document.createElement('div'); + await mountComponent(dataViewMock, undefined, { attachTo: container }); + expect( + container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') + ).toBeNull(); + }); + + test('selected data view with time field displays chart toggle', async () => { + const container = document.createElement('div'); + await mountComponent(dataViewWithTimefieldMock, undefined, { attachTo: container }); + expect( + container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') + ).not.toBeNull(); + }); + + test('sql query displays no chart toggle', async () => { + const container = document.createElement('div'); + await mountComponent( + dataViewWithTimefieldMock, + false, + { attachTo: container }, + { sql: 'SELECT * FROM test' }, + true + ); + expect( + container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]') + ).toBeNull(); + }); + test('the saved search title h1 gains focus on navigate', async () => { const container = document.createElement('div'); document.body.appendChild(container); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx index 25ba2bb70027a..de061ccf09c9c 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx @@ -24,18 +24,25 @@ import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '../../../../utils/build_data_record'; -import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content'; +import { + DiscoverMainContent, + DiscoverMainContentProps, + FieldStatisticsTableMemoized, +} from './discover_main_content'; import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { CoreTheme } from '@kbn/core/public'; import { act } from 'react-dom/test-utils'; import { setTimeout } from 'timers/promises'; import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { DiscoverDocuments } from './discover_documents'; const mountComponent = async ({ isPlainRecord = false, + viewMode = VIEW_MODE.DOCUMENT_LEVEL, }: { isPlainRecord?: boolean; + viewMode?: VIEW_MODE; } = {}) => { const services = discoverServiceMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { @@ -97,7 +104,7 @@ const mountComponent = async ({ } as unknown as GetStateReturn, onFieldEdited: jest.fn(), columns: [], - viewMode: VIEW_MODE.DOCUMENT_LEVEL, + viewMode, onAddFilter: jest.fn(), }; @@ -131,4 +138,18 @@ describe('Discover main content component', () => { expect(component.find(DocumentViewModeToggle).exists()).toBe(false); }); }); + + describe('Document view', () => { + it('should show DiscoverDocuments when VIEW_MODE is DOCUMENT_LEVEL', async () => { + const component = await mountComponent(); + expect(component.find(DiscoverDocuments).exists()).toBe(true); + expect(component.find(FieldStatisticsTableMemoized).exists()).toBe(false); + }); + + it('should show FieldStatisticsTableMemoized when VIEW_MODE is not DOCUMENT_LEVEL', async () => { + const component = await mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL }); + expect(component.find(DiscoverDocuments).exists()).toBe(false); + expect(component.find(FieldStatisticsTableMemoized).exists()).toBe(true); + }); + }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index 78252702dccc7..e1953d2a68714 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -21,7 +21,7 @@ import { FieldStatisticsTable } from '../field_stats_table'; import { DiscoverDocuments } from './discover_documents'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; -const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); +export const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable); export interface DiscoverMainContentProps { dataView: DataView; diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index a4f2a6085abb6..5258de6bdfdad 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -233,7 +233,6 @@ describe('test fetchAll', () => { expect(await collectMain()).toEqual([ { fetchStatus: FetchStatus.UNINITIALIZED }, { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' }, - // { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document' }, // There is no partial, since documents query failed { fetchStatus: FetchStatus.ERROR, error: { msg: 'This query failed' }, diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index d194f4c044fa6..150c2f86dddeb 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -163,7 +163,7 @@ describe('Chart', () => { expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeFalsy(); }); - it('should render chart if data view is not time based', async () => { + it('should render chart if data view is time based', async () => { const component = await mountComponent(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); }); From b3024786c08462e6ca0d61a85fb80f768e381d65 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 21:32:26 -0400 Subject: [PATCH 63/84] [Discover] Add Discover loading spinner back in --- .../components/layout/discover_layout.scss | 2 + .../components/layout/discover_layout.tsx | 46 ++++++++++-------- .../layout/use_discover_histogram.ts | 8 +++- .../loading_spinner/loading_spinner.test.tsx | 23 +++++++++ .../loading_spinner/loading_spinner.tsx | 44 +++++++++++++++++ .../main/hooks/use_saved_search_messages.ts | 35 ++++++++++++++ .../application/main/utils/fetch_all.ts | 48 ++----------------- 7 files changed, 139 insertions(+), 67 deletions(-) create mode 100644 src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx create mode 100644 src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index 70963f50b96a7..7dfcdfa607973 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -41,6 +41,8 @@ discover-app { } .dscPageContent { + position: relative; + overflow: hidden; border: $euiBorderThin; } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index 8bba4008a231e..08e6cec024ef2 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -26,6 +26,7 @@ import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/pu import { useInspector } from '../../hooks/use_inspector'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverNoResults } from '../no_results'; +import { LoadingSpinner } from '../loading_spinner/loading_spinner'; import { DiscoverSidebarResponsive } from '../sidebar'; import { DiscoverLayoutProps } from './types'; import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS } from '../../../../../common'; @@ -216,27 +217,30 @@ export function DiscoverLayout({ } return ( - + <> + + {resultState === 'loading' && } + ); }, [ columns, diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index eda747ff04c39..9b10f7f4f3834 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -28,7 +28,7 @@ import type { AppState, GetStateReturn } from '../../services/discover_state'; import { FetchStatus } from '../../../types'; import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; import type { InspectorAdapters } from '../../hooks/use_inspector'; -import { sendErrorTo } from '../../utils/fetch_all'; +import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; export const CHART_HIDDEN_KEY = 'discover:chartHidden'; export const HISTOGRAM_HEIGHT_KEY = 'discover:histogramHeight'; @@ -184,8 +184,12 @@ export const useDiscoverHistogram = ({ result, recordRawType, }); + + if (status === UnifiedHistogramFetchStatus.complete && typeof result === 'number') { + checkHitCount(savedSearchData$.main$, result); + } }, - [data, savedSearchData$.totalHits$] + [data, savedSearchData$.main$, savedSearchData$.totalHits$] ); // We only rely on the totalHits$ observable if we don't have a local hits context yet, diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx new file mode 100644 index 0000000000000..36978a1f72684 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.test.tsx @@ -0,0 +1,23 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { ReactWrapper } from 'enzyme'; +import { LoadingSpinner } from './loading_spinner'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +describe('loading spinner', function () { + let component: ReactWrapper; + + it('LoadingSpinner renders a Searching text and a spinner', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'loadingSpinnerText').text()).toBe('Searching'); + expect(findTestSubject(component, 'loadingSpinner').length).toBe(1); + }); +}); diff --git a/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx new file mode 100644 index 0000000000000..1879b5267bbf5 --- /dev/null +++ b/src/plugins/discover/public/application/main/components/loading_spinner/loading_spinner.tsx @@ -0,0 +1,44 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiLoadingSpinner, + EuiTitle, + EuiSpacer, + useEuiPaddingSize, + useEuiBackgroundColor, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; + +export function LoadingSpinner() { + const loadingSpinnerCss = css` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + padding: ${useEuiPaddingSize('l')} 0; + background-color: ${useEuiBackgroundColor('plain')}; + z-index: 3; + `; + + return ( +
+ +

+ +

+
+ + +
+ ); +} diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts index 1657534353a75..ab121f76e15a0 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../types'; import type { @@ -105,3 +106,37 @@ export function sendResetMsg(data: SavedSearchData, initialFetchStatus: FetchSta recordRawType, }); } + +/** + * Method to create an error handler that will forward the received error + * to the specified subjects. It will ignore AbortErrors and will use the data + * plugin to show a toast for the error (e.g. allowing better insights into shard failures). + */ +export const sendErrorTo = ( + data: DataPublicPluginStart, + ...errorSubjects: Array +) => { + return (error: Error) => { + if (error instanceof Error && error.name === 'AbortError') { + return; + } + + data.search.showError(error); + errorSubjects.forEach((subject) => sendErrorMsg(subject, error)); + }; +}; + +/** + * This method checks the passed in hit count and will send a PARTIAL message to main$ + * if there are results, indicating that we have finished some of the requests that have been + * sent. If there are no results we already COMPLETE main$ with no results found, so Discover + * can show the "no results" screen. We know at that point, that the other query returning + * will neither carry any data, since there are no documents. + */ +export const checkHitCount = (main$: DataMain$, hitsCount: number) => { + if (hitsCount > 0) { + sendPartialMsg(main$); + } else { + sendNoResultsFoundMsg(main$); + } +}; diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 513f2f85e33e7..d782442db3953 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -12,24 +12,18 @@ import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; import { BehaviorSubject, filter, firstValueFrom, map, merge, scan } from 'rxjs'; import { getRawRecordType } from './get_raw_record_type'; import { + checkHitCount, sendCompleteMsg, sendErrorMsg, + sendErrorTo, sendLoadingMsg, - sendNoResultsFoundMsg, - sendPartialMsg, sendResetMsg, } from '../hooks/use_saved_search_messages'; import { updateSearchSource } from './update_search_source'; import { fetchDocuments } from './fetch_documents'; import { AppState } from '../services/discover_state'; import { FetchStatus } from '../../types'; -import { - DataDocuments$, - DataMain$, - DataMsg, - RecordRawType, - SavedSearchData, -} from '../hooks/use_saved_search'; +import { DataMsg, RecordRawType, SavedSearchData } from '../hooks/use_saved_search'; import { DiscoverServices } from '../../../build_services'; import { fetchSql } from './fetch_sql'; @@ -45,25 +39,6 @@ export interface FetchDeps { useNewFieldsApi: boolean; } -/** - * Method to create an error handler that will forward the received error - * to the specified subjects. It will ignore AbortErrors and will use the data - * plugin to show a toast for the error (e.g. allowing better insights into shard failures). - */ -export const sendErrorTo = ( - data: DataPublicPluginStart, - ...errorSubjects: Array -) => { - return (error: Error) => { - if (error instanceof Error && error.name === 'AbortError') { - return; - } - - data.search.showError(error); - errorSubjects.forEach((subject) => sendErrorMsg(subject, error)); - }; -}; - /** * This function starts fetching all required queries in Discover. This will be the query to load the individual * documents as well as any other requests that might be required to load the main view. @@ -109,21 +84,6 @@ export function fetchAll( ? fetchSql(query, services.dataViews, data, services.expressions) : fetchDocuments(searchSource.createCopy(), fetchDeps); - /** - * This method checks the passed in hit count and will send a PARTIAL message to main$ - * if there are results, indicating that we have finished some of the requests that have been - * sent. If there are no results we already COMPLETE main$ with no results found, so Discover - * can show the "no results" screen. We know at that point, that the other query returning - * will neither carry any data, since there are no documents. - */ - const checkHitCount = (hitsCount: number) => { - if (hitsCount > 0) { - sendPartialMsg(dataSubjects.main$); - } else { - sendNoResultsFoundMsg(dataSubjects.main$); - } - }; - // Handle results of the individual queries and forward the results to the corresponding dataSubjects documents .then((docs) => { @@ -144,7 +104,7 @@ export function fetchAll( query, }); - checkHitCount(docs.length); + checkHitCount(dataSubjects.main$, docs.length); }) // Only the document query should send its errors to main$, to cause the full Discover app // to get into an error state. The other queries will not cause all of Discover to error out From 963ae17b81934507ad9572e04f84d01dff6de241 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 16 Nov 2022 23:08:41 -0400 Subject: [PATCH 64/84] [Discover] Upating comments in use_discover_histogram --- .../main/components/layout/use_discover_histogram.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 9b10f7f4f3834..053794d76a91f 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -166,6 +166,7 @@ export const useDiscoverHistogram = ({ const onTotalHitsChange = useCallback( (status: UnifiedHistogramFetchStatus, result?: number | Error) => { if (result instanceof Error) { + // Display the error and set totalHits$ to an error state sendErrorTo(data, savedSearchData$.totalHits$); return; } @@ -177,14 +178,17 @@ export const useDiscoverHistogram = ({ return; } + // Set a local copy of the hits context to pass to unified histogram setLocalHitsContext({ status, total: result }); + // Sync the totalHits$ observable with the unified histogram state savedSearchData$.totalHits$.next({ fetchStatus: status.toString() as FetchStatus, result, recordRawType, }); + // Check the hits count to set a partial or no results state if (status === UnifiedHistogramFetchStatus.complete && typeof result === 'number') { checkHitCount(savedSearchData$.main$, result); } @@ -255,6 +259,7 @@ export const useDiscoverHistogram = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchSessionId]); + // Don't render the unified histogram layout until the first search has been requested return searchSessionId ? { topPanelHeight, From b53cf5ba71d0a837c6706af1a15cbd72d7346441 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 17 Nov 2022 21:52:35 -0400 Subject: [PATCH 65/84] [Discover] Add Jest tests for new files --- .../layout/reset_search_button.test.tsx | 20 + .../utils/aggregate_request_adapter.test.ts | 42 ++ .../chart/breakdown_field_selector.test.tsx | 73 +++ .../chart/field_supports_breakdown.test.ts | 35 ++ .../public/chart/get_lens_attributes.test.ts | 477 ++++++++++++++++++ .../public/chart/use_chart_actions.test.ts | 82 +++ .../public/chart/use_refetch_id.test.ts | 68 +++ .../public/chart/use_time_range.test.tsx | 239 +++++++++ .../public/chart/use_total_hits.test.ts | 212 ++++++++ 9 files changed, 1248 insertions(+) create mode 100644 src/plugins/discover/public/application/main/components/layout/reset_search_button.test.tsx create mode 100644 src/plugins/discover/public/application/main/utils/aggregate_request_adapter.test.ts create mode 100644 src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx create mode 100644 src/plugins/unified_histogram/public/chart/field_supports_breakdown.test.ts create mode 100644 src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_chart_actions.test.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_refetch_id.test.ts create mode 100644 src/plugins/unified_histogram/public/chart/use_time_range.test.tsx create mode 100644 src/plugins/unified_histogram/public/chart/use_total_hits.test.ts diff --git a/src/plugins/discover/public/application/main/components/layout/reset_search_button.test.tsx b/src/plugins/discover/public/application/main/components/layout/reset_search_button.test.tsx new file mode 100644 index 0000000000000..cde96ff72050f --- /dev/null +++ b/src/plugins/discover/public/application/main/components/layout/reset_search_button.test.tsx @@ -0,0 +1,20 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { ResetSearchButton } from './reset_search_button'; + +describe('ResetSearchButton', () => { + it('should call resetSavedSearch when the button is clicked', () => { + const resetSavedSearch = jest.fn(); + const component = mountWithIntl(); + component.find('button[data-test-subj="resetSavedSearch"]').simulate('click'); + expect(resetSavedSearch).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.test.ts b/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.test.ts new file mode 100644 index 0000000000000..effbb192e863b --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/aggregate_request_adapter.test.ts @@ -0,0 +1,42 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { AggregateRequestAdapter } from './aggregate_request_adapter'; + +describe('AggregateRequestAdapter', () => { + it('should return all requests from all adapters', () => { + const adapter1 = new RequestAdapter(); + const adapter2 = new RequestAdapter(); + const adapter3 = new RequestAdapter(); + const aggregateAdapter = new AggregateRequestAdapter([adapter1, adapter2, adapter3]); + adapter1.start('request1'); + adapter2.start('request2'); + adapter3.start('request3'); + expect(aggregateAdapter.getRequests().map((request) => request.name)).toEqual([ + 'request1', + 'request2', + 'request3', + ]); + }); + + it('should allow adding and removing change listeners for all adapters', () => { + const adapter1 = new RequestAdapter(); + const adapter2 = new RequestAdapter(); + const aggregateAdapter = new AggregateRequestAdapter([adapter1, adapter2]); + const listener = jest.fn(); + aggregateAdapter.addListener('change', listener); + adapter1.start('request1'); + adapter2.start('request2'); + expect(listener).toHaveBeenCalledTimes(2); + aggregateAdapter.removeListener('change', listener); + adapter1.start('request3'); + adapter2.start('request4'); + expect(listener).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx new file mode 100644 index 0000000000000..2b6b8bd7c537f --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { EuiComboBox } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { UnifiedHistogramBreakdownContext } from '..'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { BreakdownFieldSelector } from './breakdown_field_selector'; +import { fieldSupportsBreakdown } from './field_supports_breakdown'; + +describe('BreakdownFieldSelector', () => { + it('should pass fields that support breakdown as options to the EuiComboBox', () => { + const onBreakdownFieldChange = jest.fn(); + const breakdown: UnifiedHistogramBreakdownContext = { + field: undefined, + }; + const wrapper = mountWithIntl( + + ); + const comboBox = wrapper.find(EuiComboBox); + expect(comboBox.prop('options')).toEqual( + dataViewWithTimefieldMock.fields + .filter(fieldSupportsBreakdown) + .map((field) => ({ label: field.displayName, value: field.name })) + .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())) + ); + }); + + it('should pass selectedOptions to the EuiComboBox if breakdown.field is defined', () => { + const onBreakdownFieldChange = jest.fn(); + const field = dataViewWithTimefieldMock.fields.find((f) => f.name === 'extension')!; + const breakdown: UnifiedHistogramBreakdownContext = { field }; + const wrapper = mountWithIntl( + + ); + const comboBox = wrapper.find(EuiComboBox); + expect(comboBox.prop('selectedOptions')).toEqual([ + { label: field.displayName, value: field.name }, + ]); + }); + + it('should call onBreakdownFieldChange with the selected field when the user selects a field', () => { + const onBreakdownFieldChange = jest.fn(); + const breakdown: UnifiedHistogramBreakdownContext = { + field: undefined, + }; + const wrapper = mountWithIntl( + + ); + const comboBox = wrapper.find(EuiComboBox); + const selectedField = dataViewWithTimefieldMock.fields.find((f) => f.name === 'extension')!; + comboBox.prop('onChange')!([{ label: selectedField.displayName, value: selectedField.name }]); + expect(onBreakdownFieldChange).toHaveBeenCalledWith(selectedField); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/field_supports_breakdown.test.ts b/src/plugins/unified_histogram/public/chart/field_supports_breakdown.test.ts new file mode 100644 index 0000000000000..b38b42cf2a249 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/field_supports_breakdown.test.ts @@ -0,0 +1,35 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { fieldSupportsBreakdown } from './field_supports_breakdown'; + +describe('fieldSupportsBreakdown', () => { + it('should return false if field is not aggregatable', () => { + expect( + fieldSupportsBreakdown({ aggregatable: false, scripted: false, type: 'string' } as any) + ).toBe(false); + }); + + it('should return false if field is scripted', () => { + expect( + fieldSupportsBreakdown({ aggregatable: true, scripted: true, type: 'string' } as any) + ).toBe(false); + }); + + it('should return false if field type is not supported', () => { + expect( + fieldSupportsBreakdown({ aggregatable: true, scripted: false, type: 'unsupported' } as any) + ).toBe(false); + }); + + it('should return true if field is aggregatable and type is supported', () => { + expect( + fieldSupportsBreakdown({ aggregatable: true, scripted: false, type: 'string' } as any) + ).toBe(true); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts new file mode 100644 index 0000000000000..d9d1f03a36f8d --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts @@ -0,0 +1,477 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { getLensAttributes } from './get_lens_attributes'; +import { AggregateQuery, Filter, FilterStateStore, Query } from '@kbn/es-query'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; + +describe('getLensAttributes', () => { + const dataView: DataView = dataViewWithTimefieldMock; + const filters: Filter[] = [ + { + meta: { + index: dataView.id, + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'extension', + params: { + query: 'js', + }, + }, + query: { + match: { + extension: { + query: 'js', + type: 'phrase', + }, + }, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }, + ]; + const query: Query | AggregateQuery = { language: 'kuery', query: 'extension : css' }; + const timeInterval = 'auto'; + + it('should return correct attributes', () => { + const breakdownField: DataViewField | undefined = undefined; + expect(getLensAttributes({ filters, query, dataView, timeInterval, breakdownField })) + .toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-layer-unifiedHistogram", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "unifiedHistogram": Object { + "columnOrder": Array [ + "date_column", + "count_column", + ], + "columns": Object { + "count_column": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "format": Object { + "id": "number", + "params": Object { + "decimals": 0, + }, + }, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "date_column": Object { + "dataType": "date", + "isBucketed": true, + "label": "timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "timestamp", + }, + }, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "index-pattern-with-timefield-id", + "key": "extension", + "negate": false, + "params": Object { + "query": "js", + }, + "type": "phrase", + }, + "query": Object { + "match": Object { + "extension": Object { + "query": "js", + "type": "phrase", + }, + }, + }, + }, + ], + "query": Object { + "language": "kuery", + "query": "extension : css", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": false, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "layers": Array [ + Object { + "accessors": Array [ + "count_column", + ], + "layerId": "unifiedHistogram", + "layerType": "data", + "seriesType": "bar_stacked", + "xAccessor": "date_column", + "yConfig": Array [ + Object { + "forAccessor": "count_column", + }, + ], + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "showCurrentTimeMarker": true, + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "valueLabels": "hide", + }, + }, + "title": "", + "visualizationType": "lnsXY", + } + `); + }); + + it('should return correct attributes with breakdown field', () => { + const breakdownField: DataViewField | undefined = dataView.fields.find( + (f) => f.name === 'extension' + ); + expect(getLensAttributes({ filters, query, dataView, timeInterval, breakdownField })) + .toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-layer-unifiedHistogram", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "unifiedHistogram": Object { + "columnOrder": Array [ + "breakdown_column", + "date_column", + "count_column", + ], + "columns": Object { + "breakdown_column": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top 3 values of extension", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "count_column", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 3, + }, + "scale": "ordinal", + "sourceField": "extension", + }, + "count_column": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "format": Object { + "id": "number", + "params": Object { + "decimals": 0, + }, + }, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "date_column": Object { + "dataType": "date", + "isBucketed": true, + "label": "timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "timestamp", + }, + }, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "index-pattern-with-timefield-id", + "key": "extension", + "negate": false, + "params": Object { + "query": "js", + }, + "type": "phrase", + }, + "query": Object { + "match": Object { + "extension": Object { + "query": "js", + "type": "phrase", + }, + }, + }, + }, + ], + "query": Object { + "language": "kuery", + "query": "extension : css", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": false, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "layers": Array [ + Object { + "accessors": Array [ + "count_column", + ], + "layerId": "unifiedHistogram", + "layerType": "data", + "seriesType": "bar_stacked", + "splitAccessor": "breakdown_column", + "xAccessor": "date_column", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "showCurrentTimeMarker": true, + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "valueLabels": "hide", + }, + }, + "title": "", + "visualizationType": "lnsXY", + } + `); + }); + + it('should return correct attributes with unsupported breakdown field', () => { + const breakdownField: DataViewField | undefined = dataView.fields.find( + (f) => f.name === 'scripted' + ); + expect(getLensAttributes({ filters, query, dataView, timeInterval, breakdownField })) + .toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "index-pattern-with-timefield-id", + "name": "indexpattern-datasource-layer-unifiedHistogram", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "unifiedHistogram": Object { + "columnOrder": Array [ + "date_column", + "count_column", + ], + "columns": Object { + "count_column": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "format": Object { + "id": "number", + "params": Object { + "decimals": 0, + }, + }, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "date_column": Object { + "dataType": "date", + "isBucketed": true, + "label": "timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "timestamp", + }, + }, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "index-pattern-with-timefield-id", + "key": "extension", + "negate": false, + "params": Object { + "query": "js", + }, + "type": "phrase", + }, + "query": Object { + "match": Object { + "extension": Object { + "query": "js", + "type": "phrase", + }, + }, + }, + }, + ], + "query": Object { + "language": "kuery", + "query": "extension : css", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": false, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "layers": Array [ + Object { + "accessors": Array [ + "count_column", + ], + "layerId": "unifiedHistogram", + "layerType": "data", + "seriesType": "bar_stacked", + "xAccessor": "date_column", + "yConfig": Array [ + Object { + "forAccessor": "count_column", + }, + ], + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "showCurrentTimeMarker": true, + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": false, + }, + "valueLabels": "hide", + }, + }, + "title": "", + "visualizationType": "lnsXY", + } + `); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/use_chart_actions.test.ts b/src/plugins/unified_histogram/public/chart/use_chart_actions.test.ts new file mode 100644 index 0000000000000..5967f01fd543b --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_chart_actions.test.ts @@ -0,0 +1,82 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-test-renderer'; +import { UnifiedHistogramChartContext } from '..'; +import { useChartActions } from './use_chart_actions'; + +describe('useChartActions', () => { + const render = () => { + const chart: UnifiedHistogramChartContext = { + hidden: false, + timeInterval: 'auto', + }; + const onChartHiddenChange = jest.fn((hidden: boolean) => { + chart.hidden = hidden; + }); + return { + chart, + onChartHiddenChange, + hook: renderHook(() => useChartActions({ chart, onChartHiddenChange })), + }; + }; + + it('should toggle chart options', () => { + const { hook } = render(); + expect(hook.result.current.showChartOptionsPopover).toBe(false); + act(() => { + hook.result.current.toggleChartOptions(); + }); + expect(hook.result.current.showChartOptionsPopover).toBe(true); + act(() => { + hook.result.current.toggleChartOptions(); + }); + expect(hook.result.current.showChartOptionsPopover).toBe(false); + }); + + it('should close chart options', () => { + const { hook } = render(); + act(() => { + hook.result.current.toggleChartOptions(); + }); + expect(hook.result.current.showChartOptionsPopover).toBe(true); + act(() => { + hook.result.current.closeChartOptions(); + }); + expect(hook.result.current.showChartOptionsPopover).toBe(false); + }); + + it('should toggle hide chart', () => { + const { chart, onChartHiddenChange, hook } = render(); + act(() => { + hook.result.current.toggleHideChart(); + }); + expect(chart.hidden).toBe(true); + expect(onChartHiddenChange).toBeCalledWith(true); + act(() => { + hook.result.current.toggleHideChart(); + }); + expect(chart.hidden).toBe(false); + expect(onChartHiddenChange).toBeCalledWith(false); + }); + + it('should focus chart element', () => { + const { chart, hook } = render(); + hook.result.current.chartRef.current.element = document.createElement('div'); + hook.result.current.chartRef.current.element.focus = jest.fn(); + chart.hidden = true; + hook.rerender(); + act(() => { + hook.result.current.toggleHideChart(); + }); + hook.rerender(); + expect(hook.result.current.chartRef.current.moveFocus).toBe(true); + expect(hook.result.current.chartRef.current.element.focus).toBeCalled(); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/use_refetch_id.test.ts b/src/plugins/unified_histogram/public/chart/use_refetch_id.test.ts new file mode 100644 index 0000000000000..8835173df2599 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_refetch_id.test.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import { DataView } from '@kbn/data-views-plugin/common'; +import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { renderHook } from '@testing-library/react-hooks'; +import { + UnifiedHistogramBreakdownContext, + UnifiedHistogramChartContext, + UnifiedHistogramHitsContext, + UnifiedHistogramRequestContext, +} from '../types'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { useRefetchId } from './use_refetch_id'; + +describe('useRefetchId', () => { + const getDeps: () => { + dataView: DataView; + lastReloadRequestTime: number | undefined; + request: UnifiedHistogramRequestContext | undefined; + hits: UnifiedHistogramHitsContext | undefined; + chart: UnifiedHistogramChartContext | undefined; + chartVisible: boolean; + breakdown: UnifiedHistogramBreakdownContext | undefined; + filters: Filter[]; + query: Query | AggregateQuery; + relativeTimeRange: TimeRange; + } = () => ({ + dataView: dataViewWithTimefieldMock, + lastReloadRequestTime: 0, + request: undefined, + hits: undefined, + chart: undefined, + chartVisible: true, + breakdown: undefined, + filters: [], + query: { language: 'kuery', query: '' }, + relativeTimeRange: { from: 'now-15m', to: 'now' }, + }); + + it('should increment the refetchId when any of the arguments change', () => { + const hook = renderHook((props) => useRefetchId(props), { initialProps: getDeps() }); + expect(hook.result.current).toBe(0); + hook.rerender(getDeps()); + expect(hook.result.current).toBe(0); + hook.rerender({ + ...getDeps(), + lastReloadRequestTime: 1, + }); + expect(hook.result.current).toBe(1); + hook.rerender({ + ...getDeps(), + lastReloadRequestTime: 1, + }); + expect(hook.result.current).toBe(1); + hook.rerender({ + ...getDeps(), + lastReloadRequestTime: 1, + query: { language: 'kuery', query: 'foo' }, + }); + expect(hook.result.current).toBe(2); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx b/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx new file mode 100644 index 0000000000000..9ca788a489f41 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx @@ -0,0 +1,239 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; +import { TimeRange } from '@kbn/data-plugin/common'; +import { renderHook } from '@testing-library/react-hooks'; +import { UnifiedHistogramBucketInterval } from '../types'; +import { useTimeRange } from './use_time_range'; + +jest.mock('@kbn/datemath', () => ({ + parse: jest.fn((datetime: string) => { + return { + format: jest.fn(() => { + return datetime; + }), + }; + }), +})); + +describe('useTimeRange', () => { + const uiSettings = uiSettingsServiceMock.createStartContract(); + uiSettings.get.mockReturnValue('dateFormat'); + const bucketInterval: UnifiedHistogramBucketInterval = { + description: '1 minute', + }; + const timeRange: TimeRange = { + from: '2022-11-17T00:00:00.000Z', + to: '2022-11-17T12:00:00.000Z', + }; + const timeInterval = 'auto'; + + it('should return time range text', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeText).toMatchInlineSnapshot( + `"2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - 1 minute)"` + ); + }); + + it('should return time range text when timeInterval is not auto', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval, + timeRange, + timeInterval: '1m', + }) + ); + expect(result.current.timeRangeText).toMatchInlineSnapshot( + `"2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: 1 minute)"` + ); + }); + + it('should return time range text when bucketInterval is undefined', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeText).toMatchInlineSnapshot( + `"2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - loading...)"` + ); + }); + + it('should render time range display', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeDisplay).toMatchInlineSnapshot(` + + 2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - 1 minute) + + `); + }); + + it('should render time range display when buckets are too large', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval: { + ...bucketInterval, + scaled: true, + scale: 2, + }, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeDisplay).toMatchInlineSnapshot(` + + + + 2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - 1 minute) + + + + + + + `); + }); + + it('should render time range display when there are too many buckets', () => { + const { result } = renderHook(() => + useTimeRange({ + uiSettings, + bucketInterval: { + ...bucketInterval, + scaled: true, + scale: 0.5, + }, + timeRange, + timeInterval, + }) + ); + expect(result.current.timeRangeDisplay).toMatchInlineSnapshot(` + + + + 2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - 1 minute) + + + + + + + `); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/use_total_hits.test.ts b/src/plugins/unified_histogram/public/chart/use_total_hits.test.ts new file mode 100644 index 0000000000000..4782df3683fcb --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/use_total_hits.test.ts @@ -0,0 +1,212 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { Filter } from '@kbn/es-query'; +import { UnifiedHistogramFetchStatus } from '../types'; +import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { useTotalHits } from './use_total_hits'; +import { useEffect as mockUseEffect } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; +import { of, throwError } from 'rxjs'; +import { waitFor } from '@testing-library/dom'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { DataViewType, SearchSourceSearchOptions } from '@kbn/data-plugin/common'; + +jest.mock('react-use/lib/useDebounce', () => { + return jest.fn((...args) => { + mockUseEffect(args[0], args[2]); + }); +}); + +describe('useTotalHits', () => { + const getDeps = () => ({ + services: { data: dataPluginMock.createStartContract() } as any, + dataView: dataViewWithTimefieldMock, + lastReloadRequestTime: undefined, + request: undefined, + hits: { + status: UnifiedHistogramFetchStatus.uninitialized, + total: undefined, + }, + chart: { + hidden: true, + timeInterval: 'auto', + }, + chartVisible: false, + breakdown: undefined, + filters: [], + query: { query: '', language: 'kuery' }, + timeRange: { from: 'now-15m', to: 'now' }, + refetchId: 0, + onTotalHitsChange: jest.fn(), + }); + + it('should fetch total hits on first execution', async () => { + const onTotalHitsChange = jest.fn(); + let fetchOptions: SearchSourceSearchOptions | undefined; + const fetchSpy = jest + .spyOn(searchSourceInstanceMock, 'fetch$') + .mockClear() + .mockImplementation((options) => { + fetchOptions = options; + return of({ + isRunning: false, + isPartial: false, + rawResponse: { + hits: { + total: 42, + }, + }, + }) as any; + }); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + const data = dataPluginMock.createStartContract(); + const timeRange = { from: 'now-15m', to: 'now' }; + jest + .spyOn(data.query.timefilter.timefilter, 'createFilter') + .mockClear() + .mockReturnValue(timeRange as any); + const query = { query: 'test query', language: 'kuery' }; + const filters: Filter[] = [{ meta: { index: 'test' }, query: { match_all: {} } }]; + const adapter = new RequestAdapter(); + renderHook(() => + useTotalHits({ + ...getDeps(), + services: { data } as any, + request: { + searchSessionId: '123', + adapter, + }, + query, + filters, + timeRange, + onTotalHitsChange, + }) + ); + expect(onTotalHitsChange).toBeCalledTimes(1); + expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.loading, undefined); + expect(setFieldSpy).toHaveBeenCalledWith('index', dataViewWithTimefieldMock); + expect(setFieldSpy).toHaveBeenCalledWith('query', query); + expect(setFieldSpy).toHaveBeenCalledWith('size', 0); + expect(setFieldSpy).toHaveBeenCalledWith('trackTotalHits', true); + expect(setFieldSpy).toHaveBeenCalledWith('filter', [...filters, timeRange]); + expect(fetchSpy).toHaveBeenCalled(); + expect(fetchOptions?.inspector?.adapter).toBe(adapter); + expect(fetchOptions?.sessionId).toBe('123'); + expect(fetchOptions?.abortSignal).toBeInstanceOf(AbortSignal); + expect(fetchOptions?.executionContext?.description).toBe('fetch total hits'); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(2); + expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.complete, 42); + }); + }); + + it('should not fetch total hits if chartVisible is true', async () => { + const onTotalHitsChange = jest.fn(); + const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + renderHook(() => useTotalHits({ ...getDeps(), chartVisible: true, onTotalHitsChange })); + expect(onTotalHitsChange).toBeCalledTimes(0); + expect(setFieldSpy).not.toHaveBeenCalled(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('should not fetch total hits if hits is undefined', async () => { + const onTotalHitsChange = jest.fn(); + const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + renderHook(() => useTotalHits({ ...getDeps(), hits: undefined, onTotalHitsChange })); + expect(onTotalHitsChange).toBeCalledTimes(0); + expect(setFieldSpy).not.toHaveBeenCalled(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('should not fetch a second time if fetchId is the same', async () => { + const onTotalHitsChange = jest.fn(); + const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + const options = { ...getDeps(), refetchId: 0, onTotalHitsChange }; + const { rerender } = renderHook(() => useTotalHits(options)); + expect(onTotalHitsChange).toBeCalledTimes(1); + expect(setFieldSpy).toHaveBeenCalled(); + expect(fetchSpy).toHaveBeenCalled(); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(2); + }); + rerender(); + expect(onTotalHitsChange).toBeCalledTimes(2); + expect(setFieldSpy).toHaveBeenCalledTimes(5); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it('should fetch a second time if fetchId is different', async () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort').mockClear(); + const onTotalHitsChange = jest.fn(); + const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + const options = { ...getDeps(), refetchId: 0, onTotalHitsChange }; + const { rerender } = renderHook(() => useTotalHits(options)); + expect(onTotalHitsChange).toBeCalledTimes(1); + expect(setFieldSpy).toHaveBeenCalled(); + expect(fetchSpy).toHaveBeenCalled(); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(2); + }); + options.refetchId = 1; + rerender(); + expect(abortSpy).toHaveBeenCalled(); + expect(onTotalHitsChange).toBeCalledTimes(3); + expect(setFieldSpy).toHaveBeenCalledTimes(10); + expect(fetchSpy).toHaveBeenCalledTimes(2); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(4); + }); + }); + + it('should call onTotalHitsChange with an error status if fetch fails', async () => { + const onTotalHitsChange = jest.fn(); + const error = new Error('test error'); + jest + .spyOn(searchSourceInstanceMock, 'fetch$') + .mockClear() + .mockReturnValue(throwError(() => error)); + renderHook(() => useTotalHits({ ...getDeps(), onTotalHitsChange })); + await waitFor(() => { + expect(onTotalHitsChange).toBeCalledTimes(2); + expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.error, error); + }); + }); + + it('should call searchSource.setOverwriteDataViewType if dataView is a rollup', async () => { + const setOverwriteDataViewTypeSpy = jest + .spyOn(searchSourceInstanceMock, 'setOverwriteDataViewType') + .mockClear(); + const setFieldSpy = jest.spyOn(searchSourceInstanceMock, 'setField').mockClear(); + const data = dataPluginMock.createStartContract(); + const timeRange = { from: 'now-15m', to: 'now' }; + jest + .spyOn(data.query.timefilter.timefilter, 'createFilter') + .mockClear() + .mockReturnValue(timeRange as any); + const filters: Filter[] = [{ meta: { index: 'test' }, query: { match_all: {} } }]; + renderHook(() => + useTotalHits({ + ...getDeps(), + dataView: { + ...dataViewWithTimefieldMock, + type: DataViewType.ROLLUP, + } as any, + filters, + }) + ); + expect(setOverwriteDataViewTypeSpy).toHaveBeenCalledWith(undefined); + expect(setFieldSpy).toHaveBeenCalledWith('filter', filters); + }); +}); From d057d9f7f54af8fe9483c12f1b0d01273cbfbc4f Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 18 Nov 2022 00:16:44 -0400 Subject: [PATCH 66/84] [Discover] Updating existing Jest tests for new functionality --- .../layout/use_discover_histogram.test.ts | 195 ++++++++++++++++-- .../layout/use_discover_histogram.ts | 2 +- .../main/hooks/use_inspector.test.ts | 19 +- .../hooks/use_saved_search_messages.test.ts | 35 ++++ .../public/chart/chart.test.tsx | 19 ++ src/plugins/unified_histogram/public/index.ts | 1 + 6 files changed, 251 insertions(+), 20 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts index 616e5ee2b0148..91a4dd39a87fd 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts @@ -34,6 +34,9 @@ import { calculateBounds } from '@kbn/data-plugin/public'; import { createSearchSessionMock } from '../../../../__mocks__/search_session'; import { RequestAdapter } from '@kbn/inspector-plugin/public'; import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks'; +import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; +import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; +import { InspectorAdapters } from '../../hooks/use_inspector'; const mockData = dataPluginMock.createStartContract(); @@ -63,6 +66,17 @@ jest.mock('@kbn/unified-field-list-plugin/public', () => { }; }); +jest.mock('../../hooks/use_saved_search_messages', () => { + const originalModule = jest.requireActual('../../hooks/use_saved_search_messages'); + return { + ...originalModule, + checkHitCount: jest.fn(originalModule.checkHitCount), + sendErrorTo: jest.fn(originalModule.sendErrorTo), + }; +}); + +const mockCheckHitCount = checkHitCount as jest.MockedFunction; + describe('useDiscoverHistogram', () => { const renderUseDiscoverHistogram = async ({ isPlainRecord = false, @@ -71,6 +85,16 @@ describe('useDiscoverHistogram', () => { storage = new LocalStorageMock({}) as unknown as Storage, stateContainer = {}, searchSessionId = '123', + inspectorAdapters = { requests: new RequestAdapter() }, + totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + result: Number(esHits.length), + }) as DataTotalHits$, + main$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT, + foundDocuments: true, + }) as DataMain$, }: { isPlainRecord?: boolean; isTimeBased?: boolean; @@ -78,16 +102,13 @@ describe('useDiscoverHistogram', () => { storage?: Storage; stateContainer?: unknown; searchSessionId?: string | null; + inspectorAdapters?: InspectorAdapters; + totalHits$?: DataTotalHits$; + main$?: DataMain$; } = {}) => { mockStorage = storage; mockCanVisualize = canVisualize; - const main$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT, - foundDocuments: true, - }) as DataMain$; - const documents$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE, result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewWithTimefieldMock)), @@ -98,11 +119,6 @@ describe('useDiscoverHistogram', () => { fields: [] as string[], }) as AvailableFields$; - const totalHits$ = new BehaviorSubject({ - fetchStatus: FetchStatus.COMPLETE, - result: Number(esHits.length), - }) as DataTotalHits$; - const savedSearchData$ = { main$, documents$, @@ -117,13 +133,13 @@ describe('useDiscoverHistogram', () => { const hook = renderHook(() => { return useDiscoverHistogram({ stateContainer: stateContainer as GetStateReturn, - state: { interval: 'auto', hideChart: false }, + state: { interval: 'auto', hideChart: false, breakdownField: 'extension' }, savedSearchData$, dataView: dataViewWithTimefieldMock, savedSearch: savedSearchMock, isTimeBased, isPlainRecord, - inspectorAdapters: { requests: new RequestAdapter() }, + inspectorAdapters, searchSessionManager: createSearchSessionMock(session).searchSessionManager, }); }); @@ -141,7 +157,7 @@ describe('useDiscoverHistogram', () => { describe('contexts', () => { it('should output the correct hits context', async () => { const { result } = await renderUseDiscoverHistogram(); - expect(result.current?.hits?.status).toBe(FetchStatus.COMPLETE); + expect(result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.complete); expect(result.current?.hits?.total).toEqual(esHits.length); }); @@ -151,16 +167,33 @@ describe('useDiscoverHistogram', () => { expect(result.current?.chart?.timeInterval).toBe('auto'); }); - it('should output undefined for hits and chart if isPlainRecord is true', async () => { + it('should output the correct breakdown context', async () => { + const { result } = await renderUseDiscoverHistogram(); + expect(result.current?.breakdown?.field?.name).toBe('extension'); + }); + + it('should output the correct request context', async () => { + const requestAdapter = new RequestAdapter(); + const { result } = await renderUseDiscoverHistogram({ + searchSessionId: '321', + inspectorAdapters: { requests: requestAdapter }, + }); + expect(result.current?.request.adapter).toBe(requestAdapter); + expect(result.current?.request.searchSessionId).toBe('321'); + }); + + it('should output undefined for hits and chart and breakdown if isPlainRecord is true', async () => { const { result } = await renderUseDiscoverHistogram({ isPlainRecord: true }); expect(result.current?.hits).toBeUndefined(); expect(result.current?.chart).toBeUndefined(); + expect(result.current?.breakdown).toBeUndefined(); }); - it('should output undefined for chart if isTimeBased is false', async () => { + it('should output undefined for chart and breakdown if isTimeBased is false', async () => { const { result } = await renderUseDiscoverHistogram({ isTimeBased: false }); expect(result.current?.hits).not.toBeUndefined(); expect(result.current?.chart).toBeUndefined(); + expect(result.current?.breakdown).toBeUndefined(); }); }); @@ -206,9 +239,57 @@ describe('useDiscoverHistogram', () => { const stateContainer = { setAppState: jest.fn(), }; + const inspectorAdapters = { + requests: new RequestAdapter(), + lensRequests: new RequestAdapter(), + }; const { result } = await renderUseDiscoverHistogram({ storage, stateContainer, + inspectorAdapters, + }); + act(() => { + result.current?.onChartHiddenChange(false); + }); + expect(inspectorAdapters.lensRequests).toBeDefined(); + expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, false); + expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: false }); + act(() => { + result.current?.onChartHiddenChange(true); + }); + expect(inspectorAdapters.lensRequests).toBeUndefined(); + expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, true); + expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: true }); + }); + + it('should set lensRequests when onChartLoad is called', async () => { + const lensRequests = new RequestAdapter(); + const inspectorAdapters = { + requests: new RequestAdapter(), + lensRequests: undefined as RequestAdapter | undefined, + }; + const { result } = await renderUseDiscoverHistogram({ inspectorAdapters }); + expect(inspectorAdapters.lensRequests).toBeUndefined(); + act(() => { + result.current?.onChartLoad({ complete: true, adapters: { requests: lensRequests } }); + }); + expect(inspectorAdapters.lensRequests).toBeDefined(); + }); + + it('should update chart hidden when onChartHiddenChange is called', async () => { + const storage = new LocalStorageMock({}) as unknown as Storage; + storage.set = jest.fn(); + const stateContainer = { + setAppState: jest.fn(), + }; + const inspectorAdapters = { + requests: new RequestAdapter(), + lensRequests: new RequestAdapter(), + }; + const { result } = await renderUseDiscoverHistogram({ + storage, + stateContainer, + inspectorAdapters, }); act(() => { result.current?.onChartHiddenChange(true); @@ -229,5 +310,87 @@ describe('useDiscoverHistogram', () => { }); expect(stateContainer.setAppState).toHaveBeenCalledWith({ interval: 'auto' }); }); + + it('should update breakdownField when onBreakdownFieldChange is called', async () => { + const stateContainer = { + setAppState: jest.fn(), + }; + const { result } = await renderUseDiscoverHistogram({ + stateContainer, + }); + act(() => { + result.current?.onBreakdownFieldChange( + dataViewWithTimefieldMock.getFieldByName('extension') + ); + }); + expect(stateContainer.setAppState).toHaveBeenCalledWith({ breakdownField: 'extension' }); + }); + + it('should update total hits when onTotalHitsChange is called', async () => { + mockCheckHitCount.mockClear(); + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.LOADING, + result: undefined, + }) as DataTotalHits$; + const main$ = new BehaviorSubject({ + fetchStatus: FetchStatus.COMPLETE, + recordRawType: RecordRawType.DOCUMENT, + foundDocuments: true, + }) as DataMain$; + const hook = await renderUseDiscoverHistogram({ totalHits$, main$ }); + act(() => { + hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.complete, 100); + }); + hook.rerender(); + expect(hook.result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.complete); + expect(hook.result.current?.hits?.total).toBe(100); + expect(totalHits$.value).toEqual({ + fetchStatus: FetchStatus.COMPLETE, + result: 100, + }); + expect(mockCheckHitCount).toHaveBeenCalledWith(main$, 100); + }); + + it('should not update total hits when onTotalHitsChange is called with an error', async () => { + mockCheckHitCount.mockClear(); + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.UNINITIALIZED, + result: undefined, + }) as DataTotalHits$; + const hook = await renderUseDiscoverHistogram({ totalHits$ }); + const error = new Error('test'); + act(() => { + hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.error, error); + }); + hook.rerender(); + expect(sendErrorTo).toHaveBeenCalledWith(mockData, totalHits$); + expect(hook.result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.error); + expect(hook.result.current?.hits?.total).toBeUndefined(); + expect(totalHits$.value).toEqual({ + fetchStatus: FetchStatus.ERROR, + error, + }); + expect(mockCheckHitCount).not.toHaveBeenCalled(); + }); + + it('should not update total hits when onTotalHitsChange is called with a loading status while totalHits$ has a partial status', async () => { + mockCheckHitCount.mockClear(); + const totalHits$ = new BehaviorSubject({ + fetchStatus: FetchStatus.PARTIAL, + result: undefined, + }) as DataTotalHits$; + const hook = await renderUseDiscoverHistogram({ totalHits$ }); + act(() => { + hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.loading, undefined); + }); + hook.rerender(); + expect(hook.result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.partial); + expect(hook.result.current?.hits?.total).toBeUndefined(); + expect(totalHits$.value).toEqual({ + fetchStatus: FetchStatus.PARTIAL, + result: undefined, + }); + expect(mockCheckHitCount).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index 053794d76a91f..a8212cd1eb9c7 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -167,7 +167,7 @@ export const useDiscoverHistogram = ({ (status: UnifiedHistogramFetchStatus, result?: number | Error) => { if (result instanceof Error) { // Display the error and set totalHits$ to an error state - sendErrorTo(data, savedSearchData$.totalHits$); + sendErrorTo(data, savedSearchData$.totalHits$)(result); return; } diff --git a/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts b/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts index 66c5542f5647a..60327e84f2cd8 100644 --- a/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_inspector.test.ts @@ -10,15 +10,23 @@ import { renderHook } from '@testing-library/react-hooks'; import { discoverServiceMock } from '../../../__mocks__/services'; import { savedSearchMock } from '../../../__mocks__/saved_search'; import { useInspector } from './use_inspector'; -import { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { Adapters, RequestAdapter } from '@kbn/inspector-plugin/common'; +import { OverlayRef } from '@kbn/core/public'; +import { AggregateRequestAdapter } from '../utils/aggregate_request_adapter'; describe('test useInspector', () => { test('inspector open function is executed, expanded doc is closed', async () => { const setExpandedDoc = jest.fn(); - + let adapters: Adapters | undefined; + jest.spyOn(discoverServiceMock.inspector, 'open').mockImplementation((localAdapters) => { + adapters = localAdapters; + return {} as OverlayRef; + }); + const requests = new RequestAdapter(); + const lensRequests = new RequestAdapter(); const { result } = renderHook(() => { return useInspector({ - inspectorAdapters: { requests: new RequestAdapter() }, + inspectorAdapters: { requests, lensRequests }, savedSearch: savedSearchMock, inspector: discoverServiceMock.inspector, setExpandedDoc, @@ -27,5 +35,10 @@ describe('test useInspector', () => { result.current(); expect(setExpandedDoc).toHaveBeenCalledWith(undefined); expect(discoverServiceMock.inspector.open).toHaveBeenCalled(); + expect(adapters?.requests).toBeInstanceOf(AggregateRequestAdapter); + expect(adapters?.requests?.getRequests()).toEqual([ + ...requests.getRequests(), + ...lensRequests.getRequests(), + ]); }); }); diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts index 5b9b96db1afaf..5973d679b6b1c 100644 --- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ import { + checkHitCount, sendCompleteMsg, sendErrorMsg, + sendErrorTo, sendLoadingMsg, sendNoResultsFoundMsg, sendPartialMsg, @@ -16,6 +18,7 @@ import { FetchStatus } from '../../types'; import { BehaviorSubject } from 'rxjs'; import { DataMainMsg, RecordRawType } from './use_saved_search'; import { filter } from 'rxjs/operators'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; describe('test useSavedSearch message generators', () => { test('sendCompleteMsg', (done) => { @@ -95,4 +98,36 @@ describe('test useSavedSearch message generators', () => { }); sendCompleteMsg(main$, false); }); + + test('sendErrorTo', (done) => { + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.PARTIAL }); + const data = dataPluginMock.createStartContract(); + const error = new Error('Pls help!'); + main$.subscribe((value) => { + expect(data.search.showError).toBeCalledWith(error); + expect(value.fetchStatus).toBe(FetchStatus.ERROR); + expect(value.error).toBe(error); + done(); + }); + sendErrorTo(data, main$)(error); + }); + + test('checkHitCount with hits', (done) => { + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.LOADING }); + main$.subscribe((value) => { + expect(value.fetchStatus).toBe(FetchStatus.PARTIAL); + done(); + }); + checkHitCount(main$, 100); + }); + + test('checkHitCount without hits', (done) => { + const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.LOADING }); + main$.subscribe((value) => { + expect(value.fetchStatus).toBe(FetchStatus.COMPLETE); + expect(value.foundDocuments).toBe(false); + done(); + }); + checkHitCount(main$, 0); + }); }); diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index 150c2f86dddeb..2efc7643a8ecf 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -19,10 +19,12 @@ import { of } from 'rxjs'; import { HitsCounter } from '../hits_counter'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewMock } from '../__mocks__/data_view'; +import { BreakdownFieldSelector } from './breakdown_field_selector'; async function mountComponent({ noChart, noHits, + noBreakdown, chartHidden = false, appendHistogram, onEditVisualization = jest.fn(), @@ -30,6 +32,7 @@ async function mountComponent({ }: { noChart?: boolean; noHits?: boolean; + noBreakdown?: boolean; chartHidden?: boolean; appendHistogram?: ReactElement; dataView?: DataView; @@ -68,6 +71,7 @@ async function mountComponent({ scale: 2, }, }, + breakdown: noBreakdown ? undefined : { field: undefined }, appendHistogram, onEditVisualization: onEditVisualization || undefined, onResetChartHeight: jest.fn(), @@ -167,4 +171,19 @@ describe('Chart', () => { const component = await mountComponent(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBeTruthy(); }); + + it('should render BreakdownFieldSelector when chart is visible and breakdown is defined', async () => { + const component = await mountComponent(); + expect(component.find(BreakdownFieldSelector).exists()).toBeTruthy(); + }); + + it('should not render BreakdownFieldSelector when chart is hidden', async () => { + const component = await mountComponent({ chartHidden: true }); + expect(component.find(BreakdownFieldSelector).exists()).toBeFalsy(); + }); + + it('should not render BreakdownFieldSelector when chart is visible and breakdown is undefined', async () => { + const component = await mountComponent({ noBreakdown: true }); + expect(component.find(BreakdownFieldSelector).exists()).toBeFalsy(); + }); }); diff --git a/src/plugins/unified_histogram/public/index.ts b/src/plugins/unified_histogram/public/index.ts index 6843a4c824333..4a8f73477c0b6 100644 --- a/src/plugins/unified_histogram/public/index.ts +++ b/src/plugins/unified_histogram/public/index.ts @@ -12,6 +12,7 @@ export type { UnifiedHistogramLayoutProps } from './layout'; export { UnifiedHistogramLayout } from './layout'; export type { UnifiedHistogramServices, + UnifiedHistogramRequestContext, UnifiedHistogramHitsContext, UnifiedHistogramChartContext, UnifiedHistogramBreakdownContext, From d9f71f9417768097e7a06c8df4a60481ab79577b Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 18 Nov 2022 11:38:01 -0400 Subject: [PATCH 67/84] [Discover] Fix flaky histogram shown/hidden functional tests --- .../discover/group1/_discover_histogram.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/test/functional/apps/discover/group1/_discover_histogram.ts b/test/functional/apps/discover/group1/_discover_histogram.ts index 884dc665bf9db..70a1fba5afafc 100644 --- a/test/functional/apps/discover/group1/_discover_histogram.ts +++ b/test/functional/apps/discover/group1/_discover_histogram.ts @@ -148,8 +148,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(true); await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); // histogram is hidden, when reloading the page it should remain hidden await browser.refresh(); canvasExists = await elasticChart.canvasExists(); @@ -157,8 +159,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); await PageObjects.header.waitUntilLoadingHasFinished(); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(true); + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(true); + }); }); it('should allow hiding the histogram, persisted in saved search', async () => { const from = 'Jan 1, 2010 @ 00:00:00.000'; @@ -169,8 +173,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // close chart for saved search await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - let canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + let canvasExists: boolean; + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); // save search await PageObjects.discover.saveSearch(savedSearch); @@ -212,8 +219,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // close chart await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - let canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + let canvasExists: boolean; + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); // save search await PageObjects.discover.saveSearch('persisted hidden histogram'); @@ -222,8 +232,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // open chart await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(true); + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(true); + }); // go to dashboard await PageObjects.common.navigateToApp('dashboard'); @@ -238,8 +250,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // close chart await testSubjects.click('unifiedHistogramChartOptionsToggle'); await testSubjects.click('unifiedHistogramChartToggle'); - canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + await retry.try(async () => { + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); }); }); } From 8835af0f5b108c14de0eba07eea5c1575ee1d30c Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 18 Nov 2022 14:35:41 -0400 Subject: [PATCH 68/84] [Discover] Added Jest tests for histogram.tsx --- .../public/chart/histogram.test.tsx | 173 +++++++++++++++++- .../public/chart/histogram.tsx | 2 +- 2 files changed, 170 insertions(+), 5 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index c46a042d1334e..34fc2a47fd416 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -6,10 +6,21 @@ * Side Public License, v 1. */ import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { Histogram } from './histogram'; +import { getLensProps, Histogram } from './histogram'; import React from 'react'; import { unifiedHistogramServicesMock } from '../__mocks__/services'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; +import { createDefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; +import { UnifiedHistogramFetchStatus } from '../types'; +import { getLensAttributes } from './get_lens_attributes'; +import { REQUEST_DEBOUNCE_MS } from './consts'; +import { act } from 'react-dom/test-utils'; +import * as buildBucketInterval from './build_bucket_interval'; +import * as useTimeRange from './use_time_range'; + +const mockBucketInterval = { description: '1 minute', scale: undefined, scaled: false }; +jest.spyOn(buildBucketInterval, 'buildBucketInterval').mockReturnValue(mockBucketInterval); +jest.spyOn(useTimeRange, 'useTimeRange'); function mountComponent() { const services = unifiedHistogramServicesMock; @@ -21,10 +32,20 @@ function mountComponent() { const props = { services: unifiedHistogramServicesMock, + request: { + searchSessionId: '123', + }, + hits: { + status: UnifiedHistogramFetchStatus.loading, + total: undefined, + }, chart: { hidden: false, timeInterval: 'auto', }, + breakdown: { + field: dataViewWithTimefieldMock.getFieldByName('extension'), + }, timefilterUpdateHandler, dataView: dataViewWithTimefieldMock, filters: [], @@ -36,15 +57,159 @@ function mountComponent() { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590', }, - lastReloadRequestTime: 0, + lastReloadRequestTime: 42, + onTotalHitsChange: jest.fn(), + onChartLoad: jest.fn(), }; - return mountWithIntl(); + return { + props, + component: mountWithIntl(), + }; } describe('Histogram', () => { it('renders correctly', () => { - const component = mountComponent(); + const { component } = mountComponent(); expect(component.find('[data-test-subj="unifiedHistogramChart"]').exists()).toBe(true); }); + + it('should render lens.EmbeddableComponent with debounced props', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + expect(component.find(embeddable).exists()).toBe(true); + let lensProps = component.find(embeddable).props(); + const originalProps = getLensProps({ + timeRange: props.timeRange, + attributes: getLensAttributes({ + filters: props.filters, + query: props.query, + dataView: props.dataView, + timeInterval: props.chart.timeInterval, + breakdownField: props.breakdown.field, + }), + request: props.request, + lastReloadRequestTime: props.lastReloadRequestTime, + onLoad: lensProps.onLoad, + }); + expect(lensProps).toEqual(originalProps); + component.setProps({ lastReloadRequestTime: 43 }).update(); + lensProps = component.find(embeddable).props(); + expect(lensProps).toEqual(originalProps); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, REQUEST_DEBOUNCE_MS)); + }); + component.update(); + lensProps = component.find(embeddable).props(); + expect(lensProps).toEqual({ ...originalProps, lastReloadRequestTime: 43 }); + }); + + it('should execute onLoad correctly', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + const adapters = createDefaultInspectorAdapters(); + adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any; + const rawResponse = { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 100, + max_score: null, + hits: [], + }, + aggregations: { + '2': { + buckets: [ + { + key_as_string: '2022-10-05T16:00:00.000-03:00', + key: 1664996400000, + doc_count: 20, + }, + { + key_as_string: '2022-10-05T16:30:00.000-03:00', + key: 1664998200000, + doc_count: 20, + }, + { + key_as_string: '2022-10-05T17:00:00.000-03:00', + key: 1665000000000, + doc_count: 20, + }, + { + key_as_string: '2022-10-05T17:30:00.000-03:00', + key: 1665001800000, + doc_count: 20, + }, + { + key_as_string: '2022-10-05T18:00:00.000-03:00', + key: 1665003600000, + doc_count: 20, + }, + ], + }, + }, + }; + jest + .spyOn(adapters.requests, 'getRequests') + .mockReturnValue([{ response: { json: { rawResponse } } } as any]); + onLoad(true, undefined); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.loading, + undefined + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ complete: false, adapters: {} }); + expect(buildBucketInterval.buildBucketInterval).not.toHaveBeenCalled(); + expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith( + expect.objectContaining({ bucketInterval: undefined }) + ); + act(() => { + onLoad(false, adapters); + }); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.complete, + 100 + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ complete: true, adapters }); + expect(buildBucketInterval.buildBucketInterval).toHaveBeenCalled(); + expect(useTimeRange.useTimeRange).toHaveBeenLastCalledWith( + expect.objectContaining({ bucketInterval: mockBucketInterval }) + ); + }); + + it('should not recreate onLoad in debounced lens props when hits.total changes', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + onLoad(true, undefined); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.loading, + undefined + ); + component + .setProps({ + hits: { + status: UnifiedHistogramFetchStatus.complete, + total: 100, + }, + }) + .update(); + expect(component.find(embeddable).props().onLoad).toBe(onLoad); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, REQUEST_DEBOUNCE_MS)); + }); + component.update(); + expect(component.find(embeddable).props().onLoad).toBe(onLoad); + onLoad(true, undefined); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.loading, + 100 + ); + }); }); diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 59b9388d6c819..225cd0a126b13 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -162,7 +162,7 @@ export function Histogram({ ); } -const getLensProps = ({ +export const getLensProps = ({ timeRange, attributes, request, From 27922b370329fb2b718d9f29a9b3fa46dd47724a Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 21 Nov 2022 15:54:14 -0400 Subject: [PATCH 69/84] [Discover] Add placeholder text to breakdown_field_selector --- .../public/chart/breakdown_field_selector.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index bc08ab71e217c..ced77ba486132 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -56,6 +56,9 @@ export const BreakdownFieldSelector = ({ prepend={i18n.translate('unifiedHistogram.breakdownFieldSelectorLabel', { defaultMessage: 'Break down by', })} + placeholder={i18n.translate('unifiedHistogram.breakdownFieldSelectorPlaceholder', { + defaultMessage: 'Select field', + })} singleSelection={{ asPlainText: true }} options={fieldOptions} selectedOptions={selectedFields} From 900877d007a3183eb2526e521606715a89d6281c Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 22 Nov 2022 20:46:35 +0300 Subject: [PATCH 70/84] [Discover] do not bring breakdown field on Lens to Discover navigation --- .../lens/public/app_plugin/lens_top_nav.tsx | 25 ------------------- .../apps/lens/group2/show_underlying_data.ts | 25 ------------------- 2 files changed, 50 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 622082365f00b..582741fe68741 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -41,10 +41,6 @@ import { } from '../utils'; import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data'; import { changeIndexPattern } from '../state_management/lens_slice'; -import type { - FieldBasedIndexPatternColumn, - FormBasedPrivateState, -} from '../datasources/form_based/types'; function getLensTopNavConfig(options: { showSaveAndReturn: boolean; @@ -576,30 +572,12 @@ export const LensTopNavMenu = ({ getEsQueryConfig(uiSettings) ); - let breakdownField; - if (activeDatasourceId && datasourceMap[activeDatasourceId]) { - const dataSource = datasourceMap[activeDatasourceId]; - const dataSourceState = datasourceStates[activeDatasourceId].state as - | FormBasedPrivateState - | undefined; - const [firstLayerId] = dataSource.getLayers(dataSourceState); - const firstLayer = dataSourceState?.layers[firstLayerId]; - const termsColumn = Object.values(firstLayer?.columns || {}).find( - ({ operationType }) => operationType === 'terms' - ); - if (termsColumn) { - breakdownField = (termsColumn as FieldBasedIndexPatternColumn | undefined) - ?.sourceField; - } - } - return discoverLocator.getRedirectUrl({ dataViewSpec: dataViews.indexPatterns[meta.id]?.spec, timeRange: data.query.timefilter.timefilter.getTime(), filters: newFilters, query: isOnTextBasedMode ? query : newQuery, columns: meta.columns, - breakdownField, }); }, openSettings: (anchorElement: HTMLElement) => @@ -639,12 +617,9 @@ export const LensTopNavMenu = ({ query, filters, indexPatterns, - activeDatasourceId, dataViews.indexPatterns, data.query.timefilter.timefilter, isOnTextBasedMode, - datasourceMap, - datasourceStates, lensStore, theme$, ]); diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts index a8c5e415864c2..5c73e6ca7c288 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts @@ -42,11 +42,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); - - // check breakdown values - const list = await PageObjects.discover.getHistogramLegendList(); - expect(list).to.eql(['png', 'css', 'jpg']); - // check the table columns const columns = await PageObjects.discover.getColumnHeaders(); expect(columns).to.eql(['extension.raw', '@timestamp', 'bytes']); @@ -73,11 +68,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); - - // check breakdown values - const list = await PageObjects.discover.getHistogramLegendList(); - expect(list).to.eql(['Other', 'png', 'css', 'jpg']); - expect(await queryBar.getQueryString()).be.eql(''); await browser.closeCurrentWindow(); await browser.switchToWindow(lensWindowHandler); @@ -114,11 +104,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); - - // check breakdown values - const list = await PageObjects.discover.getHistogramLegendList(); - expect(list).to.eql(['png', 'css', 'jpg']); - // check the query expect(await queryBar.getQueryString()).be.eql( '( ( extension.raw: "png" ) OR ( extension.raw: "css" ) OR ( extension.raw: "jpg" ) )' @@ -155,11 +140,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); - - // check breakdown values - const list = await PageObjects.discover.getHistogramLegendList(); - expect(list).to.eql(['css', 'jpg']); - // check the columns const columns = await PageObjects.discover.getColumnHeaders(); expect(columns).to.eql(['extension.raw', '@timestamp', 'memory']); @@ -195,11 +175,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.switchToWindow(discoverWindowHandle); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('unifiedHistogramChart'); - - // check breakdown values - const list = await PageObjects.discover.getHistogramLegendList(); - expect(list).to.eql(['css', 'jpg']); - // check the query expect(await queryBar.getQueryString()).be.eql( '( ( bytes > 4000 ) AND ( ( extension.raw: "css" ) OR ( extension.raw: "gif" ) OR ( extension.raw: "jpg" ) ) )' From 395f570ebd47643d5936c6ef63e51ff5cd72bc6a Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 22 Nov 2022 18:44:27 -0400 Subject: [PATCH 71/84] [Discover] Rollback Lens visualize button changes and update Discover to use lens.navigateToPrefilledEditor instead --- .../layout/use_discover_histogram.test.ts | 20 ++++- .../layout/use_discover_histogram.ts | 77 +++++++++---------- src/plugins/ui_actions/public/types.ts | 1 - .../visualize_trigger_utils.ts | 4 +- .../public/chart/chart.test.tsx | 9 ++- .../unified_histogram/public/chart/chart.tsx | 32 ++++++-- .../public/chart/histogram.test.tsx | 29 ++++--- .../public/chart/histogram.tsx | 18 +---- .../public/layout/layout.tsx | 3 +- .../form_based_suggestions.test.tsx | 52 ------------- .../form_based/form_based_suggestions.ts | 48 ++---------- .../public/datasources/form_based/types.ts | 1 - .../editor_frame/suggestion_helpers.test.ts | 9 +-- .../editor_frame/suggestion_helpers.ts | 3 +- x-pack/plugins/lens/public/types.ts | 3 +- .../apps/discover/visualize_field.ts | 6 +- 16 files changed, 124 insertions(+), 191 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts index 91a4dd39a87fd..d565701b4fda9 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.test.ts @@ -36,7 +36,8 @@ import { RequestAdapter } from '@kbn/inspector-plugin/public'; import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mocks'; import { UnifiedHistogramFetchStatus } from '@kbn/unified-histogram-plugin/public'; import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages'; -import { InspectorAdapters } from '../../hooks/use_inspector'; +import type { InspectorAdapters } from '../../hooks/use_inspector'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; const mockData = dataPluginMock.createStartContract(); @@ -47,6 +48,10 @@ mockData.query.timefilter.timefilter.calculateBounds = (timeRange) => { return calculateBounds(timeRange); }; +const mockLens = { + navigateToPrefilledEditor: jest.fn(), +}; + let mockStorage = new LocalStorageMock({}) as unknown as Storage; let mockCanVisualize = true; @@ -54,7 +59,7 @@ jest.mock('../../../../hooks/use_discover_services', () => { const originalModule = jest.requireActual('../../../../hooks/use_discover_services'); return { ...originalModule, - useDiscoverServices: () => ({ storage: mockStorage, data: mockData }), + useDiscoverServices: () => ({ storage: mockStorage, data: mockData, lens: mockLens }), }; }); @@ -207,6 +212,17 @@ describe('useDiscoverHistogram', () => { const { result } = await renderUseDiscoverHistogram({ canVisualize: false }); expect(result.current?.onEditVisualization).toBeUndefined(); }); + + it('should call lens.navigateToPrefilledEditor when onEditVisualization is called', async () => { + const { result } = await renderUseDiscoverHistogram(); + const attributes = { title: 'test' } as TypedLensByValueInput['attributes']; + result.current?.onEditVisualization!(attributes); + expect(mockLens.navigateToPrefilledEditor).toHaveBeenCalledWith({ + id: '', + timeRange: mockData.query.timefilter.timefilter.getTime(), + attributes, + }); + }); }); describe('topPanelHeight', () => { diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index a8212cd1eb9c7..ff798385ab67d 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -8,10 +8,7 @@ import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; -import { - getVisualizeInformation, - triggerVisualizeActions, -} from '@kbn/unified-field-list-plugin/public'; +import { getVisualizeInformation } from '@kbn/unified-field-list-plugin/public'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { UnifiedHistogramFetchStatus, @@ -19,8 +16,8 @@ import { } from '@kbn/unified-histogram-plugin/public'; import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; import useObservable from 'react-use/lib/useObservable'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { getUiActions } from '../../../../kibana_services'; -import { PLUGIN_ID } from '../../../../../common'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDataState } from '../../hooks/use_data_state'; import type { SavedSearchData } from '../../hooks/use_saved_search'; @@ -55,28 +52,7 @@ export const useDiscoverHistogram = ({ inspectorAdapters: InspectorAdapters; searchSessionManager: DiscoverSearchSessionManager; }) => { - const { storage, data } = useDiscoverServices(); - - /** - * Breakdown - */ - - const onBreakdownFieldChange = useCallback( - (breakdownField: DataViewField | undefined) => { - stateContainer.setAppState({ breakdownField: breakdownField?.name }); - }, - [stateContainer] - ); - - const field = useMemo( - () => (state.breakdownField ? dataView.getFieldByName(state.breakdownField) : undefined), - [dataView, state.breakdownField] - ); - - const breakdown = useMemo( - () => (isPlainRecord || !isTimeBased ? undefined : { field }), - [field, isPlainRecord, isTimeBased] - ); + const { storage, data, lens } = useDiscoverServices(); /** * Visualize @@ -100,19 +76,19 @@ export const useDiscoverHistogram = ({ }); }, [dataView, savedSearch.columns, timeField]); - const onEditVisualization = useCallback(() => { - if (!timeField) { - return; - } - triggerVisualizeActions( - getUiActions(), - timeField, - savedSearch.columns || [], - PLUGIN_ID, - dataView, - breakdown?.field - ); - }, [breakdown?.field, dataView, savedSearch.columns, timeField]); + const onEditVisualization = useCallback( + (lensAttributes: TypedLensByValueInput['attributes']) => { + if (!timeField) { + return; + } + lens.navigateToPrefilledEditor({ + id: '', + timeRange: data.query.timefilter.timefilter.getTime(), + attributes: lensAttributes, + }); + }, + [data.query.timefilter.timefilter, lens, timeField] + ); /** * Height @@ -259,6 +235,27 @@ export const useDiscoverHistogram = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchSessionId]); + /** + * Breakdown + */ + + const onBreakdownFieldChange = useCallback( + (breakdownField: DataViewField | undefined) => { + stateContainer.setAppState({ breakdownField: breakdownField?.name }); + }, + [stateContainer] + ); + + const field = useMemo( + () => (state.breakdownField ? dataView.getFieldByName(state.breakdownField) : undefined), + [dataView, state.breakdownField] + ); + + const breakdown = useMemo( + () => (isPlainRecord || !isTimeBased ? undefined : { field }), + [field, isPlainRecord, isTimeBased] + ); + // Don't render the unified histogram layout until the first search has been requested return searchSessionId ? { diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index ee54d4daf7365..fb2d9869e21c7 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -18,7 +18,6 @@ export type TriggerToActionsRegistry = Map; export interface VisualizeFieldContext { fieldName: string; dataViewSpec: DataViewSpec; - breakdownField?: string; contextualFields?: string[]; originatingApp?: string; query?: AggregateQuery; diff --git a/src/plugins/unified_field_list/public/components/field_visualize_button/visualize_trigger_utils.ts b/src/plugins/unified_field_list/public/components/field_visualize_button/visualize_trigger_utils.ts index 75d2b5846119f..babb7f40ff92b 100644 --- a/src/plugins/unified_field_list/public/components/field_visualize_button/visualize_trigger_utils.ts +++ b/src/plugins/unified_field_list/public/components/field_visualize_button/visualize_trigger_utils.ts @@ -49,14 +49,12 @@ export function triggerVisualizeActions( field: DataViewField, contextualFields: string[] = [], originatingApp: string, - dataView?: DataView, - breakdownField?: DataViewField + dataView?: DataView ) { if (!dataView) return; const trigger = getTriggerConstant(field.type); const triggerOptions = { dataViewSpec: dataView.toSpec(false), - breakdownField: breakdownField?.name, fieldName: field.name, contextualFields, originatingApp, diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index 2efc7643a8ecf..21682cb919d3c 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -20,6 +20,7 @@ import { HitsCounter } from '../hits_counter'; import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield'; import { dataViewMock } from '../__mocks__/data_view'; import { BreakdownFieldSelector } from './breakdown_field_selector'; +import { Histogram } from './histogram'; async function mountComponent({ noChart, @@ -84,7 +85,7 @@ async function mountComponent({ instance = mountWithIntl(); // wait for initial async loading to complete await new Promise((r) => setTimeout(r, 0)); - await instance.update(); + instance.update(); }); return instance; } @@ -137,13 +138,13 @@ describe('Chart', () => { const fn = jest.fn(); const component = await mountComponent({ onEditVisualization: fn }); await act(async () => { - await component + component .find('[data-test-subj="unifiedHistogramEditVisualization"]') .first() .simulate('click'); }); - - expect(fn).toHaveBeenCalled(); + const lensAttributes = component.find(Histogram).prop('lensAttributes'); + expect(fn).toHaveBeenCalledWith(lensAttributes); }); it('should render HitsCounter when hits is defined', async () => { diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index 4cccbd32ff2b0..d339723d6082e 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { HitsCounter } from '../hits_counter'; import { Histogram } from './histogram'; import { useChartPanels } from './use_chart_panels'; @@ -36,6 +37,7 @@ import { useRequestParams } from './use_request_params'; import { useChartStyles } from './use_chart_styles'; import { useChartActions } from './use_chart_actions'; import { useRefetchId } from './use_refetch_id'; +import { getLensAttributes } from './get_lens_attributes'; export interface ChartProps { className?: string; @@ -48,7 +50,7 @@ export interface ChartProps { breakdown?: UnifiedHistogramBreakdownContext; appendHitsCounter?: ReactElement; appendHistogram?: ReactElement; - onEditVisualization?: () => void; + onEditVisualization?: (lensAttributes: TypedLensByValueInput['attributes']) => void; onResetChartHeight?: () => void; onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; @@ -70,7 +72,7 @@ export function Chart({ breakdown, appendHitsCounter, appendHistogram, - onEditVisualization, + onEditVisualization: originalOnEditVisualization, onResetChartHeight, onChartHiddenChange, onTimeIntervalChange, @@ -157,6 +159,28 @@ export function Chart({ chartToolButtonCss, } = useChartStyles(chartVisible); + const lensAttributes = useMemo( + () => + getLensAttributes({ + filters, + query, + dataView, + timeInterval: chart?.timeInterval, + breakdownField: breakdown?.field, + }), + [breakdown?.field, chart?.timeInterval, dataView, filters, query] + ); + + const onEditVisualization = useMemo( + () => + originalOnEditVisualization + ? () => { + originalOnEditVisualization(lensAttributes); + } + : undefined, + [lensAttributes, originalOnEditVisualization] + ); + return ( diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index 34fc2a47fd416..ece569582520d 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -22,6 +22,18 @@ const mockBucketInterval = { description: '1 minute', scale: undefined, scaled: jest.spyOn(buildBucketInterval, 'buildBucketInterval').mockReturnValue(mockBucketInterval); jest.spyOn(useTimeRange, 'useTimeRange'); +const getMockLensAttributes = () => + getLensAttributes({ + filters: [], + query: { + language: 'kuery', + query: '', + }, + dataView: dataViewWithTimefieldMock, + timeInterval: 'auto', + breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'), + }); + function mountComponent() { const services = unifiedHistogramServicesMock; services.data.query.timefilter.timefilter.getAbsoluteTime = () => { @@ -43,21 +55,14 @@ function mountComponent() { hidden: false, timeInterval: 'auto', }, - breakdown: { - field: dataViewWithTimefieldMock.getFieldByName('extension'), - }, timefilterUpdateHandler, dataView: dataViewWithTimefieldMock, - filters: [], - query: { - language: 'kuery', - query: '', - }, timeRange: { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590', }, lastReloadRequestTime: 42, + lensAttributes: getMockLensAttributes(), onTotalHitsChange: jest.fn(), onChartLoad: jest.fn(), }; @@ -81,13 +86,7 @@ describe('Histogram', () => { let lensProps = component.find(embeddable).props(); const originalProps = getLensProps({ timeRange: props.timeRange, - attributes: getLensAttributes({ - filters: props.filters, - query: props.query, - dataView: props.dataView, - timeInterval: props.chart.timeInterval, - breakdownField: props.breakdown.field, - }), + attributes: getMockLensAttributes(), request: props.request, lastReloadRequestTime: props.lastReloadRequestTime, onLoad: lensProps.onLoad, diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 225cd0a126b13..e2d42b605594f 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -8,17 +8,16 @@ import { useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; import type { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import type { estypes } from '@elastic/elasticsearch'; -import type { AggregateQuery, Query, Filter, TimeRange } from '@kbn/es-query'; +import type { TimeRange } from '@kbn/es-query'; import useDebounce from 'react-use/lib/useDebounce'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { - UnifiedHistogramBreakdownContext, UnifiedHistogramBucketInterval, UnifiedHistogramChartContext, UnifiedHistogramFetchStatus, @@ -27,7 +26,6 @@ import { UnifiedHistogramRequestContext, UnifiedHistogramServices, } from '../types'; -import { getLensAttributes } from './get_lens_attributes'; import { buildBucketInterval } from './build_bucket_interval'; import { useTimeRange } from './use_time_range'; import { REQUEST_DEBOUNCE_MS } from './consts'; @@ -39,10 +37,8 @@ export interface HistogramProps { request?: UnifiedHistogramRequestContext; hits?: UnifiedHistogramHitsContext; chart: UnifiedHistogramChartContext; - breakdown?: UnifiedHistogramBreakdownContext; - filters: Filter[]; - query: Query | AggregateQuery; timeRange: TimeRange; + lensAttributes: TypedLensByValueInput['attributes']; onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void; onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void; } @@ -54,17 +50,11 @@ export function Histogram({ request, hits, chart: { timeInterval }, - breakdown: { field: breakdownField } = {}, - filters, - query, timeRange, + lensAttributes: attributes, onTotalHitsChange, onChartLoad, }: HistogramProps) { - const attributes = useMemo( - () => getLensAttributes({ filters, query, dataView, timeInterval, breakdownField }), - [breakdownField, dataView, filters, query, timeInterval] - ); const [bucketInterval, setBucketInterval] = useState(); const { timeRangeText, timeRangeDisplay } = useTimeRange({ uiSettings, diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index b9fabb62b0419..87d4170a1035f 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -12,6 +12,7 @@ import React, { useMemo } from 'react'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { css } from '@emotion/css'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { Chart } from '../chart'; import { Panels, PANELS_MODE } from '../panels'; import type { @@ -76,7 +77,7 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren /** * Callback to invoke when the user clicks the edit visualization button -- leave undefined to hide the button */ - onEditVisualization?: () => void; + onEditVisualization?: (lensAttributes: TypedLensByValueInput['attributes']) => void; /** * Callback to hide or show the chart -- should set {@link UnifiedHistogramChartContext.hidden} to chartHidden */ diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx index 8e2e19e159ed7..eca0c032ee224 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx @@ -2150,58 +2150,6 @@ describe('IndexPattern Data Source suggestions', () => { }) ); }); - - it('should apply a bucketed aggregation for a date field, using breakdown', () => { - const suggestions = getDatasourceSuggestionsForVisualizeField( - stateWithoutLayer(), - '1', - 'timestamp', - expectedIndexPatterns, - 'dest' - ); - - expect(suggestions).toContainEqual( - expect.objectContaining({ - state: expect.objectContaining({ - layers: { - id1: expect.objectContaining({ - columnOrder: ['id7', 'id8', 'id6'], - columns: { - id6: expect.objectContaining({ - operationType: 'count', - sourceField: '___records___', - }), - id7: expect.objectContaining({ - operationType: 'terms', - label: 'Top 5 values of dest', - }), - id8: expect.objectContaining({ - operationType: 'date_histogram', - }), - }, - }), - }, - }), - table: { - changeType: 'initial', - label: undefined, - isMultiRow: true, - columns: [ - expect.objectContaining({ - columnId: 'id7', - }), - expect.objectContaining({ - columnId: 'id8', - }), - expect.objectContaining({ - columnId: 'id6', - }), - ], - layerId: 'id1', - }, - }) - ); - }); }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts index af167807d060c..81ce81bb49053 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.ts @@ -363,15 +363,13 @@ export function getDatasourceSuggestionsForVisualizeField( state: FormBasedPrivateState, indexPatternId: string, fieldName: string, - indexPatterns: IndexPatternMap, - breakdownField?: string + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); // Identify the field by the indexPatternId and the fieldName const indexPattern = indexPatterns[indexPatternId]; const field = indexPattern?.getFieldByName(fieldName); - const breakdown = breakdownField ? indexPattern?.getFieldByName(breakdownField) : undefined; if (layerIds.length !== 0 || !field) return []; const newId = generateId(); @@ -380,16 +378,14 @@ export function getDatasourceSuggestionsForVisualizeField( newId, indexPatternId, field, - indexPatterns, - breakdown + indexPatterns ).concat( getEmptyLayerSuggestionsForField( { ...state, layers: {} }, newId, indexPatternId, field, - indexPatterns, - breakdown + indexPatterns ) ); } @@ -522,19 +518,13 @@ function getEmptyLayerSuggestionsForField( layerId: string, indexPatternId: string, field: IndexPatternField, - indexPatterns: IndexPatternMap, - breakdownField?: IndexPatternField + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { const indexPattern = indexPatterns[indexPatternId]; let newLayer: FormBasedLayer | undefined; const bucketOperation = getBucketOperation(field); if (bucketOperation) { - newLayer = createNewLayerWithBucketAggregation( - indexPattern, - field, - bucketOperation, - breakdownField - ); + newLayer = createNewLayerWithBucketAggregation(indexPattern, field, bucketOperation); } else if (indexPattern.timeFieldName && getOperationTypesForField(field).length > 0) { newLayer = createNewLayerWithMetricAggregation(indexPattern, field); } @@ -564,36 +554,14 @@ function getEmptyLayerSuggestionsForField( function createNewLayerWithBucketAggregation( indexPattern: IndexPattern, field: IndexPatternField, - operation: OperationType, - breakdownField?: IndexPatternField + operation: OperationType ): FormBasedLayer { - const countColumnId = generateId(); - - const getBreakdownColumn = () => { - const splitColumnId = generateId(); - return insertNewColumn({ - op: 'terms', - layer: { - indexPatternId: indexPattern.id, - columns: {}, - columnOrder: [], - splitAccessor: splitColumnId, - }, - columnId: splitColumnId, - field: breakdownField, - indexPattern, - visualizationGroups: [], - }); - }; - return insertNewColumn({ op: operation, layer: insertNewColumn({ op: 'count', - layer: breakdownField - ? getBreakdownColumn() - : { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] }, - columnId: countColumnId, + layer: { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] }, + columnId: generateId(), field: documentField, indexPattern, visualizationGroups: [], diff --git a/x-pack/plugins/lens/public/datasources/form_based/types.ts b/x-pack/plugins/lens/public/datasources/form_based/types.ts index 4a89fddb46620..0846c96d76dc8 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/types.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/types.ts @@ -55,7 +55,6 @@ export interface FormBasedLayer { // Partial columns represent the temporary invalid states incompleteColumns?: Record; sampling?: number; - splitAccessor?: string; } export interface FormBasedPersistedState { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index f973fceb6c6d9..e86f602465584 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -217,8 +217,7 @@ describe('suggestion helpers', () => { datasourceStates.mock.state, '1', 'test', - dataViews.indexPatterns, - undefined + dataViews.indexPatterns ); }); @@ -259,15 +258,13 @@ describe('suggestion helpers', () => { multiDatasourceStates.mock.state, '1', 'test', - dataViews.indexPatterns, - undefined + dataViews.indexPatterns ); expect(multiDatasourceMap.mock2.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( multiDatasourceStates.mock2.state, '1', 'test', - dataViews.indexPatterns, - undefined + dataViews.indexPatterns ); expect( multiDatasourceMap.mock3.getDatasourceSuggestionsForVisualizeField diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 57edaff6c1f5a..81298a97f650e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -106,8 +106,7 @@ export function getSuggestions({ datasourceState, visualizeTriggerFieldContext.dataViewSpec.id!, visualizeTriggerFieldContext.fieldName, - dataViews.indexPatterns, - visualizeTriggerFieldContext.breakdownField + dataViews.indexPatterns ); } } else if (field) { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 7efc3bc44d238..8540f3a87b49c 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -402,8 +402,7 @@ export interface Datasource { state: T, indexPatternId: string, fieldName: string, - indexPatterns: IndexPatternMap, - breakdownField?: string + indexPatterns: IndexPatternMap ) => Array>; getDatasourceSuggestionsFromCurrentState: ( state: T, diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts index 7d77d5d9c4480..8c3ebd7fd06a2 100644 --- a/x-pack/test/functional/apps/discover/visualize_field.ts +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -99,7 +99,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { const breakdownLabel = await testSubjects.find( - 'lnsDragDrop_draggable-Top 5 values of extension.raw' + 'lnsDragDrop_draggable-Top 3 values of extension.raw' ); const lnsWorkspace = await testSubjects.find('lnsWorkspace'); @@ -108,8 +108,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { list.map((elem: WebElementWrapper) => elem.getVisibleText()) ); - expect(await breakdownLabel.getVisibleText()).to.eql('Top 5 values of extension.raw'); - expect(values).to.eql(['php', 'gif', 'png', 'css', 'jpg']); + expect(await breakdownLabel.getVisibleText()).to.eql('Top 3 values of extension.raw'); + expect(values).to.eql(['Other', 'png', 'css', 'jpg']); }); }); From 5c8e0f9c2ce8344e0fcc68748f1b7c4b73135925 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 24 Nov 2022 21:39:26 -0400 Subject: [PATCH 72/84] [Discover] Added tooltip to breakdown field selector --- .../public/chart/breakdown_field_selector.tsx | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index ced77ba486132..6773115a5ed1c 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { EuiComboBox, EuiComboBoxOptionOption, useEuiTheme } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiToolTip, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { i18n } from '@kbn/i18n'; -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { UnifiedHistogramBreakdownContext } from '../types'; import { fieldSupportsBreakdown } from './field_supports_breakdown'; @@ -45,26 +45,42 @@ export const BreakdownFieldSelector = ({ [dataView.fields, onBreakdownFieldChange] ); + const [fieldPopoverDisabled, setFieldPopoverDisabled] = useState(false); + const disableFieldPopover = useCallback(() => setFieldPopoverDisabled(true), []); + const enableFieldPopover = useCallback( + () => setTimeout(() => setFieldPopoverDisabled(false)), + [] + ); + const { euiTheme } = useEuiTheme(); const breakdownCss = css` + width: 100%; max-width: ${euiTheme.base * 22}px; `; return ( - + + + ); }; From 84c0d74dc25bd064a2314171130ccd214a70c50f Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 28 Nov 2022 15:40:41 -0400 Subject: [PATCH 73/84] [Discover] Remove dscPageContent__inner class, add Lens input title support for unified histogram, clear lensRequests when chart context becomes undefined, remove unnecessary unified_histogram TS dependencies --- .../layout/discover_histogram_layout.tsx | 7 +- .../components/layout/discover_layout.scss | 3 +- .../layout/use_discover_histogram.test.ts | 147 +++++++++++++----- .../layout/use_discover_histogram.ts | 18 ++- .../unified_histogram/public/chart/chart.tsx | 3 +- .../public/chart/get_lens_attributes.test.ts | 21 +-- .../public/chart/get_lens_attributes.ts | 4 +- .../public/chart/histogram.test.tsx | 1 + src/plugins/unified_histogram/public/types.ts | 4 + x-pack/plugins/data_visualizer/tsconfig.json | 1 - x-pack/plugins/observability/tsconfig.json | 1 - 11 files changed, 147 insertions(+), 63 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx index 7f7e573e000a9..e6225a26ffea1 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx @@ -8,6 +8,7 @@ import React, { RefObject } from 'react'; import { UnifiedHistogramLayout } from '@kbn/unified-histogram-plugin/public'; +import { css } from '@emotion/react'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverHistogram } from './use_discover_histogram'; import type { DiscoverSearchSessionManager } from '../../services/discover_search_session'; @@ -59,15 +60,19 @@ export const DiscoverHistogramLayout = ({ return null; } + const histogramLayoutCss = css` + height: 100%; + `; + return ( : undefined } + css={histogramLayoutCss} {...histogramProps} > { isTimeBased = true, canVisualize = true, storage = new LocalStorageMock({}) as unknown as Storage, + state = { interval: 'auto', hideChart: false, breakdownField: 'extension' }, stateContainer = {}, + searchSessionManager, searchSessionId = '123', inspectorAdapters = { requests: new RequestAdapter() }, totalHits$ = new BehaviorSubject({ @@ -105,7 +108,9 @@ describe('useDiscoverHistogram', () => { isTimeBased?: boolean; canVisualize?: boolean; storage?: Storage; + state?: AppState; stateContainer?: unknown; + searchSessionManager?: DiscoverSearchSessionManager; searchSessionId?: string | null; inspectorAdapters?: InspectorAdapters; totalHits$?: DataTotalHits$; @@ -131,55 +136,70 @@ describe('useDiscoverHistogram', () => { availableFields$, }; - const session = getSessionServiceMock(); - - session.getSession$.mockReturnValue(new BehaviorSubject(searchSessionId ?? undefined)); + if (!searchSessionManager) { + const session = getSessionServiceMock(); + session.getSession$.mockReturnValue(new BehaviorSubject(searchSessionId ?? undefined)); + searchSessionManager = createSearchSessionMock(session).searchSessionManager; + } + + const initialProps = { + stateContainer: stateContainer as GetStateReturn, + state, + savedSearchData$, + dataView: dataViewWithTimefieldMock, + savedSearch: savedSearchMock, + isTimeBased, + isPlainRecord, + inspectorAdapters, + searchSessionManager: searchSessionManager!, + }; - const hook = renderHook(() => { - return useDiscoverHistogram({ - stateContainer: stateContainer as GetStateReturn, - state: { interval: 'auto', hideChart: false, breakdownField: 'extension' }, - savedSearchData$, - dataView: dataViewWithTimefieldMock, - savedSearch: savedSearchMock, - isTimeBased, - isPlainRecord, - inspectorAdapters, - searchSessionManager: createSearchSessionMock(session).searchSessionManager, - }); - }); + const hook = renderHook( + (props: Parameters[0]) => useDiscoverHistogram(props), + { initialProps } + ); await act(() => setTimeout(0)); - return hook; + return { hook, initialProps }; }; it('should return undefined if there is no search session', async () => { - const { result } = await renderUseDiscoverHistogram({ searchSessionId: null }); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ searchSessionId: null }); expect(result.current).toBeUndefined(); }); describe('contexts', () => { it('should output the correct hits context', async () => { - const { result } = await renderUseDiscoverHistogram(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); expect(result.current?.hits?.status).toBe(UnifiedHistogramFetchStatus.complete); expect(result.current?.hits?.total).toEqual(esHits.length); }); it('should output the correct chart context', async () => { - const { result } = await renderUseDiscoverHistogram(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); expect(result.current?.chart?.hidden).toBe(false); expect(result.current?.chart?.timeInterval).toBe('auto'); }); it('should output the correct breakdown context', async () => { - const { result } = await renderUseDiscoverHistogram(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); expect(result.current?.breakdown?.field?.name).toBe('extension'); }); it('should output the correct request context', async () => { const requestAdapter = new RequestAdapter(); - const { result } = await renderUseDiscoverHistogram({ + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ searchSessionId: '321', inspectorAdapters: { requests: requestAdapter }, }); @@ -188,33 +208,56 @@ describe('useDiscoverHistogram', () => { }); it('should output undefined for hits and chart and breakdown if isPlainRecord is true', async () => { - const { result } = await renderUseDiscoverHistogram({ isPlainRecord: true }); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ isPlainRecord: true }); expect(result.current?.hits).toBeUndefined(); expect(result.current?.chart).toBeUndefined(); expect(result.current?.breakdown).toBeUndefined(); }); it('should output undefined for chart and breakdown if isTimeBased is false', async () => { - const { result } = await renderUseDiscoverHistogram({ isTimeBased: false }); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ isTimeBased: false }); expect(result.current?.hits).not.toBeUndefined(); expect(result.current?.chart).toBeUndefined(); expect(result.current?.breakdown).toBeUndefined(); }); + + it('should clear lensRequests when chart is undefined', async () => { + const inspectorAdapters = { + requests: new RequestAdapter(), + lensRequests: new RequestAdapter(), + }; + const { hook, initialProps } = await renderUseDiscoverHistogram({ + inspectorAdapters, + }); + expect(inspectorAdapters.lensRequests).toBeDefined(); + hook.rerender({ ...initialProps, isPlainRecord: true }); + expect(inspectorAdapters.lensRequests).toBeUndefined(); + }); }); describe('onEditVisualization', () => { it('returns a callback for onEditVisualization when the data view can be visualized', async () => { - const { result } = await renderUseDiscoverHistogram(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); expect(result.current?.onEditVisualization).toBeDefined(); }); it('returns undefined for onEditVisualization when the data view cannot be visualized', async () => { - const { result } = await renderUseDiscoverHistogram({ canVisualize: false }); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ canVisualize: false }); expect(result.current?.onEditVisualization).toBeUndefined(); }); it('should call lens.navigateToPrefilledEditor when onEditVisualization is called', async () => { - const { result } = await renderUseDiscoverHistogram(); + const { + hook: { result }, + } = await renderUseDiscoverHistogram(); const attributes = { title: 'test' } as TypedLensByValueInput['attributes']; result.current?.onEditVisualization!(attributes); expect(mockLens.navigateToPrefilledEditor).toHaveBeenCalledWith({ @@ -229,7 +272,9 @@ describe('useDiscoverHistogram', () => { it('should try to get the topPanelHeight from storage', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; storage.get = jest.fn(() => 100); - const { result } = await renderUseDiscoverHistogram({ storage }); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ storage }); expect(storage.get).toHaveBeenCalledWith(HISTOGRAM_HEIGHT_KEY); expect(result.current?.topPanelHeight).toBe(100); }); @@ -238,7 +283,9 @@ describe('useDiscoverHistogram', () => { const storage = new LocalStorageMock({}) as unknown as Storage; storage.get = jest.fn(() => 100); storage.set = jest.fn(); - const { result } = await renderUseDiscoverHistogram({ storage }); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ storage }); expect(result.current?.topPanelHeight).toBe(100); act(() => { result.current?.onTopPanelHeightChange(200); @@ -252,27 +299,37 @@ describe('useDiscoverHistogram', () => { it('should update chartHidden when onChartHiddenChange is called', async () => { const storage = new LocalStorageMock({}) as unknown as Storage; storage.set = jest.fn(); + const state = { interval: 'auto', hideChart: false, breakdownField: 'extension' }; const stateContainer = { - setAppState: jest.fn(), + setAppState: jest.fn((newState) => { + Object.assign(state, newState); + }), }; + const session = getSessionServiceMock(); + const session$ = new BehaviorSubject('123'); + session.getSession$.mockReturnValue(session$); const inspectorAdapters = { requests: new RequestAdapter(), lensRequests: new RequestAdapter(), }; - const { result } = await renderUseDiscoverHistogram({ + const { hook } = await renderUseDiscoverHistogram({ storage, + state, stateContainer, + searchSessionManager: createSearchSessionMock(session).searchSessionManager, inspectorAdapters, }); act(() => { - result.current?.onChartHiddenChange(false); + hook.result.current?.onChartHiddenChange(false); }); expect(inspectorAdapters.lensRequests).toBeDefined(); expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, false); expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: false }); act(() => { - result.current?.onChartHiddenChange(true); + hook.result.current?.onChartHiddenChange(true); + session$.next('321'); }); + hook.rerender(); expect(inspectorAdapters.lensRequests).toBeUndefined(); expect(storage.set).toHaveBeenCalledWith(CHART_HIDDEN_KEY, true); expect(stateContainer.setAppState).toHaveBeenCalledWith({ hideChart: true }); @@ -284,7 +341,9 @@ describe('useDiscoverHistogram', () => { requests: new RequestAdapter(), lensRequests: undefined as RequestAdapter | undefined, }; - const { result } = await renderUseDiscoverHistogram({ inspectorAdapters }); + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ inspectorAdapters }); expect(inspectorAdapters.lensRequests).toBeUndefined(); act(() => { result.current?.onChartLoad({ complete: true, adapters: { requests: lensRequests } }); @@ -302,7 +361,9 @@ describe('useDiscoverHistogram', () => { requests: new RequestAdapter(), lensRequests: new RequestAdapter(), }; - const { result } = await renderUseDiscoverHistogram({ + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ storage, stateContainer, inspectorAdapters, @@ -318,7 +379,9 @@ describe('useDiscoverHistogram', () => { const stateContainer = { setAppState: jest.fn(), }; - const { result } = await renderUseDiscoverHistogram({ + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ stateContainer, }); act(() => { @@ -331,7 +394,9 @@ describe('useDiscoverHistogram', () => { const stateContainer = { setAppState: jest.fn(), }; - const { result } = await renderUseDiscoverHistogram({ + const { + hook: { result }, + } = await renderUseDiscoverHistogram({ stateContainer, }); act(() => { @@ -353,7 +418,7 @@ describe('useDiscoverHistogram', () => { recordRawType: RecordRawType.DOCUMENT, foundDocuments: true, }) as DataMain$; - const hook = await renderUseDiscoverHistogram({ totalHits$, main$ }); + const { hook } = await renderUseDiscoverHistogram({ totalHits$, main$ }); act(() => { hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.complete, 100); }); @@ -373,7 +438,7 @@ describe('useDiscoverHistogram', () => { fetchStatus: FetchStatus.UNINITIALIZED, result: undefined, }) as DataTotalHits$; - const hook = await renderUseDiscoverHistogram({ totalHits$ }); + const { hook } = await renderUseDiscoverHistogram({ totalHits$ }); const error = new Error('test'); act(() => { hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.error, error); @@ -395,7 +460,7 @@ describe('useDiscoverHistogram', () => { fetchStatus: FetchStatus.PARTIAL, result: undefined, }) as DataTotalHits$; - const hook = await renderUseDiscoverHistogram({ totalHits$ }); + const { hook } = await renderUseDiscoverHistogram({ totalHits$ }); act(() => { hook.result.current?.onTotalHitsChange(UnifiedHistogramFetchStatus.loading, undefined); }); diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index ff798385ab67d..f430aa01d03f0 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -17,6 +17,7 @@ import { import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; import useObservable from 'react-use/lib/useObservable'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { i18n } from '@kbn/i18n'; import { getUiActions } from '../../../../kibana_services'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDataState } from '../../hooks/use_data_state'; @@ -196,15 +197,10 @@ export const useDiscoverHistogram = ({ const onChartHiddenChange = useCallback( (chartHidden: boolean) => { - // Clear the Lens request adapter when the chart is hidden - if (chartHidden) { - inspectorAdapters.lensRequests = undefined; - } - storage.set(CHART_HIDDEN_KEY, chartHidden); stateContainer.setAppState({ hideChart: chartHidden }); }, - [inspectorAdapters, stateContainer, storage] + [stateContainer, storage] ); const onChartLoad = useCallback( @@ -221,12 +217,22 @@ export const useDiscoverHistogram = ({ isPlainRecord || !isTimeBased ? undefined : { + title: i18n.translate('discover.histogramTitle', { + defaultMessage: 'Discover histogram', + }), hidden: chartHidden, timeInterval: state.interval, }, [chartHidden, isPlainRecord, isTimeBased, state.interval] ); + // Clear the Lens request adapter when the chart is hidden + useEffect(() => { + if (chartHidden || !chart) { + inspectorAdapters.lensRequests = undefined; + } + }, [chart, chartHidden, inspectorAdapters]); + // state.chartHidden is updated before searchSessionId, which can trigger duplicate // requests, so instead of using state.chartHidden directly, we update chartHidden // when searchSessionId changes diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index d339723d6082e..bf712765017f4 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -162,13 +162,14 @@ export function Chart({ const lensAttributes = useMemo( () => getLensAttributes({ + title: chart?.title, filters, query, dataView, timeInterval: chart?.timeInterval, breakdownField: breakdown?.field, }), - [breakdown?.field, chart?.timeInterval, dataView, filters, query] + [breakdown?.field, chart?.timeInterval, chart?.title, dataView, filters, query] ); const onEditVisualization = useMemo( diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts index d9d1f03a36f8d..3e0ac936a6573 100644 --- a/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.test.ts @@ -44,8 +44,9 @@ describe('getLensAttributes', () => { it('should return correct attributes', () => { const breakdownField: DataViewField | undefined = undefined; - expect(getLensAttributes({ filters, query, dataView, timeInterval, breakdownField })) - .toMatchInlineSnapshot(` + expect( + getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField }) + ).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -173,7 +174,7 @@ describe('getLensAttributes', () => { "valueLabels": "hide", }, }, - "title": "", + "title": "test", "visualizationType": "lnsXY", } `); @@ -183,8 +184,9 @@ describe('getLensAttributes', () => { const breakdownField: DataViewField | undefined = dataView.fields.find( (f) => f.name === 'extension' ); - expect(getLensAttributes({ filters, query, dataView, timeInterval, breakdownField })) - .toMatchInlineSnapshot(` + expect( + getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField }) + ).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -330,7 +332,7 @@ describe('getLensAttributes', () => { "valueLabels": "hide", }, }, - "title": "", + "title": "test", "visualizationType": "lnsXY", } `); @@ -340,8 +342,9 @@ describe('getLensAttributes', () => { const breakdownField: DataViewField | undefined = dataView.fields.find( (f) => f.name === 'scripted' ); - expect(getLensAttributes({ filters, query, dataView, timeInterval, breakdownField })) - .toMatchInlineSnapshot(` + expect( + getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField }) + ).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -469,7 +472,7 @@ describe('getLensAttributes', () => { "valueLabels": "hide", }, }, - "title": "", + "title": "test", "visualizationType": "lnsXY", } `); diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts index ab443ed7b4f82..7702acd62724e 100644 --- a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -19,12 +19,14 @@ import type { import { fieldSupportsBreakdown } from './field_supports_breakdown'; export const getLensAttributes = ({ + title, filters, query, dataView, timeInterval, breakdownField, }: { + title?: string; filters: Filter[]; query: Query | AggregateQuery; dataView: DataView; @@ -102,7 +104,7 @@ export const getLensAttributes = ({ } return { - title: '', + title, references: [ { id: dataView.id ?? '', diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index ece569582520d..9d61290506086 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -24,6 +24,7 @@ jest.spyOn(useTimeRange, 'useTimeRange'); const getMockLensAttributes = () => getLensAttributes({ + title: 'test', filters: [], query: { language: 'kuery', diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts index c4579812216a4..a4b253274abde 100644 --- a/src/plugins/unified_histogram/public/types.ts +++ b/src/plugins/unified_histogram/public/types.ts @@ -102,6 +102,10 @@ export interface UnifiedHistogramChartContext { * Controls the time interval of the chart */ timeInterval?: string; + /** + * The chart title -- sets the title property on the Lens chart input + */ + title?: string; } /** diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index aef3ee72ababb..f818a547aa2fa 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -29,7 +29,6 @@ { "path": "../cloud/tsconfig.json" }, { "path": "../cloud_integrations/cloud_chat/tsconfig.json" }, { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/unified_histogram/tsconfig.json" }, { "path": "../../../src/plugins/discover/tsconfig.json" } ] } diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 4e701c10cb904..4c052e8c9ecaf 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -30,7 +30,6 @@ { "path": "../translations/tsconfig.json" }, { "path": "../../../src/plugins/unified_search/tsconfig.json"}, { "path": "../../../src/plugins/guided_onboarding/tsconfig.json"}, - { "path": "../../../src/plugins/unified_histogram/tsconfig.json" }, { "path": "../../../src/plugins/discover/tsconfig.json" } ] } From bbd0ce08409c6c9845e97f459dbca92acca8b61a Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 28 Nov 2022 18:11:53 -0400 Subject: [PATCH 74/84] [Discover] Add default unified histogram Lens input title --- .../main/components/layout/use_discover_histogram.ts | 4 ---- .../unified_histogram/public/chart/get_lens_attributes.ts | 6 +++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts index f430aa01d03f0..2141ab8cd21ef 100644 --- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts +++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts @@ -17,7 +17,6 @@ import { import type { UnifiedHistogramChartLoadEvent } from '@kbn/unified-histogram-plugin/public'; import useObservable from 'react-use/lib/useObservable'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; -import { i18n } from '@kbn/i18n'; import { getUiActions } from '../../../../kibana_services'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDataState } from '../../hooks/use_data_state'; @@ -217,9 +216,6 @@ export const useDiscoverHistogram = ({ isPlainRecord || !isTimeBased ? undefined : { - title: i18n.translate('discover.histogramTitle', { - defaultMessage: 'Discover histogram', - }), hidden: chartHidden, timeInterval: state.interval, }, diff --git a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts index 7702acd62724e..dc6f9216b4e56 100644 --- a/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts +++ b/src/plugins/unified_histogram/public/chart/get_lens_attributes.ts @@ -104,7 +104,11 @@ export const getLensAttributes = ({ } return { - title, + title: + title ?? + i18n.translate('unifiedHistogram.lensTitle', { + defaultMessage: 'Edit visualization', + }), references: [ { id: dataView.id ?? '', From 46ef36adac5ccda98771cac0ee8dd6f2ca281e5e Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 28 Nov 2022 21:13:07 -0400 Subject: [PATCH 75/84] [Discover] Remove duplicate .dscPageContent selector from discover_layout.scss --- .../application/main/components/layout/discover_layout.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index d0446d7b957d4..cb02801f4ce94 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -44,9 +44,6 @@ discover-app { position: relative; overflow: hidden; border: $euiBorderThin; -} - -.dscPageContent { height: 100%; } From 2352b8c03c9687aea388e997ce1d9ebc8791b0e3 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 29 Nov 2022 21:45:06 -0400 Subject: [PATCH 76/84] [Discover] Add breakdown selector aria label --- .../public/chart/breakdown_field_selector.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index 6773115a5ed1c..ef2a14e4423b6 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -72,6 +72,9 @@ export const BreakdownFieldSelector = ({ placeholder={i18n.translate('unifiedHistogram.breakdownFieldSelectorPlaceholder', { defaultMessage: 'Select field', })} + aria-label={i18n.translate('unifiedHistogram.breakdownFieldSelectorAriaLabel', { + defaultMessage: 'Break down by', + })} singleSelection={{ asPlainText: true }} options={fieldOptions} selectedOptions={selectedFields} From e4b6a0d6f2fb4687093eec7a137ab987f412445f Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 29 Nov 2022 21:46:15 -0400 Subject: [PATCH 77/84] [Discover] Fix elements overflowing the histogram container at small widths --- src/plugins/unified_histogram/public/chart/chart.tsx | 8 +++++++- .../public/chart/use_chart_styles.tsx | 12 ++++++++++++ .../public/hits_counter/hits_counter.tsx | 7 +++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index bf712765017f4..b1970cd26e365 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -151,6 +151,7 @@ export function Chart({ const { resultCountCss, + resultCountInnerCss, resultCountTitleCss, resultCountToggleCss, histogramCss, @@ -191,7 +192,12 @@ export function Chart({ responsive={false} > - + { ${euiTheme.size.s}; min-height: ${euiTheme.base * 2.5}px; `; + const resultCountInnerCss = css` + ${useEuiBreakpoint(['xs', 's'])} { + align-items: center; + } + `; const resultCountTitleCss = css` + flex-basis: auto; + ${useEuiBreakpoint(['xs', 's'])} { margin-bottom: 0 !important; } `; const resultCountToggleCss = css` + flex-basis: auto; + min-width: 0; + ${useEuiBreakpoint(['xs', 's'])} { align-items: flex-end; } @@ -42,6 +52,7 @@ export const useChartStyles = (chartVisible: boolean) => { width: 100%; `; const breakdownFieldSelectorItemCss = css` + min-width: 0; align-items: flex-end; padding-left: ${euiTheme.size.s}; `; @@ -53,6 +64,7 @@ export const useChartStyles = (chartVisible: boolean) => { return { resultCountCss, + resultCountInnerCss, resultCountTitleCss, resultCountToggleCss, histogramCss, diff --git a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx index 39df40650557c..b6f1212bfeaed 100644 --- a/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx +++ b/src/plugins/unified_histogram/public/hits_counter/hits_counter.tsx @@ -37,6 +37,9 @@ export function HitsCounter({ hits, append }: HitsCounterProps) { const hitsCounterCss = css` flex-grow: 0; `; + const hitsCounterTextCss = css` + overflow: hidden; + `; return ( - - + + {hits.status === 'partial' && ( Date: Wed, 30 Nov 2022 12:36:59 -0400 Subject: [PATCH 78/84] [Discover] Update check_registered_types test --- .../saved_objects/migrations/check_registered_types.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index 9eec84f23a57a..d5b6d0ae5da05 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -124,7 +124,7 @@ describe('checking migration metadata changes on all registered SO types', () => "osquery-saved-query": "7b213b4b7a3e59350e99c50e8df9948662ed493a", "query": "4640ef356321500a678869f24117b7091a911cb6", "sample-data-telemetry": "8b10336d9efae6f3d5593c4cc89fb4abcdf84e04", - "search": "963f2eb44b401e48c8c7cbe980e85572c2f52506", + "search": "b212646cedccdfb0f8504426f72d1e087c4bab7c", "search-session": "ba383309da68a15be3765977f7a44c84f0ec7964", "search-telemetry": "beb3fc25488c753f2a6dcff1845d667558712b66", "security-rule": "e0dfdba5d66139d0300723b2e6672993cd4a11f3", From b5742b620da8b1d83c73b86eefeac571378e4e4e Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 30 Nov 2022 14:22:01 -0400 Subject: [PATCH 79/84] [Discover] Update time range 'loading...' to 'Loading' --- .../unified_histogram/public/chart/use_time_range.test.tsx | 2 +- src/plugins/unified_histogram/public/chart/use_time_range.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx b/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx index 9ca788a489f41..26070db1c7e54 100644 --- a/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx +++ b/src/plugins/unified_histogram/public/chart/use_time_range.test.tsx @@ -71,7 +71,7 @@ describe('useTimeRange', () => { }) ); expect(result.current.timeRangeText).toMatchInlineSnapshot( - `"2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - loading...)"` + `"2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z (interval: Auto - Loading)"` ); }); diff --git a/src/plugins/unified_histogram/public/chart/use_time_range.tsx b/src/plugins/unified_histogram/public/chart/use_time_range.tsx index b12d7f3040a7b..539f32251b832 100644 --- a/src/plugins/unified_histogram/public/chart/use_time_range.tsx +++ b/src/plugins/unified_histogram/public/chart/use_time_range.tsx @@ -59,7 +59,7 @@ export const useTimeRange = ({ }${ bucketInterval?.description ?? i18n.translate('unifiedHistogram.histogramTimeRangeIntervalLoading', { - defaultMessage: 'loading...', + defaultMessage: 'Loading', }) }`, }, From 59be2b0ca5a0e0f447dfa704024a7e97248c5152 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 30 Nov 2022 14:22:52 -0400 Subject: [PATCH 80/84] [Discover] Emit an error when Lens fails so that Discover doesn't display 'no hits' --- src/plugins/unified_histogram/kibana.json | 2 +- .../public/chart/histogram.test.tsx | 42 +++++++++++++++++++ .../public/chart/histogram.tsx | 21 ++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/plugins/unified_histogram/kibana.json b/src/plugins/unified_histogram/kibana.json index 826a440585b7f..5be043637ed33 100755 --- a/src/plugins/unified_histogram/kibana.json +++ b/src/plugins/unified_histogram/kibana.json @@ -11,5 +11,5 @@ "ui": true, "requiredPlugins": [], "optionalPlugins": [], - "requiredBundles": ["data", "dataViews", "embeddable", "kibanaUtils"] + "requiredBundles": ["data", "dataViews", "embeddable", "kibanaUtils", "inspector"] } diff --git a/src/plugins/unified_histogram/public/chart/histogram.test.tsx b/src/plugins/unified_histogram/public/chart/histogram.test.tsx index 9d61290506086..03652a63f5456 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.test.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.test.tsx @@ -17,6 +17,7 @@ import { REQUEST_DEBOUNCE_MS } from './consts'; import { act } from 'react-dom/test-utils'; import * as buildBucketInterval from './build_bucket_interval'; import * as useTimeRange from './use_time_range'; +import { RequestStatus } from '@kbn/inspector-plugin/public'; const mockBucketInterval = { description: '1 minute', scale: undefined, scaled: false }; jest.spyOn(buildBucketInterval, 'buildBucketInterval').mockReturnValue(mockBucketInterval); @@ -183,6 +184,47 @@ describe('Histogram', () => { ); }); + it('should execute onLoad correctly when the request has a failure status', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + const adapters = createDefaultInspectorAdapters(); + jest + .spyOn(adapters.requests, 'getRequests') + .mockReturnValue([{ status: RequestStatus.ERROR } as any]); + onLoad(false, adapters); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.error, + undefined + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ complete: false, adapters }); + }); + + it('should execute onLoad correctly when the response has shard failures', async () => { + const { component, props } = mountComponent(); + const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; + const onLoad = component.find(embeddable).props().onLoad; + const adapters = createDefaultInspectorAdapters(); + const rawResponse = { + _shards: { + total: 1, + successful: 0, + skipped: 0, + failed: 1, + failures: [], + }, + }; + jest + .spyOn(adapters.requests, 'getRequests') + .mockReturnValue([{ response: { json: { rawResponse } } } as any]); + onLoad(false, adapters); + expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( + UnifiedHistogramFetchStatus.error, + undefined + ); + expect(props.onChartLoad).toHaveBeenLastCalledWith({ complete: false, adapters }); + }); + it('should not recreate onLoad in debounced lens props when hits.total changes', async () => { const { component, props } = mountComponent(); const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index e2d42b605594f..c30c6a1410985 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -17,6 +17,7 @@ import type { TimeRange } from '@kbn/es-query'; import useDebounce from 'react-use/lib/useDebounce'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { RequestStatus } from '@kbn/inspector-plugin/public'; import { UnifiedHistogramBucketInterval, UnifiedHistogramChartContext, @@ -73,6 +74,22 @@ export function Histogram({ const onLoad = useCallback( (isLoading: boolean, adapters: Partial | undefined) => { + const lensRequest = adapters?.requests?.getRequests()[0]; + const requestFailed = lensRequest?.status === RequestStatus.ERROR; + const json = lensRequest?.response?.json as + | IKibanaSearchResponse + | undefined; + const response = json?.rawResponse; + + // Lens will swallow shard failures and return `isLoading: false` because it displays + // its own errors, but this causes us to emit onTotalHitsChange(UnifiedHistogramFetchStatus.complete, 0). + // This is incorrect, so we check for request failures and shard failures here, and emit an error instead. + if (requestFailed || response?._shards.failed) { + onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, undefined); + onChartLoad?.({ complete: false, adapters: adapters ?? {} }); + return; + } + const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount; onTotalHitsChange?.( @@ -80,10 +97,6 @@ export function Histogram({ totalHits ?? previousHits.current ); - const lensRequest = adapters?.requests?.getRequests()[0]; - const json = lensRequest?.response?.json as IKibanaSearchResponse; - const response = json?.rawResponse; - if (response) { const newBucketInterval = buildBucketInterval({ data, From 467eab62dfe92b335f013c07f0afec56a69561e4 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 1 Dec 2022 15:21:13 +0000 Subject: [PATCH 81/84] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- src/plugins/discover/public/__mocks__/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 60cc72176f20a..d21bc4fc115b3 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -144,7 +144,7 @@ export function createDiscoverServicesMock(): DiscoverServices { savedObjectsTagging: {}, dataViews: dataPlugin.dataViews, timefilter: dataPlugin.query.timefilter.timefilter, - lens: { EmbeddableComponent: jest.fn(() => null) }, + lens: { EmbeddableComponent: jest.fn(() => null) }, locator: { useUrl: jest.fn(() => ''), navigate: jest.fn(), From 7f291a8ee0db5f1db680747bdbc77a6d5ef0d9d4 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 1 Dec 2022 15:55:12 -0400 Subject: [PATCH 82/84] [Discover] Update breakdownField to be text type --- .../saved_objects/migrations/check_registered_types.test.ts | 2 +- src/plugins/saved_search/server/saved_objects/search.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index 151327fb56f06..ff50b46b9b3f3 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -124,7 +124,7 @@ describe('checking migration metadata changes on all registered SO types', () => "osquery-saved-query": "7b213b4b7a3e59350e99c50e8df9948662ed493a", "query": "4640ef356321500a678869f24117b7091a911cb6", "sample-data-telemetry": "8b10336d9efae6f3d5593c4cc89fb4abcdf84e04", - "search": "b212646cedccdfb0f8504426f72d1e087c4bab7c", + "search": "c48f5ab5d94545780ea98de1bff9e39f17f3606b", "search-session": "ba383309da68a15be3765977f7a44c84f0ec7964", "search-telemetry": "beb3fc25488c753f2a6dcff1845d667558712b66", "security-rule": "e0dfdba5d66139d0300723b2e6672993cd4a11f3", diff --git a/src/plugins/saved_search/server/saved_objects/search.ts b/src/plugins/saved_search/server/saved_objects/search.ts index 2e1c507434f9b..5960cd8ebdbe9 100644 --- a/src/plugins/saved_search/server/saved_objects/search.ts +++ b/src/plugins/saved_search/server/saved_objects/search.ts @@ -68,7 +68,7 @@ export function getSavedSearchObjectType( }, }, rowsPerPage: { type: 'integer', index: false, doc_values: false }, - breakdownField: { type: 'keyword', index: false, doc_values: false }, + breakdownField: { type: 'text' }, }, }, migrations: () => getAllMigrations(getSearchSourceMigrations()), From 5ce04237cf461503432814dde8e6fbbe03a7e617 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 2 Dec 2022 21:38:23 -0400 Subject: [PATCH 83/84] [Discover] Fix false positives when setting chartDisplayChanged, causing extra fetches --- .../public/application/main/hooks/use_discover_state.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts index 9eed4f9c3cb2e..df61c909c6a8e 100644 --- a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts @@ -195,7 +195,9 @@ export function useDiscoverState({ useEffect(() => { const unsubscribe = appStateContainer.subscribe(async (nextState) => { const { hideChart, interval, breakdownField, sort, index } = state; - const chartDisplayChanged = nextState.hideChart !== hideChart; + // Cast to boolean to avoid false positives when comparing + // undefined and false, which would trigger a refetch + const chartDisplayChanged = Boolean(nextState.hideChart) !== Boolean(hideChart); const chartIntervalChanged = nextState.interval !== interval; const breakdownFieldChanged = nextState.breakdownField !== breakdownField; const docTableSortChanged = !isEqual(nextState.sort, sort); From 25f23d60426508b1900624ce66d16649da51e1c3 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 2 Dec 2022 21:39:56 -0400 Subject: [PATCH 84/84] [Discover] Fix flaky autorefresh test --- test/functional/apps/discover/group2/_huge_fields.ts | 1 + .../apps/discover/group2/_search_on_page_load.ts | 2 +- test/functional/page_objects/time_picker.ts | 9 ++++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/discover/group2/_huge_fields.ts b/test/functional/apps/discover/group2/_huge_fields.ts index 085788f1139d0..7ffcc891506e3 100644 --- a/test/functional/apps/discover/group2/_huge_fields.ts +++ b/test/functional/apps/discover/group2/_huge_fields.ts @@ -42,6 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.restoreDefaults(); await esArchiver.unload('test/functional/fixtures/es_archiver/huge_fields'); await kibanaServer.uiSettings.unset('timepicker:timeDefaults'); + await kibanaServer.savedObjects.cleanStandardList(); }); }); } diff --git a/test/functional/apps/discover/group2/_search_on_page_load.ts b/test/functional/apps/discover/group2/_search_on_page_load.ts index 2adeb9606d5f6..cb2b21e3849db 100644 --- a/test/functional/apps/discover/group2/_search_on_page_load.ts +++ b/test/functional/apps/discover/group2/_search_on_page_load.ts @@ -144,7 +144,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('discoverNewButton'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('number of fetches to be 0', waitForFetches(1)); + await retry.waitFor('number of fetches to be 1', waitForFetches(1)); expect(await PageObjects.discover.doesSidebarShowFields()).to.be(false); }); }); diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 59513bf350040..6e50f69a7474a 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -285,7 +285,14 @@ export class TimePickerPageObject extends FtrService { await this.testSubjects.click('superDatePickerToggleRefreshButton'); } - await this.inputValue('superDatePickerRefreshIntervalInput', intervalS.toString()); + await this.retry.waitFor('auto refresh to be set correctly', async () => { + await this.inputValue('superDatePickerRefreshIntervalInput', intervalS.toString()); + return ( + (await this.testSubjects.getAttribute('superDatePickerRefreshIntervalInput', 'value')) === + intervalS.toString() + ); + }); + await this.quickSelectTimeMenuToggle.close(); }