diff --git a/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap
index f6870a5209c1e..0e9ae4ee2aaaa 100644
--- a/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap
+++ b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap
@@ -95,6 +95,16 @@ exports[`FieldIcon renders known field types geo_shape is rendered 1`] = `
/>
`;
+exports[`FieldIcon renders known field types histogram is rendered 1`] = `
+
+`;
+
exports[`FieldIcon renders known field types ip is rendered 1`] = `
`;
+exports[`FieldIcon renders known field types keyword is rendered 1`] = `
+
+`;
+
exports[`FieldIcon renders known field types murmur3 is rendered 1`] = `
`;
+exports[`FieldIcon renders known field types text is rendered 1`] = `
+
+`;
+
exports[`FieldIcon renders with className if provided 1`] = `
> = {
murmur3: { iconType: 'tokenFile' },
number: { iconType: 'tokenNumber' },
number_range: { iconType: 'tokenNumber' },
+ histogram: { iconType: 'tokenHistogram' },
_source: { iconType: 'editorCodeBlock', color: 'gray' },
string: { iconType: 'tokenString' },
+ text: { iconType: 'tokenString' },
+ keyword: { iconType: 'tokenKeyword' },
nested: { iconType: 'tokenNested' },
};
diff --git a/src/plugins/discover/public/application/components/field_stats_table/constants.ts b/src/plugins/discover/public/application/components/field_stats_table/constants.ts
new file mode 100644
index 0000000000000..bf1a36da59ecf
--- /dev/null
+++ b/src/plugins/discover/public/application/components/field_stats_table/constants.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+/** Telemetry related to field statistics table **/
+export const FIELD_STATISTICS_LOADED = 'field_statistics_loaded';
+export const FIELD_STATISTICS_VIEW_CLICK = 'field_statistics_view_click';
+export const DOCUMENTS_VIEW_CLICK = 'documents_view_click';
diff --git a/src/plugins/discover/public/components/data_visualizer_grid/data_visualizer_grid.tsx b/src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx
similarity index 78%
rename from src/plugins/discover/public/components/data_visualizer_grid/data_visualizer_grid.tsx
rename to src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx
index 511aa90f5f4a4..5061ab0ba3746 100644
--- a/src/plugins/discover/public/components/data_visualizer_grid/data_visualizer_grid.tsx
+++ b/src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx
@@ -7,18 +7,21 @@
*/
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { Filter } from '@kbn/es-query';
-import { IndexPatternField, IndexPattern, DataView, Query } from '../../../../data/common';
-import { DiscoverServices } from '../../build_services';
+import type { Filter } from '@kbn/es-query';
+import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
+import { IndexPatternField, IndexPattern, DataView, Query } from '../../../../../data/common';
+import type { DiscoverServices } from '../../../build_services';
import {
EmbeddableInput,
EmbeddableOutput,
ErrorEmbeddable,
IEmbeddable,
isErrorEmbeddable,
-} from '../../../../embeddable/public';
-import { SavedSearch } from '../../services/saved_searches';
-import { GetStateReturn } from '../../application/main/services/discover_state';
+} from '../../../../../embeddable/public';
+import { FIELD_STATISTICS_LOADED } from './constants';
+import type { SavedSearch } from '../../../services/saved_searches';
+import type { GetStateReturn } from '../../main/services/discover_state';
+import { DataRefetch$ } from '../../main/utils/use_saved_search';
export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
indexPattern: IndexPattern;
@@ -36,7 +39,7 @@ export interface DataVisualizerGridEmbeddableOutput extends EmbeddableOutput {
showDistributions?: boolean;
}
-export interface DiscoverDataVisualizerGridProps {
+export interface FieldStatisticsTableProps {
/**
* Determines which columns are displayed
*/
@@ -69,14 +72,24 @@ export interface DiscoverDataVisualizerGridProps {
* Filters query to update the table content
*/
filters?: Filter[];
+ /**
+ * State container with persisted settings
+ */
stateContainer?: GetStateReturn;
/**
* Callback to add a filter to filter bar
*/
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+ /**
+ * Metric tracking function
+ * @param metricType
+ * @param eventName
+ */
+ trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
+ savedSearchRefetch$?: DataRefetch$;
}
-export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProps) => {
+export const FieldStatisticsTable = (props: FieldStatisticsTableProps) => {
const {
services,
indexPattern,
@@ -86,9 +99,10 @@ export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProp
filters,
stateContainer,
onAddFilter,
+ trackUiMetric,
+ savedSearchRefetch$,
} = props;
const { uiSettings } = services;
-
const [embeddable, setEmbeddable] = useState<
| ErrorEmbeddable
| IEmbeddable
@@ -109,10 +123,16 @@ export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProp
}
});
+ const refetch = savedSearchRefetch$?.subscribe(() => {
+ if (embeddable && !isErrorEmbeddable(embeddable)) {
+ embeddable.updateInput({ lastReloadRequestTime: Date.now() });
+ }
+ });
return () => {
sub?.unsubscribe();
+ refetch?.unsubscribe();
};
- }, [embeddable, stateContainer]);
+ }, [embeddable, stateContainer, savedSearchRefetch$]);
useEffect(() => {
if (embeddable && !isErrorEmbeddable(embeddable)) {
@@ -135,17 +155,11 @@ export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProp
embeddable.updateInput({
showPreviewByDefault,
});
+
embeddable.reload();
}
}, [showPreviewByDefault, uiSettings, embeddable]);
- useEffect(() => {
- return () => {
- // Clean up embeddable upon unmounting
- embeddable?.destroy();
- };
- }, [embeddable]);
-
useEffect(() => {
let unmounted = false;
const loadEmbeddable = async () => {
@@ -181,8 +195,15 @@ export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProp
useEffect(() => {
if (embeddableRoot.current && embeddable) {
embeddable.render(embeddableRoot.current);
+
+ trackUiMetric?.(METRIC_TYPE.LOADED, FIELD_STATISTICS_LOADED);
}
- }, [embeddable, embeddableRoot, uiSettings]);
+
+ return () => {
+ // Clean up embeddable upon unmounting
+ embeddable?.destroy();
+ };
+ }, [embeddable, embeddableRoot, uiSettings, trackUiMetric]);
return (
-
{
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);
+ }
+ }
},
- [stateContainer]
+ [trackUiMetric, stateContainer]
);
const fetchCounter = useRef(0);
@@ -315,7 +327,7 @@ export function DiscoverLayout({
stateContainer={stateContainer}
/>
) : (
-
)}
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
index 678eddcdf02c0..6864a1c5c2d4a 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
@@ -59,9 +59,11 @@ const FieldInfoIcon: React.FC = memo(() => (
));
-const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => (
-
-));
+const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => {
+ // If it's a string type, we want to distinguish between keyword and text
+ const tempType = field.type === 'string' && field.esTypes ? field.esTypes[0] : field.type;
+ return ;
+});
const FieldName: React.FC<{ field: IndexPatternField }> = memo(({ field }) => {
const title =
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
index b2b5c8a056995..9dd7ef19ffc07 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { each, cloneDeep } from 'lodash';
+import { cloneDeep, each } from 'lodash';
import { ReactWrapper } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
// @ts-expect-error
diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts
index e2d4c2f7ddcf2..f68395593bd8b 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts
+++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts
@@ -51,6 +51,15 @@ export function getFieldTypeName(type: string) {
return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', {
defaultMessage: 'String field',
});
+ case 'text':
+ return i18n.translate('discover.fieldNameIcons.textFieldAriaLabel', {
+ defaultMessage: 'Text field',
+ });
+ case 'keyword':
+ return i18n.translate('discover.fieldNameIcons.keywordFieldAriaLabel', {
+ defaultMessage: 'Keyword field',
+ });
+
case 'nested':
return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', {
defaultMessage: 'Nested field',
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 66e013e8f20ea..0b855a27cc74e 100644
--- a/src/plugins/discover/public/application/main/services/discover_state.ts
+++ b/src/plugins/discover/public/application/main/services/discover_state.ts
@@ -411,5 +411,7 @@ function createUrlGeneratorState({
}
: undefined,
useHash: false,
+ viewMode: appState.viewMode,
+ hideAggregatedPreview: appState.hideAggregatedPreview,
};
}
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index 914b9f25d29ae..c04e6515cfbe1 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
@@ -45,9 +45,9 @@ import { DiscoverGridSettings } from '../components/discover_grid/types';
import { DocTableProps } from '../components/doc_table/doc_table_wrapper';
import { getDefaultSort } from '../components/doc_table';
import { SortOrder } from '../components/doc_table/components/table_header/helpers';
-import { updateSearchSource } from './utils/update_search_source';
import { VIEW_MODE } from '../components/view_mode_toggle';
-import { FieldStatsTableEmbeddable } from '../components/data_visualizer_grid/field_stats_table_embeddable';
+import { updateSearchSource } from './utils/update_search_source';
+import { FieldStatsTableSavedSearchEmbeddable } from '../application/components/field_stats_table';
export type SearchProps = Partial &
Partial & {
@@ -391,7 +391,7 @@ export class SavedSearchEmbeddable
Array.isArray(searchProps.columns)
) {
ReactDOM.render(
- ;
@@ -102,6 +111,8 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition esFilters.isFilterPinned(f));
if (refreshInterval) queryState.refreshInterval = refreshInterval;
+ if (viewMode) appState.viewMode = viewMode;
+ if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview;
let path = `#/${savedSearchPath}`;
path = setStateToKbnUrl('_g', queryState, { useHash }, path);
diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts
index 7cc729fd7f7e5..32e89691574df 100644
--- a/src/plugins/discover/public/url_generator.ts
+++ b/src/plugins/discover/public/url_generator.ts
@@ -10,6 +10,7 @@ import type { UrlGeneratorsDefinition } from '../../share/public';
import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public';
import { esFilters } from '../../data/public';
import { setStateToKbnUrl } from '../../kibana_utils/public';
+import { VIEW_MODE } from './components/view_mode_toggle';
export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR';
@@ -75,6 +76,8 @@ export interface DiscoverUrlGeneratorState {
* id of the used saved query
*/
savedQuery?: string;
+ viewMode?: VIEW_MODE;
+ hideAggregatedPreview?: boolean;
}
interface Params {
@@ -104,6 +107,8 @@ export class DiscoverUrlGenerator
savedQuery,
sort,
interval,
+ viewMode,
+ hideAggregatedPreview,
}: DiscoverUrlGeneratorState): Promise => {
const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : '';
const appState: {
@@ -114,6 +119,8 @@ export class DiscoverUrlGenerator
interval?: string;
sort?: string[][];
savedQuery?: string;
+ viewMode?: VIEW_MODE;
+ hideAggregatedPreview?: boolean;
} = {};
const queryState: QueryState = {};
@@ -130,6 +137,8 @@ export class DiscoverUrlGenerator
if (filters && filters.length)
queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f));
if (refreshInterval) queryState.refreshInterval = refreshInterval;
+ if (viewMode) appState.viewMode = viewMode;
+ if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview;
let url = `${this.params.appBasePath}#/${savedSearchPath}`;
url = setStateToKbnUrl('_g', queryState, { useHash }, url);
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts b/x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts
similarity index 96%
rename from x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts
rename to x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts
index 9a5410918a099..62a3187be47dc 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts
+++ b/x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts
@@ -31,7 +31,7 @@ export declare class TimeBuckets {
public setMaxBars(maxBars: number): void;
public setInterval(interval: string): void;
public setBounds(bounds: TimeRangeBounds): void;
- public getBounds(): { min: any; max: any };
+ public getBounds(): { min: Moment; max: Moment };
public getInterval(): TimeBucketsInterval;
public getScaledDateFormat(): string;
}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js b/x-pack/plugins/data_visualizer/common/services/time_buckets.js
similarity index 98%
rename from x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js
rename to x-pack/plugins/data_visualizer/common/services/time_buckets.js
index 5d54b6c936fb2..49de535ee6c26 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js
+++ b/x-pack/plugins/data_visualizer/common/services/time_buckets.js
@@ -5,12 +5,12 @@
* 2.0.
*/
-import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common';
-import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common';
+import { FIELD_FORMAT_IDS } from '../../../../../src/plugins/field_formats/common';
+import { UI_SETTINGS } from '../../../../../src/plugins/data/common';
import { ary, assign, isPlainObject, isString, sortBy } from 'lodash';
import moment from 'moment';
import dateMath from '@elastic/datemath';
-import { parseInterval } from '../../common/util/parse_interval';
+import { parseInterval } from '../utils/parse_interval';
const { duration: d } = moment;
diff --git a/x-pack/plugins/data_visualizer/common/types/field_request_config.ts b/x-pack/plugins/data_visualizer/common/types/field_request_config.ts
index 36e8fe14b7002..f0ea7079bf750 100644
--- a/x-pack/plugins/data_visualizer/common/types/field_request_config.ts
+++ b/x-pack/plugins/data_visualizer/common/types/field_request_config.ts
@@ -14,7 +14,7 @@ export interface Percentile {
}
export interface FieldRequestConfig {
- fieldName?: string;
+ fieldName: string;
type: JobFieldType;
cardinality: number;
}
@@ -29,6 +29,7 @@ export interface DocumentCounts {
}
export interface FieldVisStats {
+ error?: Error;
cardinality?: number;
count?: number;
sampleCount?: number;
@@ -58,3 +59,10 @@ export interface FieldVisStats {
timeRangeEarliest?: number;
timeRangeLatest?: number;
}
+
+export interface DVErrorObject {
+ causedBy?: string;
+ message: string;
+ statusCode?: number;
+ fullError?: Error;
+}
diff --git a/x-pack/plugins/data_visualizer/server/types/chart_data.ts b/x-pack/plugins/data_visualizer/common/types/field_stats.ts
similarity index 50%
rename from x-pack/plugins/data_visualizer/server/types/chart_data.ts
rename to x-pack/plugins/data_visualizer/common/types/field_stats.ts
index 99c23cf88b5ba..8932a0641cbe6 100644
--- a/x-pack/plugins/data_visualizer/server/types/chart_data.ts
+++ b/x-pack/plugins/data_visualizer/common/types/field_stats.ts
@@ -5,6 +5,12 @@
* 2.0.
*/
+import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { Query } from '@kbn/es-query';
+import { isPopulatedObject } from '../utils/object_utils';
+import { IKibanaSearchResponse } from '../../../../../src/plugins/data/common';
+import { TimeBucketsInterval } from '../services/time_buckets';
+
export interface FieldData {
fieldName: string;
existsInDocs: boolean;
@@ -19,6 +25,12 @@ export interface Field {
fieldName: string;
type: string;
cardinality: number;
+ safeFieldName: string;
+}
+
+// @todo: check
+export function isValidField(arg: unknown): arg is Field {
+ return isPopulatedObject(arg, ['fieldName', 'type']) && typeof arg.fieldName === 'string';
}
export interface HistogramField {
@@ -27,19 +39,25 @@ export interface HistogramField {
}
export interface Distribution {
- percentiles: any[];
+ percentiles: Array<{ value?: number; percent: number; minValue: number; maxValue: number }>;
minPercentile: number;
maxPercentile: number;
}
-export interface Aggs {
- [key: string]: any;
-}
-
export interface Bucket {
doc_count: number;
}
+export interface FieldStatsError {
+ fieldName?: string;
+ fields?: Field[];
+ error: Error;
+}
+
+export const isIKibanaSearchResponse = (arg: unknown): arg is IKibanaSearchResponse => {
+ return isPopulatedObject(arg, ['rawResponse']);
+};
+
export interface NumericFieldStats {
fieldName: string;
count: number;
@@ -78,15 +96,15 @@ export interface BooleanFieldStats {
}
export interface DocumentCountStats {
- documentCounts: {
- interval: number;
- buckets: { [key: string]: number };
- };
+ interval: number;
+ buckets: { [key: string]: number };
+ timeRangeEarliest: number;
+ timeRangeLatest: number;
}
export interface FieldExamples {
fieldName: string;
- examples: any[];
+ examples: unknown[];
}
export interface NumericColumnStats {
@@ -97,10 +115,7 @@ export interface NumericColumnStats {
export type NumericColumnStatsMap = Record;
export interface AggHistogram {
- histogram: {
- field: string;
- interval: number;
- };
+ histogram: estypes.AggregationsHistogramAggregation;
}
export interface AggTerms {
@@ -142,17 +157,8 @@ export interface UnsupportedChartData {
type: 'unsupported';
}
-export interface FieldAggCardinality {
- field: string;
- percent?: any;
-}
-
-export interface ScriptAggCardinality {
- script: any;
-}
-
export interface AggCardinality {
- cardinality: FieldAggCardinality | ScriptAggCardinality;
+ cardinality: estypes.AggregationsCardinalityAggregation;
}
export type ChartRequestAgg = AggHistogram | AggCardinality | AggTerms;
@@ -166,3 +172,77 @@ export type BatchStats =
| DateFieldStats
| DocumentCountStats
| FieldExamples;
+
+export type FieldStats =
+ | NumericFieldStats
+ | StringFieldStats
+ | BooleanFieldStats
+ | DateFieldStats
+ | FieldExamples
+ | FieldStatsError;
+
+export function isValidFieldStats(arg: unknown): arg is FieldStats {
+ return isPopulatedObject(arg, ['fieldName', 'type', 'count']);
+}
+
+export interface FieldStatsCommonRequestParams {
+ index: string;
+ samplerShardSize: number;
+ timeFieldName?: string;
+ earliestMs?: number | undefined;
+ latestMs?: number | undefined;
+ runtimeFieldMap?: estypes.MappingRuntimeFields;
+ intervalMs?: number;
+ query: estypes.QueryDslQueryContainer;
+ maxExamples?: number;
+}
+
+export interface OverallStatsSearchStrategyParams {
+ sessionId?: string;
+ earliest?: number;
+ latest?: number;
+ aggInterval: TimeBucketsInterval;
+ intervalMs?: number;
+ searchQuery: Query['query'];
+ samplerShardSize: number;
+ index: string;
+ timeFieldName?: string;
+ runtimeFieldMap?: estypes.MappingRuntimeFields;
+ aggregatableFields: string[];
+ nonAggregatableFields: string[];
+}
+
+export interface FieldStatsSearchStrategyReturnBase {
+ progress: DataStatsFetchProgress;
+ fieldStats: Map | undefined;
+ startFetch: () => void;
+ cancelFetch: () => void;
+}
+
+export interface DataStatsFetchProgress {
+ error?: Error;
+ isRunning: boolean;
+ loaded: number;
+ total: number;
+}
+
+export interface FieldData {
+ fieldName: string;
+ existsInDocs: boolean;
+ stats?: {
+ sampleCount?: number;
+ count?: number;
+ cardinality?: number;
+ };
+}
+
+export interface Field {
+ fieldName: string;
+ type: string;
+ cardinality: number;
+ safeFieldName: string;
+}
+
+export interface Aggs {
+ [key: string]: estypes.AggregationsAggregationContainer;
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts b/x-pack/plugins/data_visualizer/common/types/field_vis_config.ts
similarity index 92%
rename from x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts
rename to x-pack/plugins/data_visualizer/common/types/field_vis_config.ts
index eeb9fe12692fd..dcd7da74b85ef 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts
+++ b/x-pack/plugins/data_visualizer/common/types/field_vis_config.ts
@@ -5,8 +5,7 @@
* 2.0.
*/
-import type { Percentile, JobFieldType, FieldVisStats } from '../../../../../../common/types';
-
+import type { Percentile, JobFieldType, FieldVisStats } from './index';
export interface MetricFieldVisStats {
avg?: number;
distribution?: {
@@ -23,7 +22,7 @@ export interface MetricFieldVisStats {
// which display the field information.
export interface FieldVisConfig {
type: JobFieldType;
- fieldName?: string;
+ fieldName: string;
displayName?: string;
existsInDocs: boolean;
aggregatable: boolean;
diff --git a/x-pack/plugins/data_visualizer/common/types/index.ts b/x-pack/plugins/data_visualizer/common/types/index.ts
index 1153b45e1cce2..381f7a556b18d 100644
--- a/x-pack/plugins/data_visualizer/common/types/index.ts
+++ b/x-pack/plugins/data_visualizer/common/types/index.ts
@@ -15,7 +15,6 @@ export type {
FieldVisStats,
Percentile,
} from './field_request_config';
-export type InputData = any[];
export interface DataVisualizerTableState {
pageSize: number;
diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.ts b/x-pack/plugins/data_visualizer/common/utils/parse_interval.ts
similarity index 100%
rename from x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.ts
rename to x-pack/plugins/data_visualizer/common/utils/parse_interval.ts
diff --git a/x-pack/plugins/data_visualizer/common/utils/query_utils.ts b/x-pack/plugins/data_visualizer/common/utils/query_utils.ts
index 2aa4cd063d1b1..dc21bbcae96c3 100644
--- a/x-pack/plugins/data_visualizer/common/utils/query_utils.ts
+++ b/x-pack/plugins/data_visualizer/common/utils/query_utils.ts
@@ -6,6 +6,8 @@
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { Query } from '@kbn/es-query';
+
/*
* Contains utility functions for building and processing queries.
*/
@@ -16,8 +18,8 @@ export function buildBaseFilterCriteria(
timeFieldName?: string,
earliestMs?: number,
latestMs?: number,
- query?: object
-) {
+ query?: Query['query']
+): estypes.QueryDslQueryContainer[] {
const filterCriteria = [];
if (timeFieldName && earliestMs && latestMs) {
filterCriteria.push({
@@ -31,7 +33,7 @@ export function buildBaseFilterCriteria(
});
}
- if (query) {
+ if (query && typeof query === 'object') {
filterCriteria.push(query);
}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx
index d49dbdc7cb446..832e18a12369f 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx
@@ -7,30 +7,25 @@
import React, { FC } from 'react';
import { DocumentCountChart, DocumentCountChartPoint } from './document_count_chart';
-import { FieldVisConfig, FileBasedFieldVisConfig } from '../stats_table/types';
import { TotalCountHeader } from './total_count_header';
+import { DocumentCountStats } from '../../../../../common/types/field_stats';
export interface Props {
- config?: FieldVisConfig | FileBasedFieldVisConfig;
+ documentCountStats?: DocumentCountStats;
totalCount: number;
}
-export const DocumentCountContent: FC = ({ config, totalCount }) => {
- if (config?.stats === undefined) {
+export const DocumentCountContent: FC = ({ documentCountStats, totalCount }) => {
+ if (documentCountStats === undefined) {
return totalCount !== undefined ? : null;
}
- const { documentCounts, timeRangeEarliest, timeRangeLatest } = config.stats;
- if (
- documentCounts === undefined ||
- timeRangeEarliest === undefined ||
- timeRangeLatest === undefined
- )
- return null;
+ const { timeRangeEarliest, timeRangeLatest } = documentCountStats;
+ if (timeRangeEarliest === undefined || timeRangeLatest === undefined) return null;
let chartPoints: DocumentCountChartPoint[] = [];
- if (documentCounts.buckets !== undefined) {
- const buckets: Record = documentCounts?.buckets;
+ if (documentCountStats.buckets !== undefined) {
+ const buckets: Record = documentCountStats?.buckets;
chartPoints = Object.entries(buckets).map(([time, value]) => ({ time: +time, value }));
}
@@ -41,7 +36,7 @@ export const DocumentCountContent: FC = ({ config, totalCount }) => {
chartPoints={chartPoints}
timeRangeEarliest={timeRangeEarliest}
timeRangeLatest={timeRangeLatest}
- interval={documentCounts.interval}
+ interval={documentCountStats.interval}
/>
>
);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx
index 8a9f9a25c16fa..7ba1615e22b43 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx
@@ -17,7 +17,7 @@ import {
} from '../stats_table/components/field_data_expanded_row';
import { GeoPointContent } from './geo_point_content/geo_point_content';
import { JOB_FIELD_TYPES } from '../../../../../common';
-import type { FileBasedFieldVisConfig } from '../stats_table/types/field_vis_config';
+import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config';
export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFieldVisConfig }) => {
const config = item;
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
index 79af35f1c8005..b87da2b3da789 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
@@ -23,6 +23,7 @@ import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { CombinedQuery } from '../../../index_data_visualizer/types/combined_query';
import { LoadingIndicator } from '../loading_indicator';
import { IndexPatternField } from '../../../../../../../../src/plugins/data/common';
+import { ErrorMessageContent } from '../stats_table/components/field_data_expanded_row/error_message';
export const IndexBasedDataVisualizerExpandedRow = ({
item,
@@ -46,6 +47,10 @@ export const IndexBasedDataVisualizerExpandedRow = ({
return ;
}
+ if (config.stats?.error) {
+ return ;
+ }
+
switch (type) {
case JOB_FIELD_TYPES.NUMBER:
return ;
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx
index 88b4cd406b33c..58e9b9b5740dc 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx
@@ -11,7 +11,7 @@ import { MultiSelectPicker } from '../multi_select_picker';
import type {
FileBasedFieldVisConfig,
FileBasedUnknownFieldVisConfig,
-} from '../stats_table/types/field_vis_config';
+} from '../../../../../common/types/field_vis_config';
interface Props {
fields: Array;
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap
index 398dc5dad2dc7..af4464cbc6b4e 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap
@@ -3,15 +3,13 @@
exports[`FieldTypeIcon render component when type matches a field type 1`] = `
-
`;
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
index b6a5ff3e5dbed..0c036dd6c6d76 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
@@ -14,7 +14,7 @@ import { JOB_FIELD_TYPES } from '../../../../../common';
describe('FieldTypeIcon', () => {
test(`render component when type matches a field type`, () => {
const typeIconComponent = shallow(
-
+
);
expect(typeIconComponent).toMatchSnapshot();
});
@@ -24,7 +24,7 @@ describe('FieldTypeIcon', () => {
jest.useFakeTimers();
const typeIconComponent = mount(
-
+
);
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
index 9d803e3d4a80c..2a9767ccd62b1 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
@@ -6,103 +6,32 @@
*/
import React, { FC } from 'react';
-import { EuiToken, EuiToolTip } from '@elastic/eui';
+import { EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { getJobTypeAriaLabel } from '../../util/field_types_utils';
+import { FieldIcon } from '@kbn/react-field/field_icon';
+import { getJobTypeLabel } from '../../util/field_types_utils';
import type { JobFieldType } from '../../../../../common';
import './_index.scss';
interface FieldTypeIconProps {
tooltipEnabled: boolean;
type: JobFieldType;
- needsAria: boolean;
}
-interface FieldTypeIconContainerProps {
- ariaLabel: string | null;
- iconType: string;
- color?: string;
- needsAria: boolean;
- [key: string]: any;
-}
-
-// defaultIcon => a unknown datatype
-const defaultIcon = { iconType: 'questionInCircle', color: 'gray' };
-
-// Extended & modified version of src/plugins/kibana_react/public/field_icon/field_icon.tsx
-export const typeToEuiIconMap: Record = {
- boolean: { iconType: 'tokenBoolean' },
- // icon for a data view mapping conflict in discover
- conflict: { iconType: 'alert', color: 'euiColorVis9' },
- date: { iconType: 'tokenDate' },
- date_range: { iconType: 'tokenDate' },
- geo_point: { iconType: 'tokenGeo' },
- geo_shape: { iconType: 'tokenGeo' },
- ip: { iconType: 'tokenIP' },
- ip_range: { iconType: 'tokenIP' },
- // is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html
- murmur3: { iconType: 'tokenFile' },
- number: { iconType: 'tokenNumber' },
- number_range: { iconType: 'tokenNumber' },
- histogram: { iconType: 'tokenHistogram' },
- _source: { iconType: 'editorCodeBlock', color: 'gray' },
- string: { iconType: 'tokenString' },
- text: { iconType: 'tokenString' },
- keyword: { iconType: 'tokenString' },
- nested: { iconType: 'tokenNested' },
-};
-
-export const FieldTypeIcon: FC = ({
- tooltipEnabled = false,
- type,
- needsAria = true,
-}) => {
- const ariaLabel = getJobTypeAriaLabel(type);
- const token = typeToEuiIconMap[type] || defaultIcon;
- const containerProps = { ...token, ariaLabel, needsAria };
-
+export const FieldTypeIcon: FC = ({ tooltipEnabled = false, type }) => {
+ const label =
+ getJobTypeLabel(type) ??
+ i18n.translate('xpack.dataVisualizer.fieldTypeIcon.fieldTypeTooltip', {
+ defaultMessage: '{type} type',
+ values: { type },
+ });
if (tooltipEnabled === true) {
return (
-
-
+
+
);
}
- return ;
-};
-
-// If the tooltip is used, it will apply its events to its first inner child.
-// To pass on its properties we apply `rest` to the outer `span` element.
-const FieldTypeIconContainer: FC = ({
- ariaLabel,
- iconType,
- color,
- needsAria,
- ...rest
-}) => {
- const wrapperProps: { className: string; 'aria-label'?: string } = {
- className: 'field-type-icon',
- };
- if (needsAria && ariaLabel) {
- wrapperProps['aria-label'] = ariaLabel;
- }
- return (
-
- );
+ return ;
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx
index 97dc2077d5931..0fa860bc6f55e 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx
@@ -12,7 +12,7 @@ import { MultiSelectPicker, Option } from '../multi_select_picker';
import type {
FileBasedFieldVisConfig,
FileBasedUnknownFieldVisConfig,
-} from '../stats_table/types/field_vis_config';
+} from '../../../../../common/types/field_vis_config';
import { FieldTypeIcon } from '../field_type_icon';
import { jobTypeLabels } from '../../util/field_types_utils';
@@ -50,7 +50,7 @@ export const DataVisualizerFieldTypesFilter: FC = ({
{label}
{type && (
-
+
)}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx
index b57072eed2944..1173ede84e631 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx
@@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui';
import type { FindFileStructureResponse } from '../../../../../../file_upload/common';
import type { DataVisualizerTableState } from '../../../../../common';
import { DataVisualizerTable, ItemIdToExpandedRowMap } from '../stats_table';
-import type { FileBasedFieldVisConfig } from '../stats_table/types/field_vis_config';
+import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config';
import { FileBasedDataVisualizerExpandedRow } from '../expanded_row';
import { DataVisualizerFieldNamesFilter } from '../field_names_filter';
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts
index 6c164233bdbc1..9f1ea4af22537 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts
@@ -9,7 +9,7 @@ import { JOB_FIELD_TYPES } from '../../../../../common';
import type {
FileBasedFieldVisConfig,
FileBasedUnknownFieldVisConfig,
-} from '../stats_table/types/field_vis_config';
+} from '../../../../../common/types/field_vis_config';
export function filterFields(
fields: Array,
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx
new file mode 100644
index 0000000000000..1d4a685457e25
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
+import { DVErrorObject } from '../../../../../index_data_visualizer/utils/error_utils';
+
+export const ErrorMessageContent = ({
+ fieldName,
+ error,
+}: {
+ fieldName: string;
+ error: DVErrorObject;
+}) => {
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx
index a5db86e0c30a0..d32a8a6dfb907 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx
@@ -21,12 +21,14 @@ export const IpContent: FC = ({ config, onAddFilter }) => {
return (
-
+ {stats && (
+
+ )}
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx
index 6f946fc1025ed..4fc73f0831dfc 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx
@@ -30,8 +30,9 @@ export const TextContent: FC = ({ config }) => {
{numExamples > 0 && }
{numExamples === 0 && (
-
+
{
expect(getLegendText(validUnsupportedChartData, 20)).toBe('Chart not supported.');
});
it('should return the chart legend text for empty datasets', () => {
- expect(getLegendText(validNumericChartData, 20)).toBe('0 documents contain field.');
+ expect(getLegendText(validNumericChartData, 20)).toBe('');
});
it('should return the chart legend text for boolean chart types', () => {
const { getByText } = render(
@@ -186,7 +186,7 @@ describe('useColumnChart()', () => {
);
expect(result.current.data).toStrictEqual([]);
- expect(result.current.legendText).toBe('0 documents contain field.');
+ expect(result.current.legendText).toBe('');
expect(result.current.xScaleType).toBe('linear');
});
});
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
index 60e1595c64ece..827e4a7f44857 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx
@@ -83,9 +83,7 @@ export const getLegendText = (chartData: ChartData, maxChartColumns: number): Le
}
if (chartData.data.length === 0) {
- return i18n.translate('xpack.dataVisualizer.dataGridChart.notEnoughData', {
- defaultMessage: `0 documents contain field.`,
- });
+ return '';
}
if (chartData.type === 'boolean') {
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
index 4e1c03aa987bd..976afc464a672 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
@@ -33,12 +33,13 @@ import {
FieldVisConfig,
FileBasedFieldVisConfig,
isIndexBasedFieldVisConfig,
-} from './types/field_vis_config';
+} from '../../../../../common/types/field_vis_config';
import { FileBasedNumberContentPreview } from '../field_data_row';
import { BooleanContentPreview } from './components/field_data_row';
import { calculateTableColumnsDimensions } from './utils';
import { DistinctValues } from './components/field_data_row/distinct_values';
import { FieldTypeIcon } from '../field_type_icon';
+import './_index.scss';
const FIELD_NAME = 'fieldName';
@@ -54,6 +55,7 @@ interface DataVisualizerTableProps {
showPreviewByDefault?: boolean;
/** Callback to receive any updates when table or page state is changed **/
onChange?: (update: Partial) => void;
+ loading?: boolean;
}
export const DataVisualizerTable = ({
@@ -64,6 +66,7 @@ export const DataVisualizerTable = ({
extendedColumns,
showPreviewByDefault,
onChange,
+ loading,
}: DataVisualizerTableProps) => {
const [expandedRowItemIds, setExpandedRowItemIds] = useState([]);
const [expandAll, setExpandAll] = useState(false);
@@ -180,7 +183,7 @@ export const DataVisualizerTable = ({
defaultMessage: 'Type',
}),
render: (fieldType: JobFieldType) => {
- return ;
+ return ;
},
width: dimensions.type,
sortable: true,
@@ -322,6 +325,13 @@ export const DataVisualizerTable = ({
{(resizeRef) => (
+ message={
+ loading
+ ? i18n.translate('xpack.dataVisualizer.dataGrid.searchingMessage', {
+ defaultMessage: 'Searching',
+ })
+ : undefined
+ }
className={'dvTable'}
items={items}
itemId={FIELD_NAME}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts
index 94b704764c93b..3d7678c7b60a5 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts
@@ -5,8 +5,11 @@
* 2.0.
*/
-import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config';
import { IndexPatternField } from '../../../../../../../../../src/plugins/data/common';
+import {
+ FieldVisConfig,
+ FileBasedFieldVisConfig,
+} from '../../../../../../common/types/field_vis_config';
export interface FieldDataRowProps {
config: FieldVisConfig | FileBasedFieldVisConfig;
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts
index 00f8ac0c74eb9..6d9f4d5b86d28 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts
@@ -4,11 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
export type { FieldDataRowProps } from './field_data_row';
export type {
FieldVisConfig,
FileBasedFieldVisConfig,
MetricFieldVisStats,
-} from './field_vis_config';
-export { isFileBasedFieldVisConfig, isIndexBasedFieldVisConfig } from './field_vis_config';
+} from '../../../../../../common/types/field_vis_config';
+export {
+ isFileBasedFieldVisConfig,
+ isIndexBasedFieldVisConfig,
+} from '../../../../../../common/types/field_vis_config';
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
index e2793512e23df..c9b4137a0106d 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx
@@ -43,7 +43,7 @@ function getPercentLabel(docCount: number, topValuesSampleSize: number): string
}
export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, onAddFilter }) => {
- if (stats === undefined) return null;
+ if (stats === undefined || !stats.topValues) return null;
const {
topValues,
topValuesSampleSize,
@@ -81,11 +81,11 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed,
size="xs"
label={kibanaFieldFormat(value.key, fieldFormat)}
className={classNames('eui-textTruncate', 'topValuesValueLabelContainer')}
- valueText={
+ valueText={`${value.doc_count}${
progressBarMax !== undefined
- ? getPercentLabel(value.doc_count, progressBarMax)
- : undefined
- }
+ ? ` (${getPercentLabel(value.doc_count, progressBarMax)})`
+ : ''
+ }`}
/>
{fieldName !== undefined && value.key !== undefined && onAddFilter !== undefined ? (
diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts
index 5c0867c7a0745..710ba12313f17 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts
@@ -6,24 +6,23 @@
*/
import { JOB_FIELD_TYPES } from '../../../../common';
-import { getJobTypeAriaLabel, jobTypeAriaLabels } from './field_types_utils';
+import { getJobTypeLabel, jobTypeLabels } from './field_types_utils';
describe('field type utils', () => {
- describe('getJobTypeAriaLabel: Getting a field type aria label by passing what it is stored in constants', () => {
+ describe('getJobTypeLabel: Getting a field type aria label by passing what it is stored in constants', () => {
test('should returns all JOB_FIELD_TYPES labels exactly as it is for each correct value', () => {
const keys = Object.keys(JOB_FIELD_TYPES);
const receivedLabels: Record = {};
- const testStorage = jobTypeAriaLabels;
- keys.forEach((constant) => {
- receivedLabels[constant] = getJobTypeAriaLabel(
- JOB_FIELD_TYPES[constant as keyof typeof JOB_FIELD_TYPES]
- );
+ const testStorage = jobTypeLabels;
+ keys.forEach((key) => {
+ const constant = key as keyof typeof JOB_FIELD_TYPES;
+ receivedLabels[JOB_FIELD_TYPES[constant]] = getJobTypeLabel(JOB_FIELD_TYPES[constant]);
});
expect(receivedLabels).toEqual(testStorage);
});
test('should returns NULL as JOB_FIELD_TYPES does not contain such a keyword', () => {
- expect(getJobTypeAriaLabel('JOB_FIELD_TYPES')).toBe(null);
+ expect(getJobTypeLabel('JOB_FIELD_TYPES')).toBe(null);
});
});
});
diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
index 3e459cd2b079b..1fda7140dbab2 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts
@@ -10,40 +10,8 @@ import { JOB_FIELD_TYPES } from '../../../../common';
import type { IndexPatternField } from '../../../../../../../src/plugins/data/common';
import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common';
-export const jobTypeAriaLabels = {
- BOOLEAN: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.booleanTypeAriaLabel', {
- defaultMessage: 'boolean type',
- }),
- DATE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.dateTypeAriaLabel', {
- defaultMessage: 'date type',
- }),
- GEO_POINT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoPointTypeAriaLabel', {
- defaultMessage: '{geoPointParam} type',
- values: {
- geoPointParam: 'geo point',
- },
- }),
- GEO_SHAPE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeAriaLabel', {
- defaultMessage: 'geo shape type',
- }),
- IP: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel', {
- defaultMessage: 'ip type',
- }),
- KEYWORD: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.keywordTypeAriaLabel', {
- defaultMessage: 'keyword type',
- }),
- NUMBER: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel', {
- defaultMessage: 'number type',
- }),
- HISTOGRAM: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.histogramTypeAriaLabel', {
- defaultMessage: 'histogram type',
- }),
- TEXT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel', {
- defaultMessage: 'text type',
- }),
- UNKNOWN: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.unknownTypeAriaLabel', {
- defaultMessage: 'unknown type',
- }),
+export const getJobTypeLabel = (type: string) => {
+ return type in jobTypeLabels ? jobTypeLabels[type as keyof typeof jobTypeLabels] : null;
};
export const jobTypeLabels = {
@@ -88,16 +56,6 @@ export const jobTypeLabels = {
}),
};
-export const getJobTypeAriaLabel = (type: string) => {
- const requestedFieldType = Object.keys(JOB_FIELD_TYPES).find(
- (k) => JOB_FIELD_TYPES[k as keyof typeof JOB_FIELD_TYPES] === type
- );
- if (requestedFieldType === undefined) {
- return null;
- }
- return jobTypeAriaLabels[requestedFieldType as keyof typeof jobTypeAriaLabels];
-};
-
// convert kibana types to ML Job types
// this is needed because kibana types only have string and not text and keyword.
// and we can't use ES_FIELD_TYPES because it has no NUMBER type
diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts b/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts
index a1608960a91bc..c259f82d12bfb 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { parseInterval } from './parse_interval';
+import { parseInterval } from '../../../../common/utils/parse_interval';
describe('ML parse interval util', () => {
test('should correctly parse an interval containing a valid unit and value', () => {
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx
index e5bd7a0d6f526..ebddd5527f5a2 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx
@@ -6,7 +6,6 @@
*/
import React, { FC } from 'react';
-
import { FormattedMessage } from '@kbn/i18n/react';
import { Query, IndexPattern, TimefilterContract } from 'src/plugins/data/public';
import { EuiButton } from '@elastic/eui';
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
index cdf4b718a93b7..f528d8378bcd2 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx
@@ -6,7 +6,6 @@
*/
import React, { FC, Fragment, useEffect, useMemo, useState, useCallback, useRef } from 'react';
-import { merge } from 'rxjs';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -16,6 +15,7 @@ import {
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPanel,
+ EuiProgress,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
@@ -24,12 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { Required } from 'utility-types';
import { i18n } from '@kbn/i18n';
import { Filter } from '@kbn/es-query';
-import {
- KBN_FIELD_TYPES,
- UI_SETTINGS,
- Query,
- generateFilters,
-} from '../../../../../../../../src/plugins/data/public';
+import { Query, generateFilters } from '../../../../../../../../src/plugins/data/public';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import { usePageUrlState, useUrlState } from '../../../common/util/url_state';
import {
@@ -37,39 +32,29 @@ import {
ItemIdToExpandedRowMap,
} from '../../../common/components/stats_table';
import { FieldVisConfig } from '../../../common/components/stats_table/types';
-import type {
- MetricFieldsStats,
- TotalFieldsStats,
-} from '../../../common/components/stats_table/components/field_count_stats';
+import type { TotalFieldsStats } from '../../../common/components/stats_table/components/field_count_stats';
import { OverallStats } from '../../types/overall_stats';
import { getActions } from '../../../common/components/field_data_row/action_menu';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer';
import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../types/combined_query';
-import {
- FieldRequestConfig,
- JobFieldType,
- SavedSearchSavedObject,
-} from '../../../../../common/types';
+import { JobFieldType, SavedSearchSavedObject } from '../../../../../common/types';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { FieldCountPanel } from '../../../common/components/field_count_panel';
import { DocumentCountContent } from '../../../common/components/document_count_content';
-import { DataLoader } from '../../data_loader/data_loader';
-import { JOB_FIELD_TYPES, OMIT_FIELDS } from '../../../../../common';
-import { useTimefilter } from '../../hooks/use_time_filter';
+import { OMIT_FIELDS } from '../../../../../common';
import { kbnTypeToJobType } from '../../../common/util/field_types_utils';
import { SearchPanel } from '../search_panel';
import { ActionsPanel } from '../actions_panel';
import { DatePickerWrapper } from '../../../common/components/date_picker_wrapper';
-import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service';
import { HelpMenu } from '../../../common/components/help_menu';
-import { TimeBuckets } from '../../services/time_buckets';
-import { createMergedEsQuery, getEsQueryFromSavedSearch } from '../../utils/saved_search_utils';
+import { createMergedEsQuery } from '../../utils/saved_search_utils';
import { DataVisualizerIndexPatternManagement } from '../index_pattern_management';
import { ResultLink } from '../../../common/components/results_links';
-import { extractErrorProperties } from '../../utils/error_utils';
import { IndexPatternField, IndexPattern } from '../../../../../../../../src/plugins/data/common';
+import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
+import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable';
import './_index.scss';
interface DataVisualizerPageState {
@@ -155,61 +140,14 @@ export const IndexDataVisualizerView: FC = (dataVi
}
}, [dataVisualizerProps?.currentSavedSearch]);
- useEffect(() => {
- return () => {
- // When navigating away from the data view
- // Reset all previously set filters
- // to make sure new page doesn't have unrelated filters
- data.query.filterManager.removeAll();
- };
- }, [currentIndexPattern.id, data.query.filterManager]);
-
- const getTimeBuckets = useCallback(() => {
- return new TimeBuckets({
- [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
- [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
- dateFormat: uiSettings.get('dateFormat'),
- 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
- });
- }, [uiSettings]);
-
- const timefilter = useTimefilter({
- timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined,
- autoRefreshSelector: true,
- });
-
- const dataLoader = useMemo(
- () => new DataLoader(currentIndexPattern, toasts),
- [currentIndexPattern, toasts]
- );
-
- useEffect(() => {
- if (globalState?.time !== undefined) {
- timefilter.setTime({
- from: globalState.time.from,
- to: globalState.time.to,
- });
- setLastRefresh(Date.now());
- }
- }, [globalState, timefilter]);
-
- useEffect(() => {
- if (globalState?.refreshInterval !== undefined) {
- timefilter.setRefreshInterval(globalState.refreshInterval);
- setLastRefresh(Date.now());
- }
- }, [globalState, timefilter]);
-
- const [lastRefresh, setLastRefresh] = useState(0);
-
useEffect(() => {
if (!currentIndexPattern.isTimeBased()) {
toasts.addWarning({
title: i18n.translate(
- 'xpack.dataVisualizer.index.dataViewNotBasedOnTimeSeriesNotificationTitle',
+ 'xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle',
{
- defaultMessage: 'The data view {dataViewTitle} is not based on a time series',
- values: { dataViewTitle: currentIndexPattern.title },
+ defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series',
+ values: { indexPatternTitle: currentIndexPattern.title },
}
),
text: i18n.translate(
@@ -225,7 +163,7 @@ export const IndexDataVisualizerView: FC = (dataVi
const indexPatternFields: IndexPatternField[] = currentIndexPattern.fields;
const fieldTypes = useMemo(() => {
- // Obtain the list of non metric field types which appear in the data view.
+ // Obtain the list of non metric field types which appear in the index pattern.
const indexedFieldTypes: JobFieldType[] = [];
indexPatternFields.forEach((field) => {
if (!OMIT_FIELDS.includes(field.name) && field.scripted !== true) {
@@ -238,35 +176,6 @@ export const IndexDataVisualizerView: FC = (dataVi
return indexedFieldTypes.sort();
}, [indexPatternFields]);
- const defaults = getDefaultPageState();
-
- const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
- const searchData = getEsQueryFromSavedSearch({
- indexPattern: currentIndexPattern,
- uiSettings,
- savedSearch: currentSavedSearch,
- filterManager: data.query.filterManager,
- });
-
- if (searchData === undefined || dataVisualizerListState.searchString !== '') {
- if (dataVisualizerListState.filters) {
- data.query.filterManager.setFilters(dataVisualizerListState.filters);
- }
- return {
- searchQuery: dataVisualizerListState.searchQuery,
- searchString: dataVisualizerListState.searchString,
- searchQueryLanguage: dataVisualizerListState.searchQueryLanguage,
- };
- } else {
- return {
- searchQuery: searchData.searchQuery,
- searchString: searchData.searchString,
- searchQueryLanguage: searchData.queryLanguage,
- };
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentSavedSearch, currentIndexPattern, dataVisualizerListState, data.query]);
-
const setSearchParams = useCallback(
(searchParams: {
searchQuery: Query['query'];
@@ -275,7 +184,7 @@ export const IndexDataVisualizerView: FC = (dataVi
filters: Filter[];
}) => {
// When the user loads saved search and then clear or modify the query
- // we should remove the saved search and replace it with the data view id
+ // we should remove the saved search and replace it with the index pattern id
if (currentSavedSearch !== null) {
setCurrentSavedSearch(null);
}
@@ -318,15 +227,58 @@ export const IndexDataVisualizerView: FC = (dataVi
});
};
- const [overallStats, setOverallStats] = useState(defaults.overallStats);
+ const input: DataVisualizerGridInput = useMemo(() => {
+ return {
+ indexPattern: currentIndexPattern,
+ savedSearch: currentSavedSearch,
+ visibleFieldNames,
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentIndexPattern.id, currentSavedSearch?.id, visibleFieldNames]);
+
+ const {
+ configs,
+ searchQueryLanguage,
+ searchString,
+ overallStats,
+ searchQuery,
+ documentCountStats,
+ metricsStats,
+ timefilter,
+ setLastRefresh,
+ progress,
+ } = useDataVisualizerGridData(input, dataVisualizerListState, setGlobalState);
+
+ useEffect(() => {
+ return () => {
+ // When navigating away from the index pattern
+ // Reset all previously set filters
+ // to make sure new page doesn't have unrelated filters
+ data.query.filterManager.removeAll();
+ };
+ }, [currentIndexPattern.id, data.query.filterManager]);
+
+ useEffect(() => {
+ // Force refresh on index pattern change
+ setLastRefresh(Date.now());
+ }, [currentIndexPattern.id, setLastRefresh]);
- const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats);
- const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs);
- const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded);
- const [metricsStats, setMetricsStats] = useState();
+ useEffect(() => {
+ if (globalState?.time !== undefined) {
+ timefilter.setTime({
+ from: globalState.time.from,
+ to: globalState.time.to,
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [JSON.stringify(globalState?.time), timefilter]);
- const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
- const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
+ useEffect(() => {
+ if (globalState?.refreshInterval !== undefined) {
+ timefilter.setRefreshInterval(globalState.refreshInterval);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [JSON.stringify(globalState?.refreshInterval), timefilter]);
const onAddFilter = useCallback(
(field: IndexPatternField | string, values: string, operation: '+' | '-') => {
@@ -374,422 +326,8 @@ export const IndexDataVisualizerView: FC = (dataVi
]
);
- useEffect(() => {
- const timeUpdateSubscription = merge(
- timefilter.getTimeUpdate$(),
- dataVisualizerRefresh$
- ).subscribe(() => {
- setGlobalState({
- time: timefilter.getTime(),
- refreshInterval: timefilter.getRefreshInterval(),
- });
- setLastRefresh(Date.now());
- });
- return () => {
- timeUpdateSubscription.unsubscribe();
- };
- });
-
- useEffect(() => {
- loadOverallStats();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchQuery, samplerShardSize, lastRefresh]);
-
- useEffect(() => {
- createMetricCards();
- createNonMetricCards();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [overallStats, showEmptyFields]);
-
- useEffect(() => {
- loadMetricFieldStats();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [metricConfigs]);
-
- useEffect(() => {
- loadNonMetricFieldStats();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [nonMetricConfigs]);
-
- useEffect(() => {
- createMetricCards();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [metricsLoaded]);
-
- useEffect(() => {
- createNonMetricCards();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [nonMetricsLoaded]);
-
- async function loadOverallStats() {
- const tf = timefilter as any;
- let earliest;
- let latest;
-
- const activeBounds = tf.getActiveBounds();
-
- if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) {
- return;
- }
-
- if (currentIndexPattern.timeFieldName !== undefined) {
- earliest = activeBounds.min.valueOf();
- latest = activeBounds.max.valueOf();
- }
-
- try {
- const allStats = await dataLoader.loadOverallData(
- searchQuery,
- samplerShardSize,
- earliest,
- latest
- );
- // Because load overall stats perform queries in batches
- // there could be multiple errors
- if (Array.isArray(allStats.errors) && allStats.errors.length > 0) {
- allStats.errors.forEach((err: any) => {
- dataLoader.displayError(extractErrorProperties(err));
- });
- }
- setOverallStats(allStats);
- } catch (err) {
- dataLoader.displayError(err.body ?? err);
- }
- }
-
- async function loadMetricFieldStats() {
- // Only request data for fields that exist in documents.
- if (metricConfigs.length === 0) {
- return;
- }
-
- const configsToLoad = metricConfigs.filter(
- (config) => config.existsInDocs === true && config.loading === true
- );
- if (configsToLoad.length === 0) {
- return;
- }
-
- // Pass the field name, type and cardinality in the request.
- // Top values will be obtained on a sample if cardinality > 100000.
- const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => {
- const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 };
- if (config.stats !== undefined && config.stats.cardinality !== undefined) {
- props.cardinality = config.stats.cardinality;
- }
- return props;
- });
-
- // Obtain the interval to use for date histogram aggregations
- // (such as the document count chart). Aim for 75 bars.
- const buckets = getTimeBuckets();
-
- const tf = timefilter as any;
- let earliest: number | undefined;
- let latest: number | undefined;
- if (currentIndexPattern.timeFieldName !== undefined) {
- earliest = tf.getActiveBounds().min.valueOf();
- latest = tf.getActiveBounds().max.valueOf();
- }
-
- const bounds = tf.getActiveBounds();
- const BAR_TARGET = 75;
- buckets.setInterval('auto');
- buckets.setBounds(bounds);
- buckets.setBarTarget(BAR_TARGET);
- const aggInterval = buckets.getInterval();
-
- try {
- const metricFieldStats = await dataLoader.loadFieldStats(
- searchQuery,
- samplerShardSize,
- earliest,
- latest,
- existMetricFields,
- aggInterval.asMilliseconds()
- );
-
- // Add the metric stats to the existing stats in the corresponding config.
- const configs: FieldVisConfig[] = [];
- metricConfigs.forEach((config) => {
- const configWithStats = { ...config };
- if (config.fieldName !== undefined) {
- configWithStats.stats = {
- ...configWithStats.stats,
- ...metricFieldStats.find(
- (fieldStats: any) => fieldStats.fieldName === config.fieldName
- ),
- };
- configWithStats.loading = false;
- configs.push(configWithStats);
- } else {
- // Document count card.
- configWithStats.stats = metricFieldStats.find(
- (fieldStats: any) => fieldStats.fieldName === undefined
- );
-
- if (configWithStats.stats !== undefined) {
- // Add earliest / latest of timefilter for setting x axis domain.
- configWithStats.stats.timeRangeEarliest = earliest;
- configWithStats.stats.timeRangeLatest = latest;
- }
- setDocumentCountStats(configWithStats);
- }
- });
-
- setMetricConfigs(configs);
- } catch (err) {
- dataLoader.displayError(err);
- }
- }
-
- async function loadNonMetricFieldStats() {
- // Only request data for fields that exist in documents.
- if (nonMetricConfigs.length === 0) {
- return;
- }
-
- const configsToLoad = nonMetricConfigs.filter(
- (config) => config.existsInDocs === true && config.loading === true
- );
- if (configsToLoad.length === 0) {
- return;
- }
-
- // Pass the field name, type and cardinality in the request.
- // Top values will be obtained on a sample if cardinality > 100000.
- const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => {
- const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 };
- if (config.stats !== undefined && config.stats.cardinality !== undefined) {
- props.cardinality = config.stats.cardinality;
- }
- return props;
- });
-
- const tf = timefilter as any;
- let earliest;
- let latest;
- if (currentIndexPattern.timeFieldName !== undefined) {
- earliest = tf.getActiveBounds().min.valueOf();
- latest = tf.getActiveBounds().max.valueOf();
- }
-
- try {
- const nonMetricFieldStats = await dataLoader.loadFieldStats(
- searchQuery,
- samplerShardSize,
- earliest,
- latest,
- existNonMetricFields
- );
-
- // Add the field stats to the existing stats in the corresponding config.
- const configs: FieldVisConfig[] = [];
- nonMetricConfigs.forEach((config) => {
- const configWithStats = { ...config };
- if (config.fieldName !== undefined) {
- configWithStats.stats = {
- ...configWithStats.stats,
- ...nonMetricFieldStats.find(
- (fieldStats: any) => fieldStats.fieldName === config.fieldName
- ),
- };
- }
- configWithStats.loading = false;
- configs.push(configWithStats);
- });
-
- setNonMetricConfigs(configs);
- } catch (err) {
- dataLoader.displayError(err);
- }
- }
-
- const createMetricCards = useCallback(() => {
- const configs: FieldVisConfig[] = [];
- const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || [];
-
- const allMetricFields = indexPatternFields.filter((f) => {
- return (
- f.type === KBN_FIELD_TYPES.NUMBER &&
- f.displayName !== undefined &&
- dataLoader.isDisplayField(f.displayName) === true
- );
- });
- const metricExistsFields = allMetricFields.filter((f) => {
- return aggregatableExistsFields.find((existsF) => {
- return existsF.fieldName === f.spec.name;
- });
- });
-
- // Add a config for 'document count', identified by no field name if indexpattern is time based.
- if (currentIndexPattern.timeFieldName !== undefined) {
- configs.push({
- type: JOB_FIELD_TYPES.NUMBER,
- existsInDocs: true,
- loading: true,
- aggregatable: true,
- });
- }
-
- if (metricsLoaded === false) {
- setMetricsLoaded(true);
- return;
- }
-
- let aggregatableFields: any[] = overallStats.aggregatableExistsFields;
- if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) {
- aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields);
- }
-
- const metricFieldsToShow =
- metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields;
-
- metricFieldsToShow.forEach((field) => {
- const fieldData = aggregatableFields.find((f) => {
- return f.fieldName === field.spec.name;
- });
-
- const metricConfig: FieldVisConfig = {
- ...(fieldData ? fieldData : {}),
- fieldFormat: currentIndexPattern.getFormatterForField(field),
- type: JOB_FIELD_TYPES.NUMBER,
- loading: true,
- aggregatable: true,
- deletable: field.runtimeField !== undefined,
- };
- if (field.displayName !== metricConfig.fieldName) {
- metricConfig.displayName = field.displayName;
- }
-
- configs.push(metricConfig);
- });
-
- setMetricsStats({
- totalMetricFieldsCount: allMetricFields.length,
- visibleMetricsCount: metricFieldsToShow.length,
- });
- setMetricConfigs(configs);
- }, [
- currentIndexPattern,
- dataLoader,
- indexPatternFields,
- metricsLoaded,
- overallStats,
- showEmptyFields,
- ]);
-
- const createNonMetricCards = useCallback(() => {
- const allNonMetricFields = indexPatternFields.filter((f) => {
- return (
- f.type !== KBN_FIELD_TYPES.NUMBER &&
- f.displayName !== undefined &&
- dataLoader.isDisplayField(f.displayName) === true
- );
- });
- // Obtain the list of all non-metric fields which appear in documents
- // (aggregatable or not aggregatable).
- const populatedNonMetricFields: any[] = []; // Kibana data view non metric fields.
- let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats.
- const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || [];
- const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || [];
-
- allNonMetricFields.forEach((f) => {
- const checkAggregatableField = aggregatableExistsFields.find(
- (existsField) => existsField.fieldName === f.spec.name
- );
-
- if (checkAggregatableField !== undefined) {
- populatedNonMetricFields.push(f);
- nonMetricFieldData.push(checkAggregatableField);
- } else {
- const checkNonAggregatableField = nonAggregatableExistsFields.find(
- (existsField) => existsField.fieldName === f.spec.name
- );
-
- if (checkNonAggregatableField !== undefined) {
- populatedNonMetricFields.push(f);
- nonMetricFieldData.push(checkNonAggregatableField);
- }
- }
- });
-
- if (nonMetricsLoaded === false) {
- setNonMetricsLoaded(true);
- return;
- }
-
- if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) {
- // Combine the field data obtained from Elasticsearch into a single array.
- nonMetricFieldData = nonMetricFieldData.concat(
- overallStats.aggregatableNotExistsFields,
- overallStats.nonAggregatableNotExistsFields
- );
- }
-
- const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields;
-
- const configs: FieldVisConfig[] = [];
-
- nonMetricFieldsToShow.forEach((field) => {
- const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
-
- const nonMetricConfig = {
- ...(fieldData ? fieldData : {}),
- fieldFormat: currentIndexPattern.getFormatterForField(field),
- aggregatable: field.aggregatable,
- scripted: field.scripted,
- loading: fieldData?.existsInDocs,
- deletable: field.runtimeField !== undefined,
- };
-
- // Map the field type from the Kibana data view to the field type
- // used in the data visualizer.
- const dataVisualizerType = kbnTypeToJobType(field);
- if (dataVisualizerType !== undefined) {
- nonMetricConfig.type = dataVisualizerType;
- } else {
- // Add a flag to indicate that this is one of the 'other' Kibana
- // field types that do not yet have a specific card type.
- nonMetricConfig.type = field.type;
- nonMetricConfig.isUnsupportedType = true;
- }
-
- if (field.displayName !== nonMetricConfig.fieldName) {
- nonMetricConfig.displayName = field.displayName;
- }
-
- configs.push(nonMetricConfig);
- });
-
- setNonMetricConfigs(configs);
- }, [
- currentIndexPattern,
- dataLoader,
- indexPatternFields,
- nonMetricsLoaded,
- overallStats,
- showEmptyFields,
- ]);
-
const wizardPanelWidth = '280px';
- const configs = useMemo(() => {
- let combinedConfigs = [...nonMetricConfigs, ...metricConfigs];
- if (visibleFieldTypes && visibleFieldTypes.length > 0) {
- combinedConfigs = combinedConfigs.filter(
- (config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1
- );
- }
- if (visibleFieldNames && visibleFieldNames.length > 0) {
- combinedConfigs = combinedConfigs.filter(
- (config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1
- );
- }
-
- return combinedConfigs;
- }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]);
-
const fieldsCountStats: TotalFieldsStats | undefined = useMemo(() => {
let _visibleFieldsCount = 0;
let _totalFieldsCount = 0;
@@ -923,7 +461,7 @@ export const IndexDataVisualizerView: FC = (dataVi
{overallStats?.totalCount !== undefined && (
@@ -953,12 +491,14 @@ export const IndexDataVisualizerView: FC = (dataVi
metricsStats={metricsStats}
/>
+
items={configs}
pageState={dataVisualizerListState}
updatePageState={setDataVisualizerListState}
getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
extendedColumns={extendedColumns}
+ loading={progress < 100}
/>
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx
index 7e86425c0a891..ee54683b08435 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx
@@ -29,7 +29,7 @@ export const DataVisualizerFieldTypeFilter: FC<{
{label}
{indexedFieldName && (
-
+
)}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
index f55114ca36d78..25ed13121fc34 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx
@@ -22,6 +22,7 @@ import { SearchQueryLanguage } from '../../types/combined_query';
import { useDataVisualizerKibana } from '../../../kibana_context';
import './_index.scss';
import { createMergedEsQuery } from '../../utils/saved_search_utils';
+import { OverallStats } from '../../types/overall_stats';
interface Props {
indexPattern: IndexPattern;
searchString: Query['query'];
@@ -29,7 +30,7 @@ interface Props {
searchQueryLanguage: SearchQueryLanguage;
samplerShardSize: number;
setSamplerShardSize(s: number): void;
- overallStats: any;
+ overallStats: OverallStats;
indexedFieldTypes: JobFieldType[];
setVisibleFieldTypes(q: string[]): void;
visibleFieldTypes: string[];
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts
deleted file mode 100644
index e0a2852a57b29..0000000000000
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts
+++ /dev/null
@@ -1,142 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-// Maximum number of examples to obtain for text type fields.
-import { CoreSetup } from 'kibana/public';
-import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import { i18n } from '@kbn/i18n';
-import { IndexPattern } from '../../../../../../../src/plugins/data/common';
-import { NON_AGGREGATABLE_FIELD_TYPES, OMIT_FIELDS } from '../../../../common/constants';
-import { FieldRequestConfig } from '../../../../common/types';
-import { getVisualizerFieldStats, getVisualizerOverallStats } from '../services/visualizer_stats';
-
-type IndexPatternTitle = string;
-type SavedSearchQuery = Record | null | undefined;
-
-const MAX_EXAMPLES_DEFAULT: number = 10;
-
-export class DataLoader {
- private _indexPattern: IndexPattern;
- private _runtimeMappings: estypes.MappingRuntimeFields;
- private _indexPatternTitle: IndexPatternTitle = '';
- private _maxExamples: number = MAX_EXAMPLES_DEFAULT;
- private _toastNotifications: CoreSetup['notifications']['toasts'];
-
- constructor(
- indexPattern: IndexPattern,
- toastNotifications: CoreSetup['notifications']['toasts']
- ) {
- this._indexPattern = indexPattern;
- this._runtimeMappings = this._indexPattern.getComputedFields()
- .runtimeFields as estypes.MappingRuntimeFields;
- this._indexPatternTitle = indexPattern.title;
- this._toastNotifications = toastNotifications;
- }
-
- async loadOverallData(
- query: string | SavedSearchQuery,
- samplerShardSize: number,
- earliest: number | undefined,
- latest: number | undefined
- ): Promise {
- const aggregatableFields: string[] = [];
- const nonAggregatableFields: string[] = [];
- this._indexPattern.fields.forEach((field) => {
- const fieldName = field.displayName !== undefined ? field.displayName : field.name;
- if (this.isDisplayField(fieldName) === true) {
- if (field.aggregatable === true && !NON_AGGREGATABLE_FIELD_TYPES.has(field.type)) {
- aggregatableFields.push(field.name);
- } else {
- nonAggregatableFields.push(field.name);
- }
- }
- });
-
- // Need to find:
- // 1. List of aggregatable fields that do exist in docs
- // 2. List of aggregatable fields that do not exist in docs
- // 3. List of non-aggregatable fields that do exist in docs.
- // 4. List of non-aggregatable fields that do not exist in docs.
- const stats = await getVisualizerOverallStats({
- indexPatternTitle: this._indexPatternTitle,
- query,
- timeFieldName: this._indexPattern.timeFieldName,
- samplerShardSize,
- earliest,
- latest,
- aggregatableFields,
- nonAggregatableFields,
- runtimeMappings: this._runtimeMappings,
- });
-
- return stats;
- }
-
- async loadFieldStats(
- query: string | SavedSearchQuery,
- samplerShardSize: number,
- earliest: number | undefined,
- latest: number | undefined,
- fields: FieldRequestConfig[],
- interval?: number
- ): Promise {
- const stats = await getVisualizerFieldStats({
- indexPatternTitle: this._indexPatternTitle,
- query,
- timeFieldName: this._indexPattern.timeFieldName,
- earliest,
- latest,
- samplerShardSize,
- interval,
- fields,
- maxExamples: this._maxExamples,
- runtimeMappings: this._runtimeMappings,
- });
-
- return stats;
- }
-
- displayError(err: any) {
- if (err.statusCode === 500) {
- this._toastNotifications.addError(err, {
- title: i18n.translate('xpack.dataVisualizer.index.dataLoader.internalServerErrorMessage', {
- defaultMessage:
- 'Error loading data in index {index}. {message}. ' +
- 'The request may have timed out. Try using a smaller sample size or narrowing the time range.',
- values: {
- index: this._indexPattern.title,
- message: err.error ?? err.message,
- },
- }),
- });
- } else {
- this._toastNotifications.addError(err, {
- title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage', {
- defaultMessage: 'Error loading data in index {index}. {message}.',
- values: {
- index: this._indexPattern.title,
- message: err.error ?? err.message,
- },
- }),
- });
- }
- }
-
- public set maxExamples(max: number) {
- this._maxExamples = max;
- }
-
- public get maxExamples(): number {
- return this._maxExamples;
- }
-
- // Returns whether the field with the specified name should be displayed,
- // as certain fields such as _id and _source should be omitted from the view.
- public isDisplayField(fieldName: string): boolean {
- return !OMIT_FIELDS.includes(fieldName);
- }
-}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx
index f59225b1c019f..0391d5ae5d5d5 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx
@@ -8,7 +8,7 @@
import { Observable, Subject } from 'rxjs';
import { CoreStart } from 'kibana/public';
import ReactDOM from 'react-dom';
-import React, { Suspense, useCallback, useState } from 'react';
+import React, { Suspense, useCallback, useEffect, useState } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
import { Filter } from '@kbn/es-query';
@@ -36,15 +36,15 @@ import {
} from '../../../common/components/stats_table';
import { FieldVisConfig } from '../../../common/components/stats_table/types';
import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view';
-import { DataVisualizerTableState } from '../../../../../common';
+import { DataVisualizerTableState, SavedSearchSavedObject } from '../../../../../common';
import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
-import { useDataVisualizerGridData } from './use_data_visualizer_grid_data';
+import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies];
-export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
+export interface DataVisualizerGridInput {
indexPattern: IndexPattern;
- savedSearch?: SavedSearch;
+ savedSearch?: SavedSearch | SavedSearchSavedObject | null;
query?: Query;
visibleFieldNames?: string[];
filters?: Filter[];
@@ -54,6 +54,7 @@ export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
*/
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
}
+export type DataVisualizerGridEmbeddableInput = EmbeddableInput & DataVisualizerGridInput;
export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput;
export type IDataVisualizerGridEmbeddable = typeof DataVisualizerGridEmbeddable;
@@ -79,8 +80,13 @@ export const EmbeddableWrapper = ({
},
[dataVisualizerListState, onOutputChange]
);
- const { configs, searchQueryLanguage, searchString, extendedColumns, loaded } =
+ const { configs, searchQueryLanguage, searchString, extendedColumns, progress, setLastRefresh } =
useDataVisualizerGridData(input, dataVisualizerListState);
+
+ useEffect(() => {
+ setLastRefresh(Date.now());
+ }, [input?.lastReloadRequestTime, setLastRefresh]);
+
const getItemIdToExpandedRowMap = useCallback(
function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap {
return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => {
@@ -101,13 +107,7 @@ export const EmbeddableWrapper = ({
[input, searchQueryLanguage, searchString]
);
- if (
- loaded &&
- (configs.length === 0 ||
- // FIXME: Configs might have a placeholder document count stats field
- // This will be removed in the future
- (configs.length === 1 && configs[0].fieldName === undefined))
- ) {
+ if (progress === 100 && configs.length === 0) {
return (
);
};
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
similarity index 52%
rename from x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts
rename to x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
index fc0fc7a2134b4..e6e7a96e0329f 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts
@@ -10,39 +10,54 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { merge } from 'rxjs';
import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types';
import { i18n } from '@kbn/i18n';
-import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
-import { useDataVisualizerKibana } from '../../../kibana_context';
-import { getEsQueryFromSavedSearch } from '../../utils/saved_search_utils';
-import { MetricFieldsStats } from '../../../common/components/stats_table/components/field_count_stats';
-import { DataLoader } from '../../data_loader/data_loader';
-import { useTimefilter } from '../../hooks/use_time_filter';
-import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service';
-import { TimeBuckets } from '../../services/time_buckets';
+import { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state';
+import { useDataVisualizerKibana } from '../../kibana_context';
+import { getEsQueryFromSavedSearch } from '../utils/saved_search_utils';
+import { MetricFieldsStats } from '../../common/components/stats_table/components/field_count_stats';
+import { useTimefilter } from './use_time_filter';
+import { dataVisualizerRefresh$ } from '../services/timefilter_refresh_service';
+import { TimeBuckets } from '../../../../common/services/time_buckets';
import {
DataViewField,
KBN_FIELD_TYPES,
UI_SETTINGS,
-} from '../../../../../../../../src/plugins/data/common';
-import { extractErrorProperties } from '../../utils/error_utils';
-import { FieldVisConfig } from '../../../common/components/stats_table/types';
-import { FieldRequestConfig, JOB_FIELD_TYPES } from '../../../../../common';
-import { kbnTypeToJobType } from '../../../common/util/field_types_utils';
-import { getActions } from '../../../common/components/field_data_row/action_menu';
-import { DataVisualizerGridEmbeddableInput } from './grid_embeddable';
-import { getDefaultPageState } from '../../components/index_data_visualizer_view/index_data_visualizer_view';
+} from '../../../../../../../src/plugins/data/common';
+import { FieldVisConfig } from '../../common/components/stats_table/types';
+import {
+ FieldRequestConfig,
+ JOB_FIELD_TYPES,
+ JobFieldType,
+ NON_AGGREGATABLE_FIELD_TYPES,
+ OMIT_FIELDS,
+} from '../../../../common';
+import { kbnTypeToJobType } from '../../common/util/field_types_utils';
+import { getActions } from '../../common/components/field_data_row/action_menu';
+import { DataVisualizerGridInput } from '../embeddables/grid_embeddable/grid_embeddable';
+import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view';
+import { useFieldStatsSearchStrategy } from './use_field_stats';
+import { useOverallStats } from './use_overall_stats';
+import { OverallStatsSearchStrategyParams } from '../../../../common/types/field_stats';
+import { Dictionary } from '../../common/util/url_state';
+import { AggregatableField, NonAggregatableField } from '../types/overall_stats';
const defaults = getDefaultPageState();
+function isDisplayField(fieldName: string): boolean {
+ return !OMIT_FIELDS.includes(fieldName);
+}
+
export const useDataVisualizerGridData = (
- input: DataVisualizerGridEmbeddableInput,
- dataVisualizerListState: Required
+ input: DataVisualizerGridInput,
+ dataVisualizerListState: Required,
+ onUpdate?: (params: Dictionary) => void
) => {
const { services } = useDataVisualizerKibana();
- const { notifications, uiSettings } = services;
- const { toasts } = notifications;
+ const { uiSettings, data } = services;
const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState;
+ const dataVisualizerListStateRef = useRef(dataVisualizerListState);
const [lastRefresh, setLastRefresh] = useState(0);
+ const [searchSessionId, setSearchSessionId] = useState();
const {
currentSavedSearch,
@@ -61,6 +76,7 @@ export const useDataVisualizerGridData = (
[input]
);
+ /** Prepare required params to pass to search strategy **/
const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
const searchData = getEsQueryFromSavedSearch({
indexPattern: currentIndexPattern,
@@ -68,9 +84,13 @@ export const useDataVisualizerGridData = (
savedSearch: currentSavedSearch,
query: currentQuery,
filters: currentFilters,
+ filterManager: data.query.filterManager,
});
if (searchData === undefined || dataVisualizerListState.searchString !== '') {
+ if (dataVisualizerListState.filters) {
+ data.query.filterManager.setFilters(dataVisualizerListState.filters);
+ }
return {
searchQuery: dataVisualizerListState.searchQuery,
searchString: dataVisualizerListState.searchString,
@@ -85,16 +105,40 @@ export const useDataVisualizerGridData = (
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
- currentSavedSearch,
- currentIndexPattern,
- dataVisualizerListState,
- currentQuery,
- currentFilters,
+ currentSavedSearch?.id,
+ currentIndexPattern.id,
+ dataVisualizerListState.searchString,
+ dataVisualizerListState.searchQueryLanguage,
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ JSON.stringify({
+ searchQuery: dataVisualizerListState.searchQuery,
+ currentQuery,
+ currentFilters,
+ }),
+ lastRefresh,
]);
- const [overallStats, setOverallStats] = useState(defaults.overallStats);
+ useEffect(() => {
+ const currentSearchSessionId = data.search?.session?.getSessionId();
+ if (currentSearchSessionId !== undefined) {
+ setSearchSessionId(currentSearchSessionId);
+ }
+ }, [data]);
+
+ const _timeBuckets = useMemo(() => {
+ return new TimeBuckets({
+ [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
+ [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
+ dateFormat: uiSettings.get('dateFormat'),
+ 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
+ });
+ }, [uiSettings]);
+
+ const timefilter = useTimefilter({
+ timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined,
+ autoRefreshSelector: true,
+ });
- const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats);
const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs);
const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded);
const [metricsStats, setMetricsStats] = useState();
@@ -102,21 +146,134 @@ export const useDataVisualizerGridData = (
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
- const dataLoader = useMemo(
- () => new DataLoader(currentIndexPattern, toasts),
- [currentIndexPattern, toasts]
+ /** Search strategy **/
+ const fieldStatsRequest: OverallStatsSearchStrategyParams | undefined = useMemo(
+ () => {
+ // Obtain the interval to use for date histogram aggregations
+ // (such as the document count chart). Aim for 75 bars.
+ const buckets = _timeBuckets;
+
+ const tf = timefilter;
+
+ if (!buckets || !tf || !currentIndexPattern) return;
+
+ const activeBounds = tf.getActiveBounds();
+
+ let earliest: number | undefined;
+ let latest: number | undefined;
+ if (activeBounds !== undefined && currentIndexPattern.timeFieldName !== undefined) {
+ earliest = activeBounds.min?.valueOf();
+ latest = activeBounds.max?.valueOf();
+ }
+
+ const bounds = tf.getActiveBounds();
+ const BAR_TARGET = 75;
+ buckets.setInterval('auto');
+
+ if (bounds) {
+ buckets.setBounds(bounds);
+ buckets.setBarTarget(BAR_TARGET);
+ }
+
+ const aggInterval = buckets.getInterval();
+
+ const aggregatableFields: string[] = [];
+ const nonAggregatableFields: string[] = [];
+ currentIndexPattern.fields.forEach((field) => {
+ const fieldName = field.displayName !== undefined ? field.displayName : field.name;
+ if (!OMIT_FIELDS.includes(fieldName)) {
+ if (field.aggregatable === true && !NON_AGGREGATABLE_FIELD_TYPES.has(field.type)) {
+ aggregatableFields.push(field.name);
+ } else {
+ nonAggregatableFields.push(field.name);
+ }
+ }
+ });
+ return {
+ earliest,
+ latest,
+ aggInterval,
+ intervalMs: aggInterval?.asMilliseconds(),
+ searchQuery,
+ samplerShardSize,
+ sessionId: searchSessionId,
+ index: currentIndexPattern.title,
+ timeFieldName: currentIndexPattern.timeFieldName,
+ runtimeFieldMap: currentIndexPattern.getComputedFields().runtimeFields,
+ aggregatableFields,
+ nonAggregatableFields,
+ };
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [
+ _timeBuckets,
+ timefilter,
+ currentIndexPattern.id,
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ JSON.stringify(searchQuery),
+ samplerShardSize,
+ searchSessionId,
+ lastRefresh,
+ ]
+ );
+
+ const { overallStats, progress: overallStatsProgress } = useOverallStats(
+ fieldStatsRequest,
+ lastRefresh
);
- const timefilter = useTimefilter({
- timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined,
- autoRefreshSelector: true,
- });
+ const configsWithoutStats = useMemo(() => {
+ if (overallStatsProgress.loaded < 100) return;
+ const existMetricFields = metricConfigs
+ .map((config) => {
+ if (config.existsInDocs === false) return;
+ return {
+ fieldName: config.fieldName,
+ type: config.type,
+ cardinality: config.stats?.cardinality ?? 0,
+ };
+ })
+ .filter((c) => c !== undefined) as FieldRequestConfig[];
+
+ // Pass the field name, type and cardinality in the request.
+ // Top values will be obtained on a sample if cardinality > 100000.
+ const existNonMetricFields: FieldRequestConfig[] = nonMetricConfigs
+ .map((config) => {
+ if (config.existsInDocs === false) return;
+ return {
+ fieldName: config.fieldName,
+ type: config.type,
+ cardinality: config.stats?.cardinality ?? 0,
+ };
+ })
+ .filter((c) => c !== undefined) as FieldRequestConfig[];
+
+ return { metricConfigs: existMetricFields, nonMetricConfigs: existNonMetricFields };
+ }, [metricConfigs, nonMetricConfigs, overallStatsProgress.loaded]);
+
+ const strategyResponse = useFieldStatsSearchStrategy(
+ fieldStatsRequest,
+ configsWithoutStats,
+ dataVisualizerListStateRef.current
+ );
+
+ const combinedProgress = useMemo(
+ () => overallStatsProgress.loaded * 0.2 + strategyResponse.progress.loaded * 0.8,
+ [overallStatsProgress.loaded, strategyResponse.progress.loaded]
+ );
useEffect(() => {
const timeUpdateSubscription = merge(
timefilter.getTimeUpdate$(),
+ timefilter.getAutoRefreshFetch$(),
dataVisualizerRefresh$
).subscribe(() => {
+ if (onUpdate) {
+ onUpdate({
+ time: timefilter.getTime(),
+ refreshInterval: timefilter.getRefreshInterval(),
+ });
+ }
setLastRefresh(Date.now());
});
return () => {
@@ -124,65 +281,21 @@ export const useDataVisualizerGridData = (
};
});
- const getTimeBuckets = useCallback(() => {
- return new TimeBuckets({
- [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
- [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
- dateFormat: uiSettings.get('dateFormat'),
- 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
- });
- }, [uiSettings]);
-
const indexPatternFields: DataViewField[] = useMemo(
() => currentIndexPattern.fields,
[currentIndexPattern]
);
- async function loadOverallStats() {
- const tf = timefilter as any;
- let earliest;
- let latest;
-
- const activeBounds = tf.getActiveBounds();
-
- if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) {
- return;
- }
-
- if (currentIndexPattern.timeFieldName !== undefined) {
- earliest = activeBounds.min.valueOf();
- latest = activeBounds.max.valueOf();
- }
-
- try {
- const allStats = await dataLoader.loadOverallData(
- searchQuery,
- samplerShardSize,
- earliest,
- latest
- );
- // Because load overall stats perform queries in batches
- // there could be multiple errors
- if (Array.isArray(allStats.errors) && allStats.errors.length > 0) {
- allStats.errors.forEach((err: any) => {
- dataLoader.displayError(extractErrorProperties(err));
- });
- }
- setOverallStats(allStats);
- } catch (err) {
- dataLoader.displayError(err.body ?? err);
- }
- }
-
const createMetricCards = useCallback(() => {
const configs: FieldVisConfig[] = [];
- const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || [];
+ const aggregatableExistsFields: AggregatableField[] =
+ overallStats.aggregatableExistsFields || [];
const allMetricFields = indexPatternFields.filter((f) => {
return (
f.type === KBN_FIELD_TYPES.NUMBER &&
f.displayName !== undefined &&
- dataLoader.isDisplayField(f.displayName) === true
+ isDisplayField(f.displayName) === true
);
});
const metricExistsFields = allMetricFields.filter((f) => {
@@ -191,22 +304,12 @@ export const useDataVisualizerGridData = (
});
});
- // Add a config for 'document count', identified by no field name if indexpattern is time based.
- if (currentIndexPattern.timeFieldName !== undefined) {
- configs.push({
- type: JOB_FIELD_TYPES.NUMBER,
- existsInDocs: true,
- loading: true,
- aggregatable: true,
- });
- }
-
if (metricsLoaded === false) {
setMetricsLoaded(true);
return;
}
- let aggregatableFields: any[] = overallStats.aggregatableExistsFields;
+ let aggregatableFields: AggregatableField[] = overallStats.aggregatableExistsFields;
if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) {
aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields);
}
@@ -218,9 +321,10 @@ export const useDataVisualizerGridData = (
const fieldData = aggregatableFields.find((f) => {
return f.fieldName === field.spec.name;
});
+ if (!fieldData) return;
const metricConfig: FieldVisConfig = {
- ...(fieldData ? fieldData : {}),
+ ...fieldData,
fieldFormat: currentIndexPattern.getFormatterForField(field),
type: JOB_FIELD_TYPES.NUMBER,
loading: true,
@@ -239,29 +343,24 @@ export const useDataVisualizerGridData = (
visibleMetricsCount: metricFieldsToShow.length,
});
setMetricConfigs(configs);
- }, [
- currentIndexPattern,
- dataLoader,
- indexPatternFields,
- metricsLoaded,
- overallStats,
- showEmptyFields,
- ]);
+ }, [currentIndexPattern, indexPatternFields, metricsLoaded, overallStats, showEmptyFields]);
const createNonMetricCards = useCallback(() => {
const allNonMetricFields = indexPatternFields.filter((f) => {
return (
f.type !== KBN_FIELD_TYPES.NUMBER &&
f.displayName !== undefined &&
- dataLoader.isDisplayField(f.displayName) === true
+ isDisplayField(f.displayName) === true
);
});
// Obtain the list of all non-metric fields which appear in documents
// (aggregatable or not aggregatable).
- const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields.
- let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats.
- const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || [];
- const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || [];
+ const populatedNonMetricFields: DataViewField[] = []; // Kibana index pattern non metric fields.
+ let nonMetricFieldData: Array = []; // Basic non metric field data loaded from requesting overall stats.
+ const aggregatableExistsFields: AggregatableField[] =
+ overallStats.aggregatableExistsFields || [];
+ const nonAggregatableExistsFields: NonAggregatableField[] =
+ overallStats.nonAggregatableExistsFields || [];
allNonMetricFields.forEach((f) => {
const checkAggregatableField = aggregatableExistsFields.find(
@@ -303,12 +402,11 @@ export const useDataVisualizerGridData = (
nonMetricFieldsToShow.forEach((field) => {
const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
- const nonMetricConfig = {
+ const nonMetricConfig: Partial = {
...(fieldData ? fieldData : {}),
fieldFormat: currentIndexPattern.getFormatterForField(field),
aggregatable: field.aggregatable,
- scripted: field.scripted,
- loading: fieldData?.existsInDocs,
+ loading: fieldData?.existsInDocs ?? true,
deletable: field.runtimeField !== undefined,
};
@@ -320,7 +418,7 @@ export const useDataVisualizerGridData = (
} else {
// Add a flag to indicate that this is one of the 'other' Kibana
// field types that do not yet have a specific card type.
- nonMetricConfig.type = field.type;
+ nonMetricConfig.type = field.type as JobFieldType;
nonMetricConfig.isUnsupportedType = true;
}
@@ -328,171 +426,11 @@ export const useDataVisualizerGridData = (
nonMetricConfig.displayName = field.displayName;
}
- configs.push(nonMetricConfig);
+ configs.push(nonMetricConfig as FieldVisConfig);
});
setNonMetricConfigs(configs);
- }, [
- currentIndexPattern,
- dataLoader,
- indexPatternFields,
- nonMetricsLoaded,
- overallStats,
- showEmptyFields,
- ]);
-
- async function loadMetricFieldStats() {
- // Only request data for fields that exist in documents.
- if (metricConfigs.length === 0) {
- return;
- }
-
- const configsToLoad = metricConfigs.filter(
- (config) => config.existsInDocs === true && config.loading === true
- );
- if (configsToLoad.length === 0) {
- return;
- }
-
- // Pass the field name, type and cardinality in the request.
- // Top values will be obtained on a sample if cardinality > 100000.
- const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => {
- const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 };
- if (config.stats !== undefined && config.stats.cardinality !== undefined) {
- props.cardinality = config.stats.cardinality;
- }
- return props;
- });
-
- // Obtain the interval to use for date histogram aggregations
- // (such as the document count chart). Aim for 75 bars.
- const buckets = getTimeBuckets();
-
- const tf = timefilter as any;
- let earliest: number | undefined;
- let latest: number | undefined;
- if (currentIndexPattern.timeFieldName !== undefined) {
- earliest = tf.getActiveBounds().min.valueOf();
- latest = tf.getActiveBounds().max.valueOf();
- }
-
- const bounds = tf.getActiveBounds();
- const BAR_TARGET = 75;
- buckets.setInterval('auto');
- buckets.setBounds(bounds);
- buckets.setBarTarget(BAR_TARGET);
- const aggInterval = buckets.getInterval();
-
- try {
- const metricFieldStats = await dataLoader.loadFieldStats(
- searchQuery,
- samplerShardSize,
- earliest,
- latest,
- existMetricFields,
- aggInterval.asMilliseconds()
- );
-
- // Add the metric stats to the existing stats in the corresponding config.
- const configs: FieldVisConfig[] = [];
- metricConfigs.forEach((config) => {
- const configWithStats = { ...config };
- if (config.fieldName !== undefined) {
- configWithStats.stats = {
- ...configWithStats.stats,
- ...metricFieldStats.find(
- (fieldStats: any) => fieldStats.fieldName === config.fieldName
- ),
- };
- configWithStats.loading = false;
- configs.push(configWithStats);
- } else {
- // Document count card.
- configWithStats.stats = metricFieldStats.find(
- (fieldStats: any) => fieldStats.fieldName === undefined
- );
-
- if (configWithStats.stats !== undefined) {
- // Add earliest / latest of timefilter for setting x axis domain.
- configWithStats.stats.timeRangeEarliest = earliest;
- configWithStats.stats.timeRangeLatest = latest;
- }
- setDocumentCountStats(configWithStats);
- }
- });
-
- setMetricConfigs(configs);
- } catch (err) {
- dataLoader.displayError(err);
- }
- }
-
- async function loadNonMetricFieldStats() {
- // Only request data for fields that exist in documents.
- if (nonMetricConfigs.length === 0) {
- return;
- }
-
- const configsToLoad = nonMetricConfigs.filter(
- (config) => config.existsInDocs === true && config.loading === true
- );
- if (configsToLoad.length === 0) {
- return;
- }
-
- // Pass the field name, type and cardinality in the request.
- // Top values will be obtained on a sample if cardinality > 100000.
- const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => {
- const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 };
- if (config.stats !== undefined && config.stats.cardinality !== undefined) {
- props.cardinality = config.stats.cardinality;
- }
- return props;
- });
-
- const tf = timefilter as any;
- let earliest;
- let latest;
- if (currentIndexPattern.timeFieldName !== undefined) {
- earliest = tf.getActiveBounds().min.valueOf();
- latest = tf.getActiveBounds().max.valueOf();
- }
-
- try {
- const nonMetricFieldStats = await dataLoader.loadFieldStats(
- searchQuery,
- samplerShardSize,
- earliest,
- latest,
- existNonMetricFields
- );
-
- // Add the field stats to the existing stats in the corresponding config.
- const configs: FieldVisConfig[] = [];
- nonMetricConfigs.forEach((config) => {
- const configWithStats = { ...config };
- if (config.fieldName !== undefined) {
- configWithStats.stats = {
- ...configWithStats.stats,
- ...nonMetricFieldStats.find(
- (fieldStats: any) => fieldStats.fieldName === config.fieldName
- ),
- };
- }
- configWithStats.loading = false;
- configs.push(configWithStats);
- });
-
- setNonMetricConfigs(configs);
- } catch (err) {
- dataLoader.displayError(err);
- }
- }
-
- useEffect(() => {
- loadOverallStats();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchQuery, samplerShardSize, lastRefresh]);
+ }, [currentIndexPattern, indexPatternFields, nonMetricsLoaded, overallStats, showEmptyFields]);
useEffect(() => {
createMetricCards();
@@ -500,27 +438,8 @@ export const useDataVisualizerGridData = (
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overallStats, showEmptyFields]);
- useEffect(() => {
- loadMetricFieldStats();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [metricConfigs]);
-
- useEffect(() => {
- loadNonMetricFieldStats();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [nonMetricConfigs]);
-
- useEffect(() => {
- createMetricCards();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [metricsLoaded]);
-
- useEffect(() => {
- createNonMetricCards();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [nonMetricsLoaded]);
-
const configs = useMemo(() => {
+ const fieldStats = strategyResponse.fieldStats;
let combinedConfigs = [...nonMetricConfigs, ...metricConfigs];
if (visibleFieldTypes && visibleFieldTypes.length > 0) {
combinedConfigs = combinedConfigs.filter(
@@ -533,8 +452,27 @@ export const useDataVisualizerGridData = (
);
}
+ if (fieldStats) {
+ combinedConfigs = combinedConfigs.map((c) => {
+ const loadedFullStats = fieldStats.get(c.fieldName) ?? {};
+ return loadedFullStats
+ ? {
+ ...c,
+ loading: false,
+ stats: { ...c.stats, ...loadedFullStats },
+ }
+ : c;
+ });
+ }
+
return combinedConfigs;
- }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]);
+ }, [
+ nonMetricConfigs,
+ metricConfigs,
+ visibleFieldTypes,
+ visibleFieldNames,
+ strategyResponse.fieldStats,
+ ]);
// Some actions open up fly-out or popup
// This variable is used to keep track of them and clean up when unmounting
@@ -575,13 +513,16 @@ export const useDataVisualizerGridData = (
}, [input.indexPattern, services, searchQueryLanguage, searchString]);
return {
+ progress: combinedProgress,
configs,
searchQueryLanguage,
searchString,
searchQuery,
extendedColumns,
- documentCountStats,
+ documentCountStats: overallStats.documentCountStats,
metricsStats,
- loaded: metricsLoaded && nonMetricsLoaded,
+ overallStats,
+ timefilter,
+ setLastRefresh,
};
};
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts
new file mode 100644
index 0000000000000..64654d56db05b
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts
@@ -0,0 +1,287 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
+import { combineLatest, Observable, Subject, Subscription } from 'rxjs';
+import { i18n } from '@kbn/i18n';
+import { last, cloneDeep } from 'lodash';
+import { switchMap } from 'rxjs/operators';
+import type {
+ DataStatsFetchProgress,
+ FieldStatsSearchStrategyReturnBase,
+ OverallStatsSearchStrategyParams,
+ FieldStatsCommonRequestParams,
+ Field,
+} from '../../../../common/types/field_stats';
+import { useDataVisualizerKibana } from '../../kibana_context';
+import type { FieldRequestConfig } from '../../../../common';
+import type { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state';
+import {
+ buildBaseFilterCriteria,
+ getSafeAggregationName,
+} from '../../../../common/utils/query_utils';
+import type { FieldStats, FieldStatsError } from '../../../../common/types/field_stats';
+import { getInitialProgress, getReducer } from '../progress_utils';
+import { MAX_EXAMPLES_DEFAULT } from '../search_strategy/requests/constants';
+import type { ISearchOptions } from '../../../../../../../src/plugins/data/common';
+import { getFieldsStats } from '../search_strategy/requests/get_fields_stats';
+interface FieldStatsParams {
+ metricConfigs: FieldRequestConfig[];
+ nonMetricConfigs: FieldRequestConfig[];
+}
+
+const createBatchedRequests = (fields: Field[], maxBatchSize = 10) => {
+ // Batch up fields by type, getting stats for multiple fields at a time.
+ const batches: Field[][] = [];
+ const batchedFields: { [key: string]: Field[][] } = {};
+
+ fields.forEach((field) => {
+ const fieldType = field.type;
+ if (batchedFields[fieldType] === undefined) {
+ batchedFields[fieldType] = [[]];
+ }
+ let lastArray: Field[] = last(batchedFields[fieldType]) as Field[];
+ if (lastArray.length === maxBatchSize) {
+ lastArray = [];
+ batchedFields[fieldType].push(lastArray);
+ }
+ lastArray.push(field);
+ });
+
+ Object.values(batchedFields).forEach((lists) => {
+ batches.push(...lists);
+ });
+ return batches;
+};
+
+export function useFieldStatsSearchStrategy(
+ searchStrategyParams: OverallStatsSearchStrategyParams | undefined,
+ fieldStatsParams: FieldStatsParams | undefined,
+ initialDataVisualizerListState: DataVisualizerIndexBasedAppState
+): FieldStatsSearchStrategyReturnBase {
+ const {
+ services: {
+ data,
+ notifications: { toasts },
+ },
+ } = useDataVisualizerKibana();
+
+ const [fieldStats, setFieldStats] = useState