diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts
index b30fcf972eda55..32704d95423f71 100644
--- a/src/plugins/discover/common/index.ts
+++ b/src/plugins/discover/common/index.ts
@@ -19,5 +19,6 @@ export const DOC_TABLE_LEGACY = 'doc_table:legacy';
export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch';
export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource';
export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed';
+export const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics';
export const SHOW_MULTIFIELDS = 'discover:showMultiFields';
export const SEARCH_EMBEDDABLE_TYPE = 'search';
diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx
index 15f6e619c86508..f7a383be76b9e0 100644
--- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx
@@ -19,6 +19,7 @@ import { discoverServiceMock } from '../../../../../__mocks__/services';
import { FetchStatus } from '../../../../types';
import { Chart } from './point_series';
import { DiscoverChart } from './discover_chart';
+import { VIEW_MODE } from '../view_mode_toggle';
setHeaderActionMenuMounter(jest.fn());
@@ -94,6 +95,8 @@ function getProps(timefield?: string) {
state: { columns: [] },
stateContainer: {} as GetStateReturn,
timefield,
+ viewMode: VIEW_MODE.DOCUMENT_LEVEL,
+ setDiscoverViewMode: jest.fn(),
};
}
diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx
index b6509356c8c417..166c2272a00f42 100644
--- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx
@@ -23,6 +23,8 @@ import { DiscoverHistogram } from './histogram';
import { DataCharts$, DataTotalHits$ } from '../../services/use_saved_search';
import { DiscoverServices } from '../../../../../build_services';
import { useChartPanels } from './use_chart_panels';
+import { VIEW_MODE, DocumentViewModeToggle } from '../view_mode_toggle';
+import { SHOW_FIELD_STATISTICS } from '../../../../../../common';
const DiscoverHistogramMemoized = memo(DiscoverHistogram);
export const CHART_HIDDEN_KEY = 'discover:chartHidden';
@@ -36,6 +38,8 @@ export function DiscoverChart({
state,
stateContainer,
timefield,
+ viewMode,
+ setDiscoverViewMode,
}: {
resetSavedSearch: () => void;
savedSearch: SavedSearch;
@@ -45,8 +49,11 @@ export function DiscoverChart({
state: AppState;
stateContainer: GetStateReturn;
timefield?: string;
+ viewMode: VIEW_MODE;
+ setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
}) {
const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false);
+ const showViewModeToggle = services.uiSettings.get(SHOW_FIELD_STATISTICS) ?? false;
const { data, storage } = services;
@@ -108,6 +115,16 @@ export function DiscoverChart({
onResetQuery={resetSavedSearch}
/>
+
+ {showViewModeToggle && (
+
+
+
+ )}
+
{timefield && (
(undefined);
const [inspectorSession, setInspectorSession] = useState(undefined);
+
+ const viewMode = useMemo(() => {
+ if (uiSettings.get(SHOW_FIELD_STATISTICS) !== true) return VIEW_MODE.DOCUMENT_LEVEL;
+ return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL;
+ }, [uiSettings, state.viewMode]);
+
+ const setDiscoverViewMode = useCallback(
+ (mode: VIEW_MODE) => {
+ stateContainer.setAppState({ viewMode: mode });
+ },
+ [stateContainer]
+ );
+
const fetchCounter = useRef(0);
const dataState: DataMainMsg = useDataState(main$);
@@ -213,6 +229,7 @@ export function DiscoverLayout({
trackUiMetric={trackUiMetric}
useNewFieldsApi={useNewFieldsApi}
onEditRuntimeField={onEditRuntimeField}
+ viewMode={viewMode}
/>
@@ -279,22 +296,36 @@ export function DiscoverLayout({
services={services}
stateContainer={stateContainer}
timefield={timeField}
+ viewMode={viewMode}
+ setDiscoverViewMode={setDiscoverViewMode}
/>
-
-
+ {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
+
+ ) : (
+
+ )}
)}
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
index f2919f6a9bfd42..89e7b501876307 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
@@ -19,6 +19,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
+ EuiHorizontalRule,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { UiCounterMetricType } from '@kbn/analytics';
@@ -251,6 +252,11 @@ export interface DiscoverFieldProps {
* @param fieldName name of the field to delete
*/
onDeleteField?: (fieldName: string) => void;
+
+ /**
+ * Optionally show or hide field stats in the popover
+ */
+ showFieldStats?: boolean;
}
function DiscoverFieldComponent({
@@ -266,6 +272,7 @@ function DiscoverFieldComponent({
multiFields,
onEditField,
onDeleteField,
+ showFieldStats,
}: DiscoverFieldProps) {
const [infoIsOpen, setOpen] = useState(false);
@@ -362,15 +369,27 @@ function DiscoverFieldComponent({
const details = getDetails(field);
return (
<>
-
+ {showFieldStats && (
+ <>
+
+
+ {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
+ defaultMessage: 'Top 5 values',
+ })}
+
+
+
+ >
+ )}
+
{multiFields && (
<>
-
+ {showFieldStats && }
>
)}
+ {(showFieldStats || multiFields) && }
);
};
-
return (
{popoverTitle}
-
-
- {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
- defaultMessage: 'Top 5 values',
- })}
-
-
{infoIsOpen && renderPopover()}
);
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx
index baf740531e6bfe..e974a67aef60d0 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_visualize.tsx
@@ -7,7 +7,7 @@
*/
import React, { useEffect, useState } from 'react';
-import { EuiButton, EuiPopoverFooter } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common';
@@ -46,21 +46,19 @@ export const DiscoverFieldVisualize: React.FC = React.memo(
};
return (
-
- {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
-
-
-
-
+ // eslint-disable-next-line @elastic/eui/href-or-on-click
+
+
+
);
}
);
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx
index a550dbd59b9fa7..03616c136df3ed 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx
@@ -22,6 +22,7 @@ import { DiscoverSidebarComponent as DiscoverSidebar } from './discover_sidebar'
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
import { discoverServiceMock as mockDiscoverServices } from '../../../../../__mocks__/services';
import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs';
+import { VIEW_MODE } from '../view_mode_toggle';
jest.mock('../../../../../kibana_services', () => ({
getServices: () => mockDiscoverServices,
@@ -65,6 +66,7 @@ function getCompProps(): DiscoverSidebarProps {
setFieldFilter: jest.fn(),
onEditRuntimeField: jest.fn(),
editField: jest.fn(),
+ viewMode: VIEW_MODE.DOCUMENT_LEVEL,
};
}
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
index 0bd8c59b90c018..d13860eab0d242 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
@@ -40,6 +40,7 @@ import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
+import { VIEW_MODE } from '../view_mode_toggle';
/**
* Default number of available fields displayed and added on scroll
@@ -77,6 +78,10 @@ export interface DiscoverSidebarProps extends Omit(null);
@@ -205,6 +211,8 @@ export function DiscoverSidebarComponent({
return result;
}, [fields]);
+ const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]);
+
const calculateMultiFields = () => {
if (!useNewFieldsApi || !fields) {
return undefined;
@@ -407,6 +415,7 @@ export function DiscoverSidebarComponent({
multiFields={multiFields?.get(field.name)}
onEditField={canEditIndexPatternField ? editField : undefined}
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
+ showFieldStats={showFieldStats}
/>
);
@@ -466,6 +475,7 @@ export function DiscoverSidebarComponent({
multiFields={multiFields?.get(field.name)}
onEditField={canEditIndexPatternField ? editField : undefined}
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
+ showFieldStats={showFieldStats}
/>
);
@@ -494,6 +504,7 @@ export function DiscoverSidebarComponent({
multiFields={multiFields?.get(field.name)}
onEditField={canEditIndexPatternField ? editField : undefined}
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
+ showFieldStats={showFieldStats}
/>
);
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
index ded7897d2a9e5a..4e4fed8c65bf7e 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
@@ -26,6 +26,7 @@ import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
import { FetchStatus } from '../../../../types';
import { DataDocuments$ } from '../../services/use_saved_search';
import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs';
+import { VIEW_MODE } from '../view_mode_toggle';
const mockServices = {
history: () => ({
@@ -103,6 +104,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
state: {},
trackUiMetric: jest.fn(),
onEditRuntimeField: jest.fn(),
+ viewMode: VIEW_MODE.DOCUMENT_LEVEL,
};
}
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
index 90357b73c68815..368a2b2e92d342 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
@@ -37,6 +37,7 @@ import { AppState } from '../../services/discover_state';
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
import { DataDocuments$ } from '../../services/use_saved_search';
import { calcFieldCounts } from '../../utils/calc_field_counts';
+import { VIEW_MODE } from '../view_mode_toggle';
export interface DiscoverSidebarResponsiveProps {
/**
@@ -106,6 +107,10 @@ export interface DiscoverSidebarResponsiveProps {
* callback to execute on edit runtime field
*/
onEditRuntimeField: () => void;
+ /**
+ * Discover view mode
+ */
+ viewMode: VIEW_MODE;
}
/**
diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
index 44d2999947f41d..653e878ad01bb5 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
@@ -16,6 +16,7 @@ import { SavedSearch } from '../../../../../saved_searches';
import { onSaveSearch } from './on_save_search';
import { GetStateReturn } from '../../services/discover_state';
import { openOptionsPopover } from './open_options_popover';
+import type { TopNavMenuData } from '../../../../../../../navigation/public';
/**
* Helper function to build the top nav links
@@ -38,7 +39,7 @@ export const getTopNavLinks = ({
onOpenInspector: () => void;
searchSource: ISearchSource;
onOpenSavedSearch: (id: string) => void;
-}) => {
+}): TopNavMenuData[] => {
const options = {
id: 'options',
label: i18n.translate('discover.localMenu.localMenu.optionsTitle', {
diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss
new file mode 100644
index 00000000000000..a76c3453de32a4
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_index.scss
@@ -0,0 +1 @@
+@import 'view_mode_toggle';
diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss
new file mode 100644
index 00000000000000..1009ab0511957e
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/_view_mode_toggle.scss
@@ -0,0 +1,12 @@
+.dscViewModeToggle {
+ padding-right: $euiSize;
+}
+
+.fieldStatsButton {
+ display: flex;
+ align-items: center;
+}
+
+.fieldStatsBetaBadge {
+ margin-left: $euiSizeXS;
+}
diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/constants.ts b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/constants.ts
new file mode 100644
index 00000000000000..d03c0710d12b30
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/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.
+ */
+
+export enum VIEW_MODE {
+ DOCUMENT_LEVEL = 'documents',
+ AGGREGATED_LEVEL = 'aggregated',
+}
diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts
new file mode 100644
index 00000000000000..95b76f5879d197
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { DocumentViewModeToggle } from './view_mode_toggle';
+export { VIEW_MODE } from './constants';
diff --git a/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx
new file mode 100644
index 00000000000000..3aa24c05e98d43
--- /dev/null
+++ b/src/plugins/discover/public/application/apps/main/components/view_mode_toggle/view_mode_toggle.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiButtonGroup, EuiBetaBadge } from '@elastic/eui';
+import React, { useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { VIEW_MODE } from './constants';
+import './_index.scss';
+
+export const DocumentViewModeToggle = ({
+ viewMode,
+ setDiscoverViewMode,
+}: {
+ viewMode: VIEW_MODE;
+ setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
+}) => {
+ const toggleButtons = useMemo(
+ () => [
+ {
+ id: VIEW_MODE.DOCUMENT_LEVEL,
+ label: i18n.translate('discover.viewModes.document.label', {
+ defaultMessage: 'Documents',
+ }),
+ 'data-test-subj': 'dscViewModeDocumentButton',
+ },
+ {
+ id: VIEW_MODE.AGGREGATED_LEVEL,
+ label: (
+
+
+
+
+ ),
+ },
+ ],
+ []
+ );
+
+ return (
+ setDiscoverViewMode(id as VIEW_MODE)}
+ data-test-subj={'dscViewModeToggle'}
+ />
+ );
+};
diff --git a/src/plugins/discover/public/application/apps/main/services/discover_state.ts b/src/plugins/discover/public/application/apps/main/services/discover_state.ts
index 16eb622c4a7c40..9a61fdc996e3b7 100644
--- a/src/plugins/discover/public/application/apps/main/services/discover_state.ts
+++ b/src/plugins/discover/public/application/apps/main/services/discover_state.ts
@@ -35,6 +35,7 @@ import { DiscoverGridSettings } from '../../../components/discover_grid/types';
import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../../../url_generator';
import { SavedSearch } from '../../../../saved_searches';
import { handleSourceColumnState } from '../../../helpers/state_helpers';
+import { VIEW_MODE } from '../components/view_mode_toggle';
export interface AppState {
/**
@@ -73,6 +74,14 @@ export interface AppState {
* id of the used saved query
*/
savedQuery?: string;
+ /**
+ * Table view: Documents vs Field Statistics
+ */
+ viewMode?: VIEW_MODE;
+ /**
+ * Hide mini distribution/preview charts when in Field Statistics mode
+ */
+ hideAggregatedPreview?: boolean;
}
interface GetStateParams {
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts
index 45447fe642ad47..6cf34fd8cb0242 100644
--- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts
+++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts
@@ -31,6 +31,7 @@ describe('getStateDefaults', () => {
"default_column",
],
"filters": undefined,
+ "hideAggregatedPreview": undefined,
"hideChart": undefined,
"index": "index-pattern-with-timefield-id",
"interval": "auto",
@@ -42,6 +43,7 @@ describe('getStateDefaults', () => {
"desc",
],
],
+ "viewMode": undefined,
}
`);
});
@@ -61,12 +63,14 @@ describe('getStateDefaults', () => {
"default_column",
],
"filters": undefined,
+ "hideAggregatedPreview": undefined,
"hideChart": undefined,
"index": "the-index-pattern-id",
"interval": "auto",
"query": undefined,
"savedQuery": undefined,
"sort": Array [],
+ "viewMode": undefined,
}
`);
});
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts
index 6fa4dda2eab190..50dab0273d4616 100644
--- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts
+++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts
@@ -60,6 +60,8 @@ export function getStateDefaults({
interval: 'auto',
filters: cloneDeep(searchSource.getOwnField('filter')),
hideChart: chartHidden ? chartHidden : undefined,
+ viewMode: undefined,
+ hideAggregatedPreview: undefined,
savedQuery: undefined,
} as AppState;
if (savedSearch.grid) {
@@ -68,6 +70,13 @@ export function getStateDefaults({
if (savedSearch.hideChart !== undefined) {
defaultState.hideChart = savedSearch.hideChart;
}
+ if (savedSearch.viewMode) {
+ defaultState.viewMode = savedSearch.viewMode;
+ }
+
+ if (savedSearch.hideAggregatedPreview) {
+ defaultState.hideAggregatedPreview = savedSearch.hideAggregatedPreview;
+ }
return defaultState;
}
diff --git a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts
index 584fbe14cb59ed..fa566fd485942c 100644
--- a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts
+++ b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts
@@ -52,6 +52,14 @@ export async function persistSavedSearch(
savedSearch.hideChart = state.hideChart;
}
+ if (state.viewMode) {
+ savedSearch.viewMode = state.viewMode;
+ }
+
+ if (state.hideAggregatedPreview) {
+ savedSearch.hideAggregatedPreview = state.hideAggregatedPreview;
+ }
+
try {
const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client);
if (id) {
diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx b/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx
new file mode 100644
index 00000000000000..5492fac014b747
--- /dev/null
+++ b/src/plugins/discover/public/application/components/data_visualizer_grid/data_visualizer_grid.tsx
@@ -0,0 +1,196 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { 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 {
+ EmbeddableInput,
+ EmbeddableOutput,
+ ErrorEmbeddable,
+ IEmbeddable,
+ isErrorEmbeddable,
+} from '../../../../../embeddable/public';
+import { SavedSearch } from '../../../saved_searches';
+import { GetStateReturn } from '../../apps/main/services/discover_state';
+
+export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
+ indexPattern: IndexPattern;
+ savedSearch?: SavedSearch;
+ query?: Query;
+ visibleFieldNames?: string[];
+ filters?: Filter[];
+ showPreviewByDefault?: boolean;
+ /**
+ * Callback to add a filter to filter bar
+ */
+ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+}
+export interface DataVisualizerGridEmbeddableOutput extends EmbeddableOutput {
+ showDistributions?: boolean;
+}
+
+export interface DiscoverDataVisualizerGridProps {
+ /**
+ * Determines which columns are displayed
+ */
+ columns: string[];
+ /**
+ * The used index pattern
+ */
+ indexPattern: DataView;
+ /**
+ * Saved search description
+ */
+ searchDescription?: string;
+ /**
+ * Saved search title
+ */
+ searchTitle?: string;
+ /**
+ * Discover plugin services
+ */
+ services: DiscoverServices;
+ /**
+ * Optional saved search
+ */
+ savedSearch?: SavedSearch;
+ /**
+ * Optional query to update the table content
+ */
+ query?: Query;
+ /**
+ * Filters query to update the table content
+ */
+ filters?: Filter[];
+ stateContainer?: GetStateReturn;
+ /**
+ * Callback to add a filter to filter bar
+ */
+ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+}
+
+export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProps) => {
+ const {
+ services,
+ indexPattern,
+ savedSearch,
+ query,
+ columns,
+ filters,
+ stateContainer,
+ onAddFilter,
+ } = props;
+ const { uiSettings } = services;
+
+ const [embeddable, setEmbeddable] = useState<
+ | ErrorEmbeddable
+ | IEmbeddable
+ | undefined
+ >();
+ const embeddableRoot: React.RefObject = useRef(null);
+
+ const showPreviewByDefault = useMemo(
+ () =>
+ stateContainer ? !stateContainer.appStateContainer.getState().hideAggregatedPreview : true,
+ [stateContainer]
+ );
+
+ useEffect(() => {
+ const sub = embeddable?.getOutput$().subscribe((output: DataVisualizerGridEmbeddableOutput) => {
+ if (output.showDistributions !== undefined && stateContainer) {
+ stateContainer.setAppState({ hideAggregatedPreview: !output.showDistributions });
+ }
+ });
+
+ return () => {
+ sub?.unsubscribe();
+ };
+ }, [embeddable, stateContainer]);
+
+ useEffect(() => {
+ if (embeddable && !isErrorEmbeddable(embeddable)) {
+ // Update embeddable whenever one of the important input changes
+ embeddable.updateInput({
+ indexPattern,
+ savedSearch,
+ query,
+ filters,
+ visibleFieldNames: columns,
+ onAddFilter,
+ });
+ embeddable.reload();
+ }
+ }, [embeddable, indexPattern, savedSearch, query, columns, filters, onAddFilter]);
+
+ useEffect(() => {
+ if (showPreviewByDefault && embeddable && !isErrorEmbeddable(embeddable)) {
+ // Update embeddable whenever one of the important input changes
+ 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 () => {
+ if (services.embeddable) {
+ const factory = services.embeddable.getEmbeddableFactory<
+ DataVisualizerGridEmbeddableInput,
+ DataVisualizerGridEmbeddableOutput
+ >('data_visualizer_grid');
+ if (factory) {
+ // Initialize embeddable with information available at mount
+ const initializedEmbeddable = await factory.create({
+ id: 'discover_data_visualizer_grid',
+ indexPattern,
+ savedSearch,
+ query,
+ showPreviewByDefault,
+ onAddFilter,
+ });
+ if (!unmounted) {
+ setEmbeddable(initializedEmbeddable);
+ }
+ }
+ }
+ };
+ loadEmbeddable();
+ return () => {
+ unmounted = true;
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [services.embeddable, showPreviewByDefault]);
+
+ // We can only render after embeddable has already initialized
+ useEffect(() => {
+ if (embeddableRoot.current && embeddable) {
+ embeddable.render(embeddableRoot.current);
+ }
+ }, [embeddable, embeddableRoot, uiSettings]);
+
+ return (
+
+ );
+};
diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx b/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx
new file mode 100644
index 00000000000000..099f45bf988ccf
--- /dev/null
+++ b/src/plugins/discover/public/application/components/data_visualizer_grid/field_stats_table_embeddable.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { I18nProvider } from '@kbn/i18n/react';
+import {
+ DiscoverDataVisualizerGrid,
+ DiscoverDataVisualizerGridProps,
+} from './data_visualizer_grid';
+
+export function FieldStatsTableEmbeddable(renderProps: DiscoverDataVisualizerGridProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/plugins/discover/public/application/components/data_visualizer_grid/index.ts b/src/plugins/discover/public/application/components/data_visualizer_grid/index.ts
new file mode 100644
index 00000000000000..dc85495a7c2ec8
--- /dev/null
+++ b/src/plugins/discover/public/application/components/data_visualizer_grid/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { DiscoverDataVisualizerGrid } from './data_visualizer_grid';
diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
index 89c47559d7b4cb..808962dc8319d5 100644
--- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
@@ -19,12 +19,12 @@ import { SEARCH_EMBEDDABLE_TYPE } from './constants';
import { APPLY_FILTER_TRIGGER, esFilters, FilterManager } from '../../../../data/public';
import { DiscoverServices } from '../../build_services';
import {
- Query,
- TimeRange,
Filter,
IndexPattern,
- ISearchSource,
IndexPatternField,
+ ISearchSource,
+ Query,
+ TimeRange,
} from '../../../../data/common';
import { ElasticSearchHit } from '../doc_views/doc_views_types';
import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
@@ -35,6 +35,7 @@ import {
DOC_TABLE_LEGACY,
SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
+ SHOW_FIELD_STATISTICS,
SORT_DEFAULT_ORDER_SETTING,
} from '../../../common';
import * as columnActions from '../apps/main/components/doc_table/actions/columns';
@@ -45,6 +46,8 @@ import { DocTableProps } from '../apps/main/components/doc_table/doc_table_wrapp
import { getDefaultSort } from '../apps/main/components/doc_table';
import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers';
import { updateSearchSource } from './helpers/update_search_source';
+import { VIEW_MODE } from '../apps/main/components/view_mode_toggle';
+import { FieldStatsTableEmbeddable } from '../components/data_visualizer_grid/field_stats_table_embeddable';
export type SearchProps = Partial &
Partial & {
@@ -379,6 +382,28 @@ export class SavedSearchEmbeddable
if (!this.searchProps) {
return;
}
+
+ if (
+ this.services.uiSettings.get(SHOW_FIELD_STATISTICS) === true &&
+ this.savedSearch.viewMode === VIEW_MODE.AGGREGATED_LEVEL &&
+ searchProps.services &&
+ searchProps.indexPattern &&
+ Array.isArray(searchProps.columns)
+ ) {
+ ReactDOM.render(
+ ,
+ domNode
+ );
+ return;
+ }
const useLegacyTable = this.services.uiSettings.get(DOC_TABLE_LEGACY);
const props = {
searchProps,
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts
index ac16b6b3cc2bab..a6b175e34bd136 100644
--- a/src/plugins/discover/public/build_services.ts
+++ b/src/plugins/discover/public/build_services.ts
@@ -37,6 +37,7 @@ import { UrlForwardingStart } from '../../url_forwarding/public';
import { NavigationPublicPluginStart } from '../../navigation/public';
import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public';
import { FieldFormatsStart } from '../../field_formats/public';
+import { EmbeddableStart } from '../../embeddable/public';
import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public';
@@ -47,6 +48,7 @@ export interface DiscoverServices {
core: CoreStart;
data: DataPublicPluginStart;
docLinks: DocLinksStart;
+ embeddable: EmbeddableStart;
history: () => History;
theme: ChartsPluginStart['theme'];
filterManager: FilterManager;
@@ -83,6 +85,7 @@ export function buildServices(
core,
data: plugins.data,
docLinks: core.docLinks,
+ embeddable: plugins.embeddable,
theme: plugins.charts.theme,
fieldFormats: plugins.fieldFormats,
filterManager: plugins.data.query.filterManager,
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index e170e61f7ebc56..c91bcf3897e145 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -348,6 +348,11 @@ export class DiscoverPlugin
await depsStart.data.indexPatterns.clearCache();
const { renderApp } = await import('./application');
+
+ // FIXME: Temporarily hide overflow-y in Discover app when Field Stats table is shown
+ // due to EUI bug https://github.com/elastic/eui/pull/5152
+ params.element.classList.add('dscAppWrapper');
+
const unmount = renderApp(params.element);
return () => {
unlistenParentHistory();
diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts
index 755831e7009ed9..560e16b12e5ed5 100644
--- a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts
+++ b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts
@@ -101,6 +101,7 @@ describe('getSavedSearch', () => {
],
"description": "description",
"grid": Object {},
+ "hideAggregatedPreview": undefined,
"hideChart": false,
"id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7",
"searchSource": Object {
@@ -138,6 +139,7 @@ describe('getSavedSearch', () => {
],
],
"title": "test1",
+ "viewMode": undefined,
}
`);
});
diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts
index 12c73e86b3dc4d..82510340f30f11 100644
--- a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts
+++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts
@@ -54,6 +54,7 @@ describe('saved_searches_utils', () => {
],
"description": "foo",
"grid": Object {},
+ "hideAggregatedPreview": undefined,
"hideChart": true,
"id": "id",
"searchSource": SearchSource {
@@ -74,6 +75,7 @@ describe('saved_searches_utils', () => {
"sharingSavedObjectProps": Object {},
"sort": Array [],
"title": "saved search",
+ "viewMode": undefined,
}
`);
});
@@ -122,6 +124,7 @@ describe('saved_searches_utils', () => {
],
"description": "description",
"grid": Object {},
+ "hideAggregatedPreview": undefined,
"hideChart": true,
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{}",
@@ -133,6 +136,7 @@ describe('saved_searches_utils', () => {
],
],
"title": "title",
+ "viewMode": undefined,
}
`);
});
diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts
index 98ab2267a875ec..064ee6afe0e997 100644
--- a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts
+++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts
@@ -41,6 +41,8 @@ export const fromSavedSearchAttributes = (
description: attributes.description,
grid: attributes.grid,
hideChart: attributes.hideChart,
+ viewMode: attributes.viewMode,
+ hideAggregatedPreview: attributes.hideAggregatedPreview,
});
export const toSavedSearchAttributes = (
@@ -54,4 +56,6 @@ export const toSavedSearchAttributes = (
description: savedSearch.description ?? '',
grid: savedSearch.grid ?? {},
hideChart: savedSearch.hideChart ?? false,
+ viewMode: savedSearch.viewMode,
+ hideAggregatedPreview: savedSearch.hideAggregatedPreview,
});
diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts
index 10a6282063d38d..b3a67ea57e769e 100644
--- a/src/plugins/discover/public/saved_searches/types.ts
+++ b/src/plugins/discover/public/saved_searches/types.ts
@@ -8,6 +8,7 @@
import type { ISearchSource } from '../../../data/public';
import { DiscoverGridSettingsColumn } from '../application/components/discover_grid/types';
+import { VIEW_MODE } from '../application/apps/main/components/view_mode_toggle';
/** @internal **/
export interface SavedSearchAttributes {
@@ -22,6 +23,8 @@ export interface SavedSearchAttributes {
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
+ viewMode?: VIEW_MODE;
+ hideAggregatedPreview?: boolean;
}
/** @internal **/
@@ -44,4 +47,6 @@ export interface SavedSearch {
aliasTargetId?: string;
errorJSON?: string;
};
+ viewMode?: VIEW_MODE;
+ hideAggregatedPreview?: boolean;
}
diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts
index 6a856854076129..23d9312e828970 100644
--- a/src/plugins/discover/server/saved_objects/search.ts
+++ b/src/plugins/discover/server/saved_objects/search.ts
@@ -32,7 +32,9 @@ export const searchSavedObjectType: SavedObjectsType = {
properties: {
columns: { type: 'keyword', index: false, doc_values: false },
description: { type: 'text' },
+ viewMode: { type: 'keyword', index: false, doc_values: false },
hideChart: { type: 'boolean', index: false, doc_values: false },
+ hideAggregatedPreview: { type: 'boolean', index: false, doc_values: false },
hits: { type: 'integer', index: false, doc_values: false },
kibanaSavedObjectMeta: {
properties: {
diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts
index d6a105bdb62630..529ba0d1beef12 100644
--- a/src/plugins/discover/server/ui_settings.ts
+++ b/src/plugins/discover/server/ui_settings.ts
@@ -26,6 +26,7 @@ import {
SEARCH_FIELDS_FROM_SOURCE,
MAX_DOC_FIELDS_DISPLAYED,
SHOW_MULTIFIELDS,
+ SHOW_FIELD_STATISTICS,
} from '../common';
export const getUiSettings: () => Record = () => ({
@@ -172,6 +173,7 @@ export const getUiSettings: () => Record = () => ({
name: 'discover:useLegacyDataGrid',
},
},
+
[MODIFY_COLUMNS_ON_SWITCH]: {
name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', {
defaultMessage: 'Modify columns when changing data views',
@@ -201,6 +203,24 @@ export const getUiSettings: () => Record = () => ({
category: ['discover'],
schema: schema.boolean(),
},
+ [SHOW_FIELD_STATISTICS]: {
+ name: i18n.translate('discover.advancedSettings.discover.showFieldStatistics', {
+ defaultMessage: 'Show field statistics',
+ }),
+ description: i18n.translate(
+ 'discover.advancedSettings.discover.showFieldStatisticsDescription',
+ {
+ defaultMessage: `Enable "Field statistics" table in Discover.`,
+ }
+ ),
+ value: false,
+ category: ['discover'],
+ schema: schema.boolean(),
+ metric: {
+ type: METRIC_TYPE.CLICK,
+ name: 'discover:showFieldStatistics',
+ },
+ },
[SHOW_MULTIFIELDS]: {
name: i18n.translate('discover.advancedSettings.discover.showMultifields', {
defaultMessage: 'Show multi-fields',
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
index a8a391995b0051..bf936b2ae8dbe7 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -448,6 +448,10 @@ export const stackManagementSchema: MakeSchemaFrom = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
+ 'discover:showFieldStatistics': {
+ type: 'boolean',
+ _meta: { description: 'Non-default value of setting.' },
+ },
'discover:showMultiFields': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
index 7ea80ffb77dda3..7575fa5d2b3f3d 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -31,6 +31,7 @@ export interface UsageStats {
'doc_table:legacy': boolean;
'discover:modifyColumnsOnSwitch': boolean;
'discover:searchFieldsFromSource': boolean;
+ 'discover:showFieldStatistics': boolean;
'discover:showMultiFields': boolean;
'discover:maxDocFieldsDisplayed': number;
'securitySolution:rulesTableRefresh': string;
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index c6724056f77a54..f9ca99a26ec198 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -7689,6 +7689,12 @@
"description": "Non-default value of setting."
}
},
+ "discover:showFieldStatistics": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Non-default value of setting."
+ }
+ },
"discover:showMultiFields": {
"type": "boolean",
"_meta": {
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index a6ee65e0febb5e..a45c1a23ed3a5c 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import expect from '@kbn/expect';
import { FtrService } from '../ftr_provider_context';
export class DiscoverPageObject extends FtrService {
@@ -307,6 +308,13 @@ export class DiscoverPageObject extends FtrService {
return await this.testSubjects.click('collapseSideBarButton');
}
+ public async closeSidebar() {
+ await this.retry.tryForTime(2 * 1000, async () => {
+ await this.toggleSidebarCollapse();
+ await this.testSubjects.missingOrFail('discover-sidebar');
+ });
+ }
+
public async getAllFieldNames() {
const sidebar = await this.testSubjects.find('discover-sidebar');
const $ = await sidebar.parseDomContent();
@@ -545,4 +553,37 @@ export class DiscoverPageObject extends FtrService {
public async clearSavedQuery() {
await this.testSubjects.click('saved-query-management-clear-button');
}
+
+ public async assertHitCount(expectedHitCount: string) {
+ await this.retry.tryForTime(2 * 1000, async () => {
+ // Close side bar to ensure Discover hit count shows
+ // edge case for when browser width is small
+ await this.closeSidebar();
+ const hitCount = await this.getHitCount();
+ expect(hitCount).to.eql(
+ expectedHitCount,
+ `Expected Discover hit count to be ${expectedHitCount} but got ${hitCount}.`
+ );
+ });
+ }
+
+ public async assertViewModeToggleNotExists() {
+ await this.testSubjects.missingOrFail('dscViewModeToggle', { timeout: 2 * 1000 });
+ }
+
+ public async assertViewModeToggleExists() {
+ await this.testSubjects.existOrFail('dscViewModeToggle', { timeout: 2 * 1000 });
+ }
+
+ public async assertFieldStatsTableNotExists() {
+ await this.testSubjects.missingOrFail('dscFieldStatsEmbeddedContent', { timeout: 2 * 1000 });
+ }
+
+ public async clickViewModeFieldStatsButton() {
+ await this.retry.tryForTime(2 * 1000, async () => {
+ await this.testSubjects.existOrFail('dscViewModeFieldStatsButton');
+ await this.testSubjects.clickWhenNotDisabled('dscViewModeFieldStatsButton');
+ await this.testSubjects.existOrFail('dscFieldStatsEmbeddedContent');
+ });
+ }
}
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 927d8ddb7a851b..398dc5dad2dc78 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
@@ -10,7 +10,7 @@ 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.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
index 2373cfe1f32841..9d803e3d4a80c6 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
@@ -48,7 +48,7 @@ export const typeToEuiIconMap: Record {
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/constants.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/constants.ts
new file mode 100644
index 00000000000000..26004db8fd5299
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export const DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE = 'data_visualizer_grid';
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_loading_fallback.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_loading_fallback.tsx
new file mode 100644
index 00000000000000..01644efd6652c2
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_loading_fallback.tsx
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
+
+export const EmbeddableLoading = () => {
+ return (
+
+
+
+
+
+ );
+};
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
new file mode 100644
index 00000000000000..f59225b1c019fc
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx
@@ -0,0 +1,234 @@
+/*
+ * 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 { Observable, Subject } from 'rxjs';
+import { CoreStart } from 'kibana/public';
+import ReactDOM from 'react-dom';
+import React, { Suspense, useCallback, useState } from 'react';
+import useObservable from 'react-use/lib/useObservable';
+import { EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
+import { Filter } from '@kbn/es-query';
+import { Required } from 'utility-types';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ Embeddable,
+ EmbeddableInput,
+ EmbeddableOutput,
+ IContainer,
+} from '../../../../../../../../src/plugins/embeddable/public';
+import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
+import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
+import { EmbeddableLoading } from './embeddable_loading_fallback';
+import { DataVisualizerStartDependencies } from '../../../../plugin';
+import {
+ IndexPattern,
+ IndexPatternField,
+ Query,
+} from '../../../../../../../../src/plugins/data/common';
+import { SavedSearch } from '../../../../../../../../src/plugins/discover/public';
+import {
+ DataVisualizerTable,
+ ItemIdToExpandedRowMap,
+} 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 { 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';
+
+export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies];
+export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
+ indexPattern: IndexPattern;
+ savedSearch?: SavedSearch;
+ query?: Query;
+ visibleFieldNames?: string[];
+ filters?: Filter[];
+ showPreviewByDefault?: boolean;
+ /**
+ * Callback to add a filter to filter bar
+ */
+ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+}
+export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput;
+
+export type IDataVisualizerGridEmbeddable = typeof DataVisualizerGridEmbeddable;
+
+const restorableDefaults = getDefaultDataVisualizerListState();
+
+export const EmbeddableWrapper = ({
+ input,
+ onOutputChange,
+}: {
+ input: DataVisualizerGridEmbeddableInput;
+ onOutputChange?: (ouput: any) => void;
+}) => {
+ const [dataVisualizerListState, setDataVisualizerListState] =
+ useState>(restorableDefaults);
+
+ const onTableChange = useCallback(
+ (update: DataVisualizerTableState) => {
+ setDataVisualizerListState({ ...dataVisualizerListState, ...update });
+ if (onOutputChange) {
+ onOutputChange(update);
+ }
+ },
+ [dataVisualizerListState, onOutputChange]
+ );
+ const { configs, searchQueryLanguage, searchString, extendedColumns, loaded } =
+ useDataVisualizerGridData(input, dataVisualizerListState);
+ const getItemIdToExpandedRowMap = useCallback(
+ function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap {
+ return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => {
+ const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName);
+ if (item !== undefined) {
+ m[fieldName] = (
+
+ );
+ }
+ return m;
+ }, {} as ItemIdToExpandedRowMap);
+ },
+ [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))
+ ) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ return (
+
+ items={configs}
+ pageState={dataVisualizerListState}
+ updatePageState={onTableChange}
+ getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
+ extendedColumns={extendedColumns}
+ showPreviewByDefault={input?.showPreviewByDefault}
+ onChange={onOutputChange}
+ />
+ );
+};
+
+export const IndexDataVisualizerViewWrapper = (props: {
+ id: string;
+ embeddableContext: InstanceType;
+ embeddableInput: Readonly>;
+ onOutputChange?: (output: any) => void;
+}) => {
+ const { embeddableInput, onOutputChange } = props;
+
+ const input = useObservable(embeddableInput);
+ if (input && input.indexPattern) {
+ return ;
+ } else {
+ return (
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+ );
+ }
+};
+export class DataVisualizerGridEmbeddable extends Embeddable<
+ DataVisualizerGridEmbeddableInput,
+ DataVisualizerGridEmbeddableOutput
+> {
+ private node?: HTMLElement;
+ private reload$ = new Subject();
+ public readonly type: string = DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE;
+
+ constructor(
+ initialInput: DataVisualizerGridEmbeddableInput,
+ public services: DataVisualizerGridEmbeddableServices,
+ parent?: IContainer
+ ) {
+ super(initialInput, {}, parent);
+ }
+
+ public render(node: HTMLElement) {
+ super.render(node);
+ this.node = node;
+
+ const I18nContext = this.services[0].i18n.Context;
+
+ ReactDOM.render(
+
+
+ }>
+ this.updateOutput(output)}
+ />
+
+
+ ,
+ node
+ );
+ }
+
+ public destroy() {
+ super.destroy();
+ if (this.node) {
+ ReactDOM.unmountComponentAtNode(this.node);
+ }
+ }
+
+ public reload() {
+ this.reload$.next();
+ }
+
+ public supportedTriggers() {
+ return [];
+ }
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable_factory.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable_factory.tsx
new file mode 100644
index 00000000000000..08ddc2d5fe3c22
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable_factory.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { StartServicesAccessor } from 'kibana/public';
+import {
+ EmbeddableFactoryDefinition,
+ IContainer,
+} from '../../../../../../../../src/plugins/embeddable/public';
+import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
+import {
+ DataVisualizerGridEmbeddableInput,
+ DataVisualizerGridEmbeddableServices,
+} from './grid_embeddable';
+import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../../plugin';
+
+export class DataVisualizerGridEmbeddableFactory
+ implements EmbeddableFactoryDefinition
+{
+ public readonly type = DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE;
+
+ public readonly grouping = [
+ {
+ id: 'data_visualizer_grid',
+ getDisplayName: () => 'Data Visualizer Grid',
+ },
+ ];
+
+ constructor(
+ private getStartServices: StartServicesAccessor<
+ DataVisualizerStartDependencies,
+ DataVisualizerPluginStart
+ >
+ ) {}
+
+ public async isEditable() {
+ return false;
+ }
+
+ public canCreateNew() {
+ return false;
+ }
+
+ public getDisplayName() {
+ return i18n.translate('xpack.dataVisualizer.index.components.grid.displayName', {
+ defaultMessage: 'Data visualizer grid',
+ });
+ }
+
+ public getDescription() {
+ return i18n.translate('xpack.dataVisualizer.index.components.grid.description', {
+ defaultMessage: 'Visualize data',
+ });
+ }
+
+ private async getServices(): Promise {
+ const [coreStart, pluginsStart] = await this.getStartServices();
+ return [coreStart, pluginsStart];
+ }
+
+ public async create(initialInput: DataVisualizerGridEmbeddableInput, parent?: IContainer) {
+ const [coreStart, pluginsStart] = await this.getServices();
+ const { DataVisualizerGridEmbeddable } = await import('./grid_embeddable');
+ return new DataVisualizerGridEmbeddable(initialInput, [coreStart, pluginsStart], parent);
+ }
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/index.ts
new file mode 100644
index 00000000000000..91ca8e1633eb90
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+export { DataVisualizerGridEmbeddable } from './grid_embeddable';
+export { DataVisualizerGridEmbeddableFactory } from './grid_embeddable_factory';
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/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts
new file mode 100644
index 00000000000000..fc0fc7a2134b44
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts
@@ -0,0 +1,587 @@
+/*
+ * 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 { Required } from 'utility-types';
+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 {
+ 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';
+
+const defaults = getDefaultPageState();
+
+export const useDataVisualizerGridData = (
+ input: DataVisualizerGridEmbeddableInput,
+ dataVisualizerListState: Required
+) => {
+ const { services } = useDataVisualizerKibana();
+ const { notifications, uiSettings } = services;
+ const { toasts } = notifications;
+ const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState;
+
+ const [lastRefresh, setLastRefresh] = useState(0);
+
+ const {
+ currentSavedSearch,
+ currentIndexPattern,
+ currentQuery,
+ currentFilters,
+ visibleFieldNames,
+ } = useMemo(
+ () => ({
+ currentSavedSearch: input?.savedSearch,
+ currentIndexPattern: input.indexPattern,
+ currentQuery: input?.query,
+ visibleFieldNames: input?.visibleFieldNames ?? [],
+ currentFilters: input?.filters,
+ }),
+ [input]
+ );
+
+ const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
+ const searchData = getEsQueryFromSavedSearch({
+ indexPattern: currentIndexPattern,
+ uiSettings,
+ savedSearch: currentSavedSearch,
+ query: currentQuery,
+ filters: currentFilters,
+ });
+
+ if (searchData === undefined || dataVisualizerListState.searchString !== '') {
+ 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,
+ currentQuery,
+ currentFilters,
+ ]);
+
+ const [overallStats, setOverallStats] = useState(defaults.overallStats);
+
+ const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats);
+ const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs);
+ const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded);
+ const [metricsStats, setMetricsStats] = useState();
+
+ const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
+ const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
+
+ const dataLoader = useMemo(
+ () => new DataLoader(currentIndexPattern, toasts),
+ [currentIndexPattern, toasts]
+ );
+
+ const timefilter = useTimefilter({
+ timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined,
+ autoRefreshSelector: true,
+ });
+
+ useEffect(() => {
+ const timeUpdateSubscription = merge(
+ timefilter.getTimeUpdate$(),
+ dataVisualizerRefresh$
+ ).subscribe(() => {
+ setLastRefresh(Date.now());
+ });
+ return () => {
+ timeUpdateSubscription.unsubscribe();
+ };
+ });
+
+ 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 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 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 || [];
+
+ 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 index pattern 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,
+ ]);
+
+ 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]);
+
+ 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]);
+
+ 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]);
+
+ // Some actions open up fly-out or popup
+ // This variable is used to keep track of them and clean up when unmounting
+ const actionFlyoutRef = useRef<() => void | undefined>();
+ useEffect(() => {
+ const ref = actionFlyoutRef;
+ return () => {
+ // Clean up any of the flyout/editor opened from the actions
+ if (ref.current) {
+ ref.current();
+ }
+ };
+ }, []);
+
+ // Inject custom action column for the index based visualizer
+ // Hide the column completely if no access to any of the plugins
+ const extendedColumns = useMemo(() => {
+ const actions = getActions(
+ input.indexPattern,
+ { lens: services.lens },
+ {
+ searchQueryLanguage,
+ searchString,
+ },
+ actionFlyoutRef
+ );
+ if (!Array.isArray(actions) || actions.length < 1) return;
+
+ const actionColumn: EuiTableActionsColumnType = {
+ name: i18n.translate('xpack.dataVisualizer.index.dataGrid.actionsColumnLabel', {
+ defaultMessage: 'Actions',
+ }),
+ actions,
+ width: '70px',
+ };
+
+ return [actionColumn];
+ }, [input.indexPattern, services, searchQueryLanguage, searchString]);
+
+ return {
+ configs,
+ searchQueryLanguage,
+ searchString,
+ searchQuery,
+ extendedColumns,
+ documentCountStats,
+ metricsStats,
+ loaded: metricsLoaded && nonMetricsLoaded,
+ };
+};
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/index.ts
new file mode 100644
index 00000000000000..add99a8d2501d5
--- /dev/null
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { CoreSetup } from 'kibana/public';
+import { EmbeddableSetup } from '../../../../../../../src/plugins/embeddable/public';
+import { DataVisualizerGridEmbeddableFactory } from './grid_embeddable/grid_embeddable_factory';
+import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../plugin';
+
+export function registerEmbeddables(
+ embeddable: EmbeddableSetup,
+ core: CoreSetup
+) {
+ const dataVisualizerGridEmbeddableFactory = new DataVisualizerGridEmbeddableFactory(
+ core.getStartServices
+ );
+ embeddable.registerEmbeddableFactory(
+ dataVisualizerGridEmbeddableFactory.type,
+ dataVisualizerGridEmbeddableFactory
+ );
+}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
index a474ed3521580e..83e013703c1fcf 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
@@ -9,7 +9,6 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { parse, stringify } from 'query-string';
import { isEqual } from 'lodash';
-// @ts-ignore
import { encode } from 'rison-node';
import { SimpleSavedObject } from 'kibana/public';
import { i18n } from '@kbn/i18n';
@@ -29,7 +28,7 @@ import {
isRisonSerializationRequired,
} from '../common/util/url_state';
import { useDataVisualizerKibana } from '../kibana_context';
-import { IndexPattern } from '../../../../../../src/plugins/data/common';
+import { DataView } from '../../../../../../src/plugins/data/common';
import { ResultLink } from '../common/components/results_links';
export type IndexDataVisualizerSpec = typeof IndexDataVisualizer;
@@ -51,9 +50,7 @@ export const DataVisualizerUrlStateContextProvider: FC(
- undefined
- );
+ const [currentIndexPattern, setCurrentIndexPattern] = useState(undefined);
const [currentSavedSearch, setCurrentSavedSearch] = useState | null>(
null
);
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts
index c26a668bd04ab8..aab67d0b52aecd 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts
@@ -4,8 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-// @ts-ignore
import { encode } from 'rison-node';
import { stringify } from 'query-string';
import { SerializableRecord } from '@kbn/utility-types';
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts
index 43d815f6e9d411..ad3229676b31b6 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts
@@ -6,7 +6,7 @@
*/
import {
- getQueryFromSavedSearch,
+ getQueryFromSavedSearchObject,
createMergedEsQuery,
getEsQueryFromSavedSearch,
} from './saved_search_utils';
@@ -82,9 +82,9 @@ const kqlSavedSearch: SavedSearch = {
},
};
-describe('getQueryFromSavedSearch()', () => {
+describe('getQueryFromSavedSearchObject()', () => {
it('should return parsed searchSourceJSON with query and filter', () => {
- expect(getQueryFromSavedSearch(luceneSavedSearchObj)).toEqual({
+ expect(getQueryFromSavedSearchObject(luceneSavedSearchObj)).toEqual({
filter: [
{
$state: { store: 'appState' },
@@ -106,7 +106,7 @@ describe('getQueryFromSavedSearch()', () => {
query: { language: 'lucene', query: 'responsetime:>50' },
version: true,
});
- expect(getQueryFromSavedSearch(kqlSavedSearch)).toEqual({
+ expect(getQueryFromSavedSearchObject(kqlSavedSearch)).toEqual({
filter: [
{
$state: { store: 'appState' },
@@ -130,7 +130,7 @@ describe('getQueryFromSavedSearch()', () => {
});
});
it('should return undefined if invalid searchSourceJSON', () => {
- expect(getQueryFromSavedSearch(luceneInvalidSavedSearchObj)).toEqual(undefined);
+ expect(getQueryFromSavedSearchObject(luceneInvalidSavedSearchObj)).toEqual(undefined);
});
});
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
index 80a2069aab1a88..1401b1038b8f24 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts
@@ -16,17 +16,31 @@ import {
Filter,
} from '@kbn/es-query';
import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types';
-import { IndexPattern } from '../../../../../../../src/plugins/data/common';
+import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/common';
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query';
import { SavedSearch } from '../../../../../../../src/plugins/discover/public';
import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
+const DEFAULT_QUERY = {
+ bool: {
+ must: [
+ {
+ match_all: {},
+ },
+ ],
+ },
+};
+
+export function getDefaultQuery() {
+ return cloneDeep(DEFAULT_QUERY);
+}
+
/**
* Parse the stringified searchSourceJSON
* from a saved search or saved search object
*/
-export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject | SavedSearch) {
+export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject | SavedSearch) {
const search = isSavedSearchSavedObject(savedSearch)
? savedSearch?.attributes?.kibanaSavedObjectMeta
: // @ts-expect-error kibanaSavedObjectMeta does exist
@@ -69,20 +83,22 @@ export function createMergedEsQuery(
if (query.query !== '') {
combinedQuery = toElasticsearchQuery(ast, indexPattern);
}
- const filterQuery = buildQueryFromFilters(filters, indexPattern);
+ if (combinedQuery.bool !== undefined) {
+ const filterQuery = buildQueryFromFilters(filters, indexPattern);
- if (Array.isArray(combinedQuery.bool.filter) === false) {
- combinedQuery.bool.filter =
- combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
- }
+ if (Array.isArray(combinedQuery.bool.filter) === false) {
+ combinedQuery.bool.filter =
+ combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
+ }
- if (Array.isArray(combinedQuery.bool.must_not) === false) {
- combinedQuery.bool.must_not =
- combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
- }
+ if (Array.isArray(combinedQuery.bool.must_not) === false) {
+ combinedQuery.bool.must_not =
+ combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
+ }
- combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
- combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
+ combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
+ combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
+ }
} else {
combinedQuery = buildEsQuery(
indexPattern,
@@ -115,10 +131,31 @@ export function getEsQueryFromSavedSearch({
}) {
if (!indexPattern || !savedSearch) return;
- const savedSearchData = getQueryFromSavedSearch(savedSearch);
const userQuery = query;
const userFilters = filters;
+ // If saved search has a search source with nested parent
+ // e.g. a search coming from Dashboard saved search embeddable
+ // which already combines both the saved search's original query/filters and the Dashboard's
+ // then no need to process any further
+ if (
+ savedSearch &&
+ 'searchSource' in savedSearch &&
+ savedSearch?.searchSource instanceof SearchSource &&
+ savedSearch.searchSource.getParent() !== undefined &&
+ userQuery
+ ) {
+ return {
+ searchQuery: savedSearch.searchSource.getSearchRequestBody()?.query ?? getDefaultQuery(),
+ searchString: userQuery.query,
+ queryLanguage: userQuery.language as SearchQueryLanguage,
+ };
+ }
+
+ // If saved search is an json object with the original query and filter
+ // retrieve the parsed query and filter
+ const savedSearchData = getQueryFromSavedSearchObject(savedSearch);
+
// If no saved search available, use user's query and filters
if (!savedSearchData && userQuery) {
if (filterManager && userFilters) filterManager.setFilters(userFilters);
@@ -137,7 +174,8 @@ export function getEsQueryFromSavedSearch({
};
}
- // If saved search available, merge saved search with latest user query or filters differ from extracted saved search data
+ // If saved search available, merge saved search with latest user query or filters
+ // which might differ from extracted saved search data
if (savedSearchData) {
const currentQuery = userQuery ?? savedSearchData?.query;
const currentFilters = userFilters ?? savedSearchData?.filter;
@@ -158,17 +196,3 @@ export function getEsQueryFromSavedSearch({
};
}
}
-
-const DEFAULT_QUERY = {
- bool: {
- must: [
- {
- match_all: {},
- },
- ],
- },
-};
-
-export function getDefaultQuery() {
- return cloneDeep(DEFAULT_QUERY);
-}
diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts
index 112294f4b246fe..df1a5ea406d768 100644
--- a/x-pack/plugins/data_visualizer/public/plugin.ts
+++ b/x-pack/plugins/data_visualizer/public/plugin.ts
@@ -6,7 +6,7 @@
*/
import { CoreSetup, CoreStart } from 'kibana/public';
-import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
+import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import type { SharePluginStart } from '../../../../src/plugins/share/public';
import { Plugin } from '../../../../src/core/public';
@@ -21,9 +21,11 @@ import type { IndexPatternFieldEditorStart } from '../../../../src/plugins/index
import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api';
import { getMaxBytesFormatted } from './application/common/util/get_max_bytes';
import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home';
+import { registerEmbeddables } from './application/index_data_visualizer/embeddables';
export interface DataVisualizerSetupDependencies {
home?: HomePublicPluginSetup;
+ embeddable: EmbeddableSetup;
}
export interface DataVisualizerStartDependencies {
data: DataPublicPluginStart;
@@ -56,6 +58,9 @@ export class DataVisualizerPlugin
registerHomeAddData(plugins.home);
registerHomeFeatureCatalogue(plugins.home);
}
+ if (plugins.embeddable) {
+ registerEmbeddables(plugins.embeddable, core);
+ }
}
public start(core: CoreStart, plugins: DataVisualizerStartDependencies) {
diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json
index 3b424ef8b9f658..df41fdbd62663f 100644
--- a/x-pack/plugins/data_visualizer/tsconfig.json
+++ b/x-pack/plugins/data_visualizer/tsconfig.json
@@ -6,9 +6,18 @@
"declaration": true,
"declarationMap": true
},
- "include": ["common/**/*", "public/**/*", "server/**/*"],
+ "include": [
+ "../../../typings/**/*",
+ "common/**/*",
+ "public/**/*",
+ "scripts/**/*",
+ "server/**/*",
+ "types/**/*"
+ ],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
{ "path": "../../../src/plugins/custom_integrations/tsconfig.json" },
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts
index 324db0d6b2ad48..41973b5ec2d017 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/use_saved_search.ts
@@ -15,7 +15,7 @@ import {
import { estypes } from '@elastic/elasticsearch';
import { useMlContext } from '../../../../../contexts/ml';
import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search';
-import { getQueryFromSavedSearch } from '../../../../../util/index_utils';
+import { getQueryFromSavedSearchObject } from '../../../../../util/index_utils';
// `undefined` is used for a non-initialized state
// `null` is set if no saved search is used
@@ -40,7 +40,7 @@ export function useSavedSearch() {
let qryString;
if (currentSavedSearch !== null) {
- const { query } = getQueryFromSavedSearch(currentSavedSearch);
+ const { query } = getQueryFromSavedSearchObject(currentSavedSearch);
const queryLanguage = query.language;
qryString = query.query;
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts
index 5eae60900e09ff..ebab3769fbe578 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts
@@ -18,7 +18,7 @@ import { IUiSettingsClient } from 'kibana/public';
import { getEsQueryConfig } from '../../../../../../../../src/plugins/data/public';
import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search';
import { SavedSearchSavedObject } from '../../../../../common/types/kibana';
-import { getQueryFromSavedSearch } from '../../../util/index_utils';
+import { getQueryFromSavedSearchObject } from '../../../util/index_utils';
// Provider for creating the items used for searching and job creation.
@@ -52,7 +52,7 @@ export function createSearchItems(
let combinedQuery: any = getDefaultDatafeedQuery();
if (savedSearch !== null) {
- const data = getQueryFromSavedSearch(savedSearch);
+ const data = getQueryFromSavedSearchObject(savedSearch);
query = data.query;
const filter = data.filter;
diff --git a/x-pack/plugins/ml/public/application/util/index_utils.ts b/x-pack/plugins/ml/public/application/util/index_utils.ts
index e4c18308bf0171..b105761e5ebcff 100644
--- a/x-pack/plugins/ml/public/application/util/index_utils.ts
+++ b/x-pack/plugins/ml/public/application/util/index_utils.ts
@@ -80,7 +80,7 @@ export async function getIndexPatternAndSavedSearch(savedSearchId: string) {
return resp;
}
-export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) {
+export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject) {
const search = savedSearch.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string };
return JSON.parse(search.searchSourceJSON) as {
query: Query;
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index.ts b/x-pack/test/functional/apps/ml/data_visualizer/index.ts
index 3e6b644a0b494a..c1e5d0b4b6aaec 100644
--- a/x-pack/test/functional/apps/ml/data_visualizer/index.ts
+++ b/x-pack/test/functional/apps/ml/data_visualizer/index.ts
@@ -9,9 +9,10 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('data visualizer', function () {
- this.tags(['skipFirefox']);
+ this.tags(['skipFirefox', 'mlqa']);
loadTestFile(require.resolve('./index_data_visualizer'));
+ loadTestFile(require.resolve('./index_data_visualizer_grid_in_discover'));
loadTestFile(require.resolve('./index_data_visualizer_actions_panel'));
loadTestFile(require.resolve('./index_data_visualizer_index_pattern_management'));
loadTestFile(require.resolve('./file_data_visualizer'));
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts
index 542f7f3116c944..ff0d489293682a 100644
--- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts
+++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts
@@ -6,374 +6,18 @@
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
-import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types';
-import { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types';
-
-interface MetricFieldVisConfig extends FieldVisConfig {
- statsMaxDecimalPlaces: number;
- docCountFormatted: string;
- topValuesCount: number;
- viewableInLens: boolean;
-}
-
-interface NonMetricFieldVisConfig extends FieldVisConfig {
- docCountFormatted: string;
- exampleCount: number;
- viewableInLens: boolean;
-}
-
-interface TestData {
- suiteTitle: string;
- sourceIndexOrSavedSearch: string;
- fieldNameFilters: string[];
- fieldTypeFilters: string[];
- rowsPerPage?: 10 | 25 | 50;
- sampleSizeValidations: Array<{
- size: number;
- expected: { field: string; docCountFormatted: string };
- }>;
- expected: {
- totalDocCountFormatted: string;
- metricFields?: MetricFieldVisConfig[];
- nonMetricFields?: NonMetricFieldVisConfig[];
- emptyFields: string[];
- visibleMetricFieldsCount: number;
- totalMetricFieldsCount: number;
- populatedFieldsCount: number;
- totalFieldsCount: number;
- fieldNameFiltersResultCount: number;
- fieldTypeFiltersResultCount: number;
- };
-}
+import { TestData, MetricFieldVisConfig } from './types';
+import {
+ farequoteDataViewTestData,
+ farequoteKQLSearchTestData,
+ farequoteLuceneSearchTestData,
+ sampleLogTestData,
+} from './index_test_data';
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
- const farequoteDataViewTestData: TestData = {
- suiteTitle: 'data view',
- sourceIndexOrSavedSearch: 'ft_farequote',
- fieldNameFilters: ['airline', '@timestamp'],
- fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD],
- sampleSizeValidations: [
- { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
- { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
- ],
- expected: {
- totalDocCountFormatted: '86,274',
- metricFields: [
- {
- fieldName: 'responsetime',
- type: ML_JOB_FIELD_TYPES.NUMBER,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- docCountFormatted: '5000 (100%)',
- statsMaxDecimalPlaces: 3,
- topValuesCount: 10,
- viewableInLens: true,
- },
- ],
- nonMetricFields: [
- {
- fieldName: '@timestamp',
- type: ML_JOB_FIELD_TYPES.DATE,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- docCountFormatted: '5000 (100%)',
- exampleCount: 2,
- viewableInLens: true,
- },
- {
- fieldName: '@version',
- type: ML_JOB_FIELD_TYPES.TEXT,
- existsInDocs: true,
- aggregatable: false,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '',
- viewableInLens: false,
- },
- {
- fieldName: '@version.keyword',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- {
- fieldName: 'airline',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 10,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- {
- fieldName: 'type',
- type: ML_JOB_FIELD_TYPES.TEXT,
- existsInDocs: true,
- aggregatable: false,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '',
- viewableInLens: false,
- },
- {
- fieldName: 'type.keyword',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- ],
- emptyFields: ['sourcetype'],
- visibleMetricFieldsCount: 1,
- totalMetricFieldsCount: 1,
- populatedFieldsCount: 7,
- totalFieldsCount: 8,
- fieldNameFiltersResultCount: 2,
- fieldTypeFiltersResultCount: 3,
- },
- };
-
- const farequoteKQLSearchTestData: TestData = {
- suiteTitle: 'KQL saved search',
- sourceIndexOrSavedSearch: 'ft_farequote_kuery',
- fieldNameFilters: ['@version'],
- fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT],
- sampleSizeValidations: [
- { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
- { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
- ],
- expected: {
- totalDocCountFormatted: '34,415',
- metricFields: [
- {
- fieldName: 'responsetime',
- type: ML_JOB_FIELD_TYPES.NUMBER,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- docCountFormatted: '5000 (100%)',
- statsMaxDecimalPlaces: 3,
- topValuesCount: 10,
- viewableInLens: true,
- },
- ],
- nonMetricFields: [
- {
- fieldName: '@timestamp',
- type: ML_JOB_FIELD_TYPES.DATE,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- docCountFormatted: '5000 (100%)',
- exampleCount: 2,
- viewableInLens: true,
- },
- {
- fieldName: '@version',
- type: ML_JOB_FIELD_TYPES.TEXT,
- existsInDocs: true,
- aggregatable: false,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '',
- viewableInLens: false,
- },
- {
- fieldName: '@version.keyword',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- {
- fieldName: 'airline',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 5,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- {
- fieldName: 'type',
- type: ML_JOB_FIELD_TYPES.TEXT,
- existsInDocs: true,
- aggregatable: false,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '',
- viewableInLens: false,
- },
- {
- fieldName: 'type.keyword',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- ],
- emptyFields: ['sourcetype'],
- visibleMetricFieldsCount: 1,
- totalMetricFieldsCount: 1,
- populatedFieldsCount: 7,
- totalFieldsCount: 8,
- fieldNameFiltersResultCount: 1,
- fieldTypeFiltersResultCount: 3,
- },
- };
-
- const farequoteLuceneSearchTestData: TestData = {
- suiteTitle: 'lucene saved search',
- sourceIndexOrSavedSearch: 'ft_farequote_lucene',
- fieldNameFilters: ['@version.keyword', 'type'],
- fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER],
- sampleSizeValidations: [
- { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
- { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
- ],
- expected: {
- totalDocCountFormatted: '34,416',
- metricFields: [
- {
- fieldName: 'responsetime',
- type: ML_JOB_FIELD_TYPES.NUMBER,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- docCountFormatted: '5000 (100%)',
- statsMaxDecimalPlaces: 3,
- topValuesCount: 10,
- viewableInLens: true,
- },
- ],
- nonMetricFields: [
- {
- fieldName: '@timestamp',
- type: ML_JOB_FIELD_TYPES.DATE,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- docCountFormatted: '5000 (100%)',
- exampleCount: 2,
- viewableInLens: true,
- },
- {
- fieldName: '@version',
- type: ML_JOB_FIELD_TYPES.TEXT,
- existsInDocs: true,
- aggregatable: false,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '',
- viewableInLens: false,
- },
- {
- fieldName: '@version.keyword',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- {
- fieldName: 'airline',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 5,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- {
- fieldName: 'type',
- type: ML_JOB_FIELD_TYPES.TEXT,
- existsInDocs: true,
- aggregatable: false,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '',
- viewableInLens: false,
- },
- {
- fieldName: 'type.keyword',
- type: ML_JOB_FIELD_TYPES.KEYWORD,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- exampleCount: 1,
- docCountFormatted: '5000 (100%)',
- viewableInLens: true,
- },
- ],
- emptyFields: ['sourcetype'],
- visibleMetricFieldsCount: 1,
- totalMetricFieldsCount: 1,
- populatedFieldsCount: 7,
- totalFieldsCount: 8,
- fieldNameFiltersResultCount: 2,
- fieldTypeFiltersResultCount: 1,
- },
- };
-
- const sampleLogTestData: TestData = {
- suiteTitle: 'geo point field',
- sourceIndexOrSavedSearch: 'ft_module_sample_logs',
- fieldNameFilters: ['geo.coordinates'],
- fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT],
- rowsPerPage: 50,
- expected: {
- totalDocCountFormatted: '408',
- metricFields: [],
- // only testing the geo_point fields
- nonMetricFields: [
- {
- fieldName: 'geo.coordinates',
- type: ML_JOB_FIELD_TYPES.GEO_POINT,
- existsInDocs: true,
- aggregatable: true,
- loading: false,
- docCountFormatted: '408 (100%)',
- exampleCount: 10,
- viewableInLens: false,
- },
- ],
- emptyFields: [],
- visibleMetricFieldsCount: 4,
- totalMetricFieldsCount: 5,
- populatedFieldsCount: 35,
- totalFieldsCount: 36,
- fieldNameFiltersResultCount: 1,
- fieldTypeFiltersResultCount: 1,
- },
- sampleSizeValidations: [
- { size: 1000, expected: { field: 'geo.coordinates', docCountFormatted: '408 (100%)' } },
- { size: 5000, expected: { field: '@timestamp', docCountFormatted: '408 (100%)' } },
- ],
- };
-
function runTests(testData: TestData) {
it(`${testData.suiteTitle} loads the source data in the data visualizer`, async () => {
await ml.testExecution.logTestStep(
@@ -541,7 +185,7 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('with module_sample_logs ', function () {
- // Run tests on full farequote index.
+ // Run tests on full ft_module_sample_logs index.
it(`${sampleLogTestData.suiteTitle} loads the data visualizer selector page`, async () => {
// Start navigation from the base of the ML app.
await ml.navigation.navigateToMl();
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts
new file mode 100644
index 00000000000000..ba24684e130362
--- /dev/null
+++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover.ts
@@ -0,0 +1,172 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+import { TestData, MetricFieldVisConfig } from './types';
+
+const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics';
+import {
+ farequoteDataViewTestData,
+ farequoteKQLSearchTestData,
+ farequoteLuceneFiltersSearchTestData,
+ farequoteKQLFiltersSearchTestData,
+ farequoteLuceneSearchTestData,
+ sampleLogTestData,
+} from './index_test_data';
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']);
+ const ml = getService('ml');
+ const testSubjects = getService('testSubjects');
+ const retry = getService('retry');
+ const toasts = getService('toasts');
+
+ const selectIndexPattern = async (indexPattern: string) => {
+ await retry.tryForTime(2 * 1000, async () => {
+ await PageObjects.discover.selectIndexPattern(indexPattern);
+ const indexPatternTitle = await testSubjects.getVisibleText('indexPattern-switch-link');
+ expect(indexPatternTitle).to.be(indexPattern);
+ });
+ };
+
+ const clearAdvancedSetting = async (propertyName: string) => {
+ await retry.tryForTime(2 * 1000, async () => {
+ await PageObjects.common.navigateToUrl('management', 'kibana/settings', {
+ shouldUseHashForSubUrl: false,
+ });
+ if ((await PageObjects.settings.getAdvancedSettingCheckbox(propertyName)) === 'true') {
+ await PageObjects.settings.clearAdvancedSettings(propertyName);
+ }
+ });
+ };
+
+ const setAdvancedSettingCheckbox = async (propertyName: string, checkedState: boolean) => {
+ await retry.tryForTime(2 * 1000, async () => {
+ await PageObjects.common.navigateToUrl('management', 'kibana/settings', {
+ shouldUseHashForSubUrl: false,
+ });
+ await testSubjects.click('settings');
+ await toasts.dismissAllToasts();
+ await PageObjects.settings.toggleAdvancedSettingCheckbox(propertyName, checkedState);
+ });
+ };
+
+ function runTestsWhenDisabled(testData: TestData) {
+ it('should not show view mode toggle or Field stats table', async function () {
+ await PageObjects.common.navigateToApp('discover');
+ if (testData.isSavedSearch) {
+ await retry.tryForTime(2 * 1000, async () => {
+ await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
+ });
+ } else {
+ await selectIndexPattern(testData.sourceIndexOrSavedSearch);
+ }
+
+ await PageObjects.timePicker.setAbsoluteRange(
+ 'Jan 1, 2016 @ 00:00:00.000',
+ 'Nov 1, 2020 @ 00:00:00.000'
+ );
+
+ await PageObjects.discover.assertViewModeToggleNotExists();
+ await PageObjects.discover.assertFieldStatsTableNotExists();
+ });
+ }
+
+ function runTests(testData: TestData) {
+ describe(`with ${testData.suiteTitle}`, function () {
+ it(`displays the 'Field statistics' table content correctly`, async function () {
+ await PageObjects.common.navigateToApp('discover');
+ if (testData.isSavedSearch) {
+ await retry.tryForTime(2 * 1000, async () => {
+ await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
+ });
+ } else {
+ await selectIndexPattern(testData.sourceIndexOrSavedSearch);
+ }
+ await PageObjects.timePicker.setAbsoluteRange(
+ 'Jan 1, 2016 @ 00:00:00.000',
+ 'Nov 1, 2020 @ 00:00:00.000'
+ );
+
+ await PageObjects.discover.assertHitCount(testData.expected.totalDocCountFormatted);
+ await PageObjects.discover.assertViewModeToggleExists();
+ await PageObjects.discover.clickViewModeFieldStatsButton();
+ await ml.testExecution.logTestStep(
+ 'displays details for metric fields and non-metric fields correctly'
+ );
+ for (const fieldRow of testData.expected.metricFields as Array<
+ Required
+ >) {
+ await ml.dataVisualizerTable.assertNumberFieldContents(
+ fieldRow.fieldName,
+ fieldRow.docCountFormatted,
+ fieldRow.topValuesCount,
+ fieldRow.viewableInLens
+ );
+ }
+
+ for (const fieldRow of testData.expected.nonMetricFields!) {
+ await ml.dataVisualizerTable.assertNonMetricFieldContents(
+ fieldRow.type,
+ fieldRow.fieldName!,
+ fieldRow.docCountFormatted,
+ fieldRow.exampleCount,
+ fieldRow.viewableInLens,
+ false,
+ fieldRow.exampleContent
+ );
+ }
+ });
+ });
+ }
+
+ describe('field statistics in Discover', function () {
+ before(async function () {
+ await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
+ await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/module_sample_logs');
+ await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
+ await ml.testResources.createIndexPatternIfNeeded('ft_module_sample_logs', '@timestamp');
+ await ml.testResources.createSavedSearchFarequoteKueryIfNeeded();
+ await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded();
+ await ml.testResources.createSavedSearchFarequoteFilterAndLuceneIfNeeded();
+ await ml.testResources.createSavedSearchFarequoteFilterAndKueryIfNeeded();
+
+ await ml.securityUI.loginAsMlPowerUser();
+ });
+
+ after(async function () {
+ await clearAdvancedSetting(SHOW_FIELD_STATISTICS);
+ });
+
+ describe('when enabled', function () {
+ before(async function () {
+ await setAdvancedSettingCheckbox(SHOW_FIELD_STATISTICS, true);
+ });
+
+ after(async function () {
+ await clearAdvancedSetting(SHOW_FIELD_STATISTICS);
+ });
+
+ runTests(farequoteDataViewTestData);
+ runTests(farequoteKQLSearchTestData);
+ runTests(farequoteLuceneSearchTestData);
+ runTests(farequoteKQLFiltersSearchTestData);
+ runTests(farequoteLuceneFiltersSearchTestData);
+ runTests(sampleLogTestData);
+ });
+
+ describe('when disabled', function () {
+ before(async function () {
+ // Ensure that the setting is set to default state which is false
+ await setAdvancedSettingCheckbox(SHOW_FIELD_STATISTICS, false);
+ });
+
+ runTestsWhenDisabled(farequoteDataViewTestData);
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_test_data.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_test_data.ts
new file mode 100644
index 00000000000000..6dd782487fdf85
--- /dev/null
+++ b/x-pack/test/functional/apps/ml/data_visualizer/index_test_data.ts
@@ -0,0 +1,533 @@
+/*
+ * 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 { TestData } from './types';
+import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types';
+
+export const farequoteDataViewTestData: TestData = {
+ suiteTitle: 'farequote index pattern',
+ isSavedSearch: false,
+ sourceIndexOrSavedSearch: 'ft_farequote',
+ fieldNameFilters: ['airline', '@timestamp'],
+ fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD],
+ sampleSizeValidations: [
+ { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
+ { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
+ ],
+ expected: {
+ totalDocCountFormatted: '86,274',
+ metricFields: [
+ {
+ fieldName: 'responsetime',
+ type: ML_JOB_FIELD_TYPES.NUMBER,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ statsMaxDecimalPlaces: 3,
+ topValuesCount: 10,
+ viewableInLens: true,
+ },
+ ],
+ nonMetricFields: [
+ {
+ fieldName: '@timestamp',
+ type: ML_JOB_FIELD_TYPES.DATE,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ exampleCount: 2,
+ viewableInLens: true,
+ },
+ {
+ fieldName: '@version',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: '@version.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'airline',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 10,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'type',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: 'type.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ ],
+ emptyFields: ['sourcetype'],
+ visibleMetricFieldsCount: 1,
+ totalMetricFieldsCount: 1,
+ populatedFieldsCount: 7,
+ totalFieldsCount: 8,
+ fieldNameFiltersResultCount: 2,
+ fieldTypeFiltersResultCount: 3,
+ },
+};
+
+export const farequoteKQLSearchTestData: TestData = {
+ suiteTitle: 'KQL saved search',
+ isSavedSearch: true,
+ sourceIndexOrSavedSearch: 'ft_farequote_kuery',
+ fieldNameFilters: ['@version'],
+ fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT],
+ sampleSizeValidations: [
+ { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
+ { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
+ ],
+ expected: {
+ totalDocCountFormatted: '34,415',
+ metricFields: [
+ {
+ fieldName: 'responsetime',
+ type: ML_JOB_FIELD_TYPES.NUMBER,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ statsMaxDecimalPlaces: 3,
+ topValuesCount: 10,
+ viewableInLens: true,
+ },
+ ],
+ nonMetricFields: [
+ {
+ fieldName: '@timestamp',
+ type: ML_JOB_FIELD_TYPES.DATE,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ exampleCount: 2,
+ viewableInLens: true,
+ },
+ {
+ fieldName: '@version',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: '@version.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'airline',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 5,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'type',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: 'type.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ ],
+ emptyFields: ['sourcetype'],
+ visibleMetricFieldsCount: 1,
+ totalMetricFieldsCount: 1,
+ populatedFieldsCount: 7,
+ totalFieldsCount: 8,
+ fieldNameFiltersResultCount: 1,
+ fieldTypeFiltersResultCount: 3,
+ },
+};
+
+export const farequoteKQLFiltersSearchTestData: TestData = {
+ suiteTitle: 'KQL saved search and filters',
+ isSavedSearch: true,
+ sourceIndexOrSavedSearch: 'ft_farequote_filter_and_kuery',
+ fieldNameFilters: ['@version'],
+ fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT],
+ sampleSizeValidations: [
+ { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
+ { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
+ ],
+ expected: {
+ totalDocCountFormatted: '5,674',
+ metricFields: [
+ {
+ fieldName: 'responsetime',
+ type: ML_JOB_FIELD_TYPES.NUMBER,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ statsMaxDecimalPlaces: 3,
+ topValuesCount: 10,
+ viewableInLens: true,
+ },
+ ],
+ nonMetricFields: [
+ {
+ fieldName: '@timestamp',
+ type: ML_JOB_FIELD_TYPES.DATE,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ exampleCount: 2,
+ viewableInLens: true,
+ },
+ {
+ fieldName: '@version',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: '@version.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'airline',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ exampleContent: ['ASA'],
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'type',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: 'type.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ ],
+ emptyFields: ['sourcetype'],
+ visibleMetricFieldsCount: 1,
+ totalMetricFieldsCount: 1,
+ populatedFieldsCount: 7,
+ totalFieldsCount: 8,
+ fieldNameFiltersResultCount: 1,
+ fieldTypeFiltersResultCount: 3,
+ },
+};
+
+export const farequoteLuceneSearchTestData: TestData = {
+ suiteTitle: 'lucene saved search',
+ isSavedSearch: true,
+ sourceIndexOrSavedSearch: 'ft_farequote_lucene',
+ fieldNameFilters: ['@version.keyword', 'type'],
+ fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER],
+ sampleSizeValidations: [
+ { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
+ { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
+ ],
+ expected: {
+ totalDocCountFormatted: '34,416',
+ metricFields: [
+ {
+ fieldName: 'responsetime',
+ type: ML_JOB_FIELD_TYPES.NUMBER,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ statsMaxDecimalPlaces: 3,
+ topValuesCount: 10,
+ viewableInLens: true,
+ },
+ ],
+ nonMetricFields: [
+ {
+ fieldName: '@timestamp',
+ type: ML_JOB_FIELD_TYPES.DATE,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ exampleCount: 2,
+ viewableInLens: true,
+ },
+ {
+ fieldName: '@version',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: '@version.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'airline',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 5,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'type',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: 'type.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ ],
+ emptyFields: ['sourcetype'],
+ visibleMetricFieldsCount: 1,
+ totalMetricFieldsCount: 1,
+ populatedFieldsCount: 7,
+ totalFieldsCount: 8,
+ fieldNameFiltersResultCount: 2,
+ fieldTypeFiltersResultCount: 1,
+ },
+};
+
+export const farequoteLuceneFiltersSearchTestData: TestData = {
+ suiteTitle: 'lucene saved search and filter',
+ isSavedSearch: true,
+ sourceIndexOrSavedSearch: 'ft_farequote_filter_and_lucene',
+ fieldNameFilters: ['@version.keyword', 'type'],
+ fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER],
+ sampleSizeValidations: [
+ { size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
+ { size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
+ ],
+ expected: {
+ totalDocCountFormatted: '5,673',
+ metricFields: [
+ {
+ fieldName: 'responsetime',
+ type: ML_JOB_FIELD_TYPES.NUMBER,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ statsMaxDecimalPlaces: 3,
+ topValuesCount: 10,
+ viewableInLens: true,
+ },
+ ],
+ nonMetricFields: [
+ {
+ fieldName: '@timestamp',
+ type: ML_JOB_FIELD_TYPES.DATE,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '5000 (100%)',
+ exampleCount: 2,
+ viewableInLens: true,
+ },
+ {
+ fieldName: '@version',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: '@version.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'airline',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ exampleContent: ['ASA'],
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ {
+ fieldName: 'type',
+ type: ML_JOB_FIELD_TYPES.TEXT,
+ existsInDocs: true,
+ aggregatable: false,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '',
+ viewableInLens: false,
+ },
+ {
+ fieldName: 'type.keyword',
+ type: ML_JOB_FIELD_TYPES.KEYWORD,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ exampleCount: 1,
+ docCountFormatted: '5000 (100%)',
+ viewableInLens: true,
+ },
+ ],
+ emptyFields: ['sourcetype'],
+ visibleMetricFieldsCount: 1,
+ totalMetricFieldsCount: 1,
+ populatedFieldsCount: 7,
+ totalFieldsCount: 8,
+ fieldNameFiltersResultCount: 2,
+ fieldTypeFiltersResultCount: 1,
+ },
+};
+
+export const sampleLogTestData: TestData = {
+ suiteTitle: 'geo point field',
+ isSavedSearch: false,
+ sourceIndexOrSavedSearch: 'ft_module_sample_logs',
+ fieldNameFilters: ['geo.coordinates'],
+ fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT],
+ rowsPerPage: 50,
+ expected: {
+ totalDocCountFormatted: '408',
+ metricFields: [],
+ // only testing the geo_point fields
+ nonMetricFields: [
+ {
+ fieldName: 'geo.coordinates',
+ type: ML_JOB_FIELD_TYPES.GEO_POINT,
+ existsInDocs: true,
+ aggregatable: true,
+ loading: false,
+ docCountFormatted: '408 (100%)',
+ exampleCount: 10,
+ viewableInLens: false,
+ },
+ ],
+ emptyFields: [],
+ visibleMetricFieldsCount: 4,
+ totalMetricFieldsCount: 5,
+ populatedFieldsCount: 35,
+ totalFieldsCount: 36,
+ fieldNameFiltersResultCount: 1,
+ fieldTypeFiltersResultCount: 1,
+ },
+ sampleSizeValidations: [
+ { size: 1000, expected: { field: 'geo.coordinates', docCountFormatted: '408 (100%)' } },
+ { size: 5000, expected: { field: '@timestamp', docCountFormatted: '408 (100%)' } },
+ ],
+};
diff --git a/x-pack/test/functional/apps/ml/data_visualizer/types.ts b/x-pack/test/functional/apps/ml/data_visualizer/types.ts
new file mode 100644
index 00000000000000..5c3f890dba5612
--- /dev/null
+++ b/x-pack/test/functional/apps/ml/data_visualizer/types.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types';
+
+export interface MetricFieldVisConfig extends FieldVisConfig {
+ statsMaxDecimalPlaces: number;
+ docCountFormatted: string;
+ topValuesCount: number;
+ viewableInLens: boolean;
+}
+
+export interface NonMetricFieldVisConfig extends FieldVisConfig {
+ docCountFormatted: string;
+ exampleCount: number;
+ exampleContent?: string[];
+ viewableInLens: boolean;
+}
+
+export interface TestData {
+ suiteTitle: string;
+ isSavedSearch?: boolean;
+ sourceIndexOrSavedSearch: string;
+ fieldNameFilters: string[];
+ fieldTypeFilters: string[];
+ rowsPerPage?: 10 | 25 | 50;
+ sampleSizeValidations: Array<{
+ size: number;
+ expected: { field: string; docCountFormatted: string };
+ }>;
+ expected: {
+ totalDocCountFormatted: string;
+ metricFields?: MetricFieldVisConfig[];
+ nonMetricFields?: NonMetricFieldVisConfig[];
+ emptyFields: string[];
+ visibleMetricFieldsCount: number;
+ totalMetricFieldsCount: number;
+ populatedFieldsCount: number;
+ totalFieldsCount: number;
+ fieldNameFiltersResultCount: number;
+ fieldTypeFiltersResultCount: number;
+ };
+}
diff --git a/x-pack/test/functional/services/ml/custom_urls.ts b/x-pack/test/functional/services/ml/custom_urls.ts
index 5b2bf0773719c3..3d26236741a8aa 100644
--- a/x-pack/test/functional/services/ml/custom_urls.ts
+++ b/x-pack/test/functional/services/ml/custom_urls.ts
@@ -169,7 +169,10 @@ export function MachineLearningCustomUrlsProvider({
async assertDiscoverCustomUrlAction(expectedHitCountFormatted: string) {
await PageObjects.discover.waitForDiscoverAppOnScreen();
- await retry.tryForTime(5000, async () => {
+ // During cloud tests, the small browser width might cause hit count to be invisible
+ // so temporarily collapsing the sidebar ensures the count shows
+ await PageObjects.discover.closeSidebar();
+ await retry.tryForTime(10 * 1000, async () => {
const hitCount = await PageObjects.discover.getHitCount();
expect(hitCount).to.eql(
expectedHitCountFormatted,
diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts
index 8094f0ad1f8d2c..860f2bd86bec73 100644
--- a/x-pack/test/functional/services/ml/data_visualizer_table.ts
+++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts
@@ -361,7 +361,27 @@ export function MachineLearningDataVisualizerTableProvider(
});
}
- public async assertTopValuesContents(fieldName: string, expectedTopValuesCount: number) {
+ public async assertTopValuesContent(fieldName: string, expectedTopValues: string[]) {
+ const selector = this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent');
+ const topValuesElement = await testSubjects.find(selector);
+ const topValuesBars = await topValuesElement.findAllByTestSubject(
+ 'dataVisualizerFieldDataTopValueBar'
+ );
+
+ const topValuesBarsValues = await Promise.all(
+ topValuesBars.map(async (bar) => {
+ const visibleText = await bar.getVisibleText();
+ return visibleText ? visibleText.split('\n')[0] : undefined;
+ })
+ );
+
+ expect(topValuesBarsValues).to.eql(
+ expectedTopValues,
+ `Expected top values for field '${fieldName}' to equal '${expectedTopValues}' (got '${topValuesBarsValues}')`
+ );
+ }
+
+ public async assertTopValuesCount(fieldName: string, expectedTopValuesCount: number) {
const selector = this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent');
const topValuesElement = await testSubjects.find(selector);
const topValuesBars = await topValuesElement.findAllByTestSubject(
@@ -401,7 +421,7 @@ export function MachineLearningDataVisualizerTableProvider(
await testSubjects.existOrFail(
this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValues')
);
- await this.assertTopValuesContents(fieldName, topValuesCount);
+ await this.assertTopValuesCount(fieldName, topValuesCount);
if (checkDistributionPreviewExist) {
await this.assertDistributionPreviewExist(fieldName);
@@ -433,7 +453,8 @@ export function MachineLearningDataVisualizerTableProvider(
public async assertKeywordFieldContents(
fieldName: string,
docCountFormatted: string,
- topValuesCount: number
+ topValuesCount: number,
+ exampleContent?: string[]
) {
await this.assertRowExists(fieldName);
await this.assertFieldDocCount(fieldName, docCountFormatted);
@@ -442,7 +463,11 @@ export function MachineLearningDataVisualizerTableProvider(
await testSubjects.existOrFail(
this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent')
);
- await this.assertTopValuesContents(fieldName, topValuesCount);
+ await this.assertTopValuesCount(fieldName, topValuesCount);
+
+ if (exampleContent) {
+ await this.assertTopValuesContent(fieldName, exampleContent);
+ }
await this.ensureDetailsClosed(fieldName);
}
@@ -508,13 +533,19 @@ export function MachineLearningDataVisualizerTableProvider(
docCountFormatted: string,
exampleCount: number,
viewableInLens: boolean,
- hasActionMenu?: boolean
+ hasActionMenu?: boolean,
+ exampleContent?: string[]
) {
// Currently the data used in the data visualizer tests only contains these field types.
if (fieldType === ML_JOB_FIELD_TYPES.DATE) {
await this.assertDateFieldContents(fieldName, docCountFormatted);
} else if (fieldType === ML_JOB_FIELD_TYPES.KEYWORD) {
- await this.assertKeywordFieldContents(fieldName, docCountFormatted, exampleCount);
+ await this.assertKeywordFieldContents(
+ fieldName,
+ docCountFormatted,
+ exampleCount,
+ exampleContent
+ );
} else if (fieldType === ML_JOB_FIELD_TYPES.TEXT) {
await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount);
} else if (fieldType === ML_JOB_FIELD_TYPES.GEO_POINT) {
diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts
index 57a44a0b7952da..4d38e6a144a787 100644
--- a/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts
+++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts
@@ -19,6 +19,11 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(
require.resolve('../../../../functional/apps/ml/data_visualizer/index_data_visualizer')
);
+ loadTestFile(
+ require.resolve(
+ '../../../../functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover'
+ )
+ );
loadTestFile(require.resolve('./index_data_visualizer_actions_panel'));
});
}