From 70d61436bc53ba80e46652026aa9554fb13c9eae Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 5 Feb 2021 11:58:57 -0600 Subject: [PATCH] [ML] Add Lens and Discover integration to index based Data Visualizer (#89471) --- x-pack/plugins/ml/kibana.json | 3 +- x-pack/plugins/ml/public/application/app.tsx | 1 + .../data_recognizer/data_recognizer.d.ts | 4 +- .../contexts/kibana/kibana_context.ts | 4 +- .../index_based/common/combined_query.ts | 11 + .../index_based/common/index.ts | 1 + .../actions_panel/actions_panel.tsx | 219 +++++++++---- .../components/expanded_row/expanded_row.tsx | 7 +- .../expanded_row/geo_point_content.tsx | 11 +- .../field_data_row/action_menu/actions.ts | 49 +++ .../field_data_row/action_menu/index.ts | 8 + .../field_data_row/action_menu/lens_utils.ts | 288 ++++++++++++++++++ .../datavisualizer/index_based/page.tsx | 53 ++-- .../data_visualizer_stats_table.tsx | 12 +- x-pack/plugins/ml/public/plugin.ts | 3 + x-pack/plugins/ml/tsconfig.json | 1 + .../data_visualizer/file_data_visualizer.ts | 4 +- .../data_visualizer/index_data_visualizer.ts | 30 +- .../index_data_visualizer_actions_panel.ts | 36 ++- .../apps/ml/permissions/full_ml_access.ts | 7 +- .../apps/ml/permissions/read_ml_access.ts | 12 +- .../ml/data_visualizer_index_based.ts | 40 +++ .../services/ml/data_visualizer_table.ts | 29 +- .../apps/ml/data_visualizer/index.ts | 3 +- .../index_data_visualizer_actions_panel.ts | 57 ++++ .../apps/ml/permissions/full_ml_access.ts | 7 +- .../apps/ml/permissions/read_ml_access.ts | 7 +- 27 files changed, 805 insertions(+), 102 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/common/combined_query.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts create mode 100644 x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index ede6b8abbd09..a73a68445a39 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -25,7 +25,8 @@ "spaces", "management", "licenseManagement", - "maps" + "maps", + "lens" ], "server": true, "ui": true, diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 44558fb9dcfe..0199e13e93d8 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -77,6 +77,7 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { data: deps.data, security: deps.security, licenseManagement: deps.licenseManagement, + lens: deps.lens, storage: localStorage, embeddable: deps.embeddable, maps: deps.maps, diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts b/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts index c47e21222097..ff6363ea2cc6 100644 --- a/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts +++ b/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts @@ -7,10 +7,10 @@ import { FC } from 'react'; import { SavedSearchSavedObject } from '../../../../common/types/kibana'; -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; +import type { IIndexPattern } from '../../../../../../../src/plugins/data/public'; declare const DataRecognizer: FC<{ - indexPattern: IndexPattern; + indexPattern: IIndexPattern; savedSearch: SavedSearchSavedObject | null; results: { count: number; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index a8df8f8174bd..1dd30d5d9933 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -17,7 +17,8 @@ import { SharePluginStart } from '../../../../../../../src/plugins/share/public' import { MlServicesContext } from '../../app'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; -import { MapsStartApi } from '../../../../../maps/public'; +import type { MapsStartApi } from '../../../../../maps/public'; +import type { LensPublicStart } from '../../../../../lens/public'; interface StartPlugins { data: DataPublicPluginStart; @@ -26,6 +27,7 @@ interface StartPlugins { share: SharePluginStart; embeddable: EmbeddableStart; maps?: MapsStartApi; + lens?: LensPublicStart; } export type StartServices = CoreStart & StartPlugins & { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/combined_query.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/combined_query.ts new file mode 100644 index 000000000000..7723277959b1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/combined_query.ts @@ -0,0 +1,11 @@ +/* + * 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 interface CombinedQuery { + searchString: string | { [key: string]: any }; + searchQueryLanguage: string; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts index 50a67b946e52..fe99a6343279 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts @@ -6,3 +6,4 @@ */ export { FieldHistogramRequestConfig, FieldRequestConfig } from './request'; +export type { CombinedQuery } from './combined_query'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 9dd455427b74..255dfcc21cca 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -5,23 +5,51 @@ * 2.0. */ -import React, { FC, useState } from 'react'; +import React, { FC, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; +import { + EuiSpacer, + EuiText, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiCard, + EuiIcon, +} from '@elastic/eui'; import { Link } from 'react-router-dom'; -import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { DataRecognizer } from '../../../../components/data_recognizer'; import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; +import { + DISCOVER_APP_URL_GENERATOR, + DiscoverUrlGeneratorState, +} from '../../../../../../../../../src/plugins/discover/public'; +import { useMlKibana } from '../../../../contexts/kibana'; +import { isFullLicense } from '../../../../license'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; +import { mlNodesAvailable } from '../../../../ml_nodes_check'; +import { useUrlState } from '../../../../util/url_state'; +import type { IIndexPattern } from '../../../../../../../../../src/plugins/data/common'; interface Props { - indexPattern: IndexPattern; + indexPattern: IIndexPattern; + searchString?: string | { [key: string]: any }; + searchQueryLanguage?: string; } -export const ActionsPanel: FC = ({ indexPattern }) => { +export const ActionsPanel: FC = ({ indexPattern, searchString, searchQueryLanguage }) => { const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); + const [discoverLink, setDiscoverLink] = useState(''); + const { + services: { + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = useMlKibana(); + const [globalState] = useUrlState('_g'); const recognizerResults = { count: 0, @@ -29,63 +57,146 @@ export const ActionsPanel: FC = ({ indexPattern }) => { setRecognizerResultsCount(recognizerResults.count); }, }; + const showCreateJob = + isFullLicense() && + checkPermission('canCreateJob') && + mlNodesAvailable() && + indexPattern.timeFieldName !== undefined; const createJobLink = `/${ML_PAGES.ANOMALY_DETECTION_CREATE_JOB}/advanced?index=${indexPattern.id}`; + useEffect(() => { + let unmounted = false; + + const indexPatternId = indexPattern.id; + const getDiscoverUrl = async (): Promise => { + const state: DiscoverUrlGeneratorState = { + indexPatternId, + }; + if (searchString && searchQueryLanguage !== undefined) { + state.query = { query: searchString, language: searchQueryLanguage }; + } + if (globalState?.time) { + state.timeRange = globalState.time; + } + if (globalState?.refreshInterval) { + state.refreshInterval = globalState.refreshInterval; + } + + let discoverUrlGenerator; + try { + discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR); + } catch (error) { + // ignore error thrown when url generator is not available + return; + } + + const discoverUrl = await discoverUrlGenerator.createUrl(state); + if (!unmounted) { + setDiscoverLink(discoverUrl); + } + }; + getDiscoverUrl(); + return () => { + unmounted = true; + }; + }, [indexPattern, searchString, searchQueryLanguage, globalState]); + // Note we use display:none for the DataRecognizer section as it needs to be // passed the recognizerResults object, and then run the recognizer check which // controls whether the recognizer section is ultimately displayed. return (
- -

- -

-
- -
- -

- + +

+ +

+ + + + +

+ +

+
+ + + + + + + )} + + {discoverLink && ( + <> + +

+ +

+
+ + + } + description={i18n.translate( + 'xpack.ml.datavisualizer.actionsPanel.viewIndexInDiscoverDescription', + { + defaultMessage: 'Explore index in Discover', + } + )} + title={ + + } + href={discoverLink} /> -

-
- - - - - -
- -

- -

-
- - - - + + + )}
); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx index 96531de23fa4..8a0656abe95c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { LoadingIndicator } from '../field_data_row/loading_indicator'; import { NotInDocsContent } from '../field_data_row/content_types'; -import { FieldVisConfig } from '../../../stats_table/types'; import { BooleanContent, DateContent, @@ -20,8 +19,10 @@ import { OtherContent, TextContent, } from '../../../stats_table/components/field_data_expanded_row'; -import { CombinedQuery, GeoPointContent } from './geo_point_content'; -import { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { GeoPointContent } from './geo_point_content'; +import type { CombinedQuery } from '../../common'; +import type { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import type { FieldVisConfig } from '../../../stats_table/types'; export const IndexBasedDataVisualizerExpandedRow = ({ item, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx index cea65edbfb55..33b347b4da80 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx @@ -9,20 +9,17 @@ import React, { FC, useEffect, useState } from 'react'; import { EuiFlexItem } from '@elastic/eui'; import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; -import { FieldVisConfig } from '../../../stats_table/types'; -import { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; import { MlEmbeddedMapComponent } from '../../../../components/ml_embedded_map'; import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { ES_GEO_FIELD_TYPE } from '../../../../../../../maps/common/constants'; -import { LayerDescriptor } from '../../../../../../../maps/common/descriptor_types'; import { useMlKibana } from '../../../../contexts/kibana'; import { DocumentStatsTable } from '../../../stats_table/components/field_data_expanded_row/document_stats'; import { ExpandedRowContent } from '../../../stats_table/components/field_data_expanded_row/expanded_row_content'; +import type { CombinedQuery } from '../../common'; +import type { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import type { LayerDescriptor } from '../../../../../../../maps/common/descriptor_types'; +import type { FieldVisConfig } from '../../../stats_table/types'; -export interface CombinedQuery { - searchString: string | { [key: string]: any }; - searchQueryLanguage: string; -} export const GeoPointContent: FC<{ config: FieldVisConfig; indexPattern: IndexPattern | undefined; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts new file mode 100644 index 000000000000..57675927ce81 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts @@ -0,0 +1,49 @@ +/* + * 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 { Action } from '@elastic/eui/src/components/basic_table/action_types'; +import { getCompatibleLensDataType, getLensAttributes } from './lens_utils'; +import type { CombinedQuery } from '../../../common'; +import type { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +import type { LensPublicStart } from '../../../../../../../../lens/public'; +import type { FieldVisConfig } from '../../../../stats_table/types'; + +export function getActions( + indexPattern: IIndexPattern, + lensPlugin: LensPublicStart, + combinedQuery: CombinedQuery +): Array> { + const canUseLensEditor = lensPlugin.canUseEditor(); + return [ + { + name: i18n.translate('xpack.ml.dataVisualizer.indexBasedDataGrid.exploreInLensTitle', { + defaultMessage: 'Explore in Lens', + }), + description: i18n.translate( + 'xpack.ml.dataVisualizer.indexBasedDataGrid.exploreInLensDescription', + { + defaultMessage: 'Explore in Lens', + } + ), + type: 'icon', + icon: 'lensApp', + available: (item: FieldVisConfig) => + getCompatibleLensDataType(item.type) !== undefined && canUseLensEditor, + onClick: (item: FieldVisConfig) => { + const lensAttributes = getLensAttributes(indexPattern, combinedQuery, item); + if (lensAttributes) { + lensPlugin.navigateToPrefilledEditor({ + id: `ml-dataVisualizer-${item.fieldName}`, + attributes: lensAttributes, + }); + } + }, + 'data-test-subj': 'mlActionButtonViewInLens', + }, + ]; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/index.ts new file mode 100644 index 000000000000..df36cc89ce91 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/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 { getActions } from './actions'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts new file mode 100644 index 000000000000..8d078b59ad77 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts @@ -0,0 +1,288 @@ +/* + * 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 { ML_JOB_FIELD_TYPES } from '../../../../../../../common/constants/field_types'; +import type { TypedLensByValueInput } from '../../../../../../../../lens/public'; +import type { FieldVisConfig } from '../../../../stats_table/types'; +import type { IndexPatternColumn, XYLayerConfig } from '../../../../../../../../lens/public'; +import type { CombinedQuery } from '../../../common'; +import type { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +interface ColumnsAndLayer { + columns: Record; + layer: XYLayerConfig; +} + +const TOP_VALUES_LABEL = i18n.translate('xpack.ml.dataVisualizer.lensChart.topValuesLabel', { + defaultMessage: 'Top values', +}); +const COUNT = i18n.translate('xpack.ml.dataVisualizer.lensChart.countLabel', { + defaultMessage: 'Count', +}); + +export function getNumberSettings(item: FieldVisConfig, defaultIndexPattern: IIndexPattern) { + // if index has no timestamp field + if (defaultIndexPattern.timeFieldName === undefined) { + const columns: Record = { + col1: { + label: item.fieldName!, + dataType: 'number', + isBucketed: true, + operationType: 'range', + params: { + type: 'histogram', + maxBars: 'auto', + ranges: [], + }, + sourceField: item.fieldName!, + }, + col2: { + label: COUNT, + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }; + + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'bar', + xAccessor: 'col1', + }; + return { columns, layer }; + } + + const columns: Record = { + col2: { + dataType: 'number', + isBucketed: false, + label: i18n.translate('xpack.ml.dataVisualizer.lensChart.averageOfLabel', { + defaultMessage: 'Average of {fieldName}', + values: { fieldName: item.fieldName }, + }), + operationType: 'avg', + sourceField: item.fieldName!, + }, + col1: { + dataType: 'date', + isBucketed: true, + label: defaultIndexPattern.timeFieldName!, + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + sourceField: defaultIndexPattern.timeFieldName!, + }, + }; + + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'line', + xAccessor: 'col1', + }; + + return { columns, layer }; +} +export function getDateSettings(item: FieldVisConfig) { + const columns: Record = { + col2: { + dataType: 'number', + isBucketed: false, + label: COUNT, + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + col1: { + dataType: 'date', + isBucketed: true, + label: item.fieldName!, + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + sourceField: item.fieldName!, + }, + }; + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'line', + xAccessor: 'col1', + }; + + return { columns, layer }; +} + +export function getKeywordSettings(item: FieldVisConfig) { + const columns: Record = { + col1: { + label: TOP_VALUES_LABEL, + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col2' }, + size: 10, + orderDirection: 'desc', + }, + sourceField: item.fieldName!, + }, + col2: { + label: COUNT, + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }; + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'bar', + xAccessor: 'col1', + }; + + return { columns, layer }; +} + +export function getBooleanSettings(item: FieldVisConfig) { + const columns: Record = { + col1: { + label: TOP_VALUES_LABEL, + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 2, + orderDirection: 'desc', + }, + sourceField: item.fieldName!, + }, + col2: { + label: COUNT, + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }; + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'bar', + xAccessor: 'col1', + }; + + return { columns, layer }; +} + +export function getCompatibleLensDataType(type: FieldVisConfig['type']): string | undefined { + let lensType: string | undefined; + switch (type) { + case ML_JOB_FIELD_TYPES.KEYWORD: + lensType = 'string'; + break; + case ML_JOB_FIELD_TYPES.DATE: + lensType = 'date'; + break; + case ML_JOB_FIELD_TYPES.NUMBER: + lensType = 'number'; + break; + case ML_JOB_FIELD_TYPES.IP: + lensType = 'ip'; + break; + case ML_JOB_FIELD_TYPES.BOOLEAN: + lensType = 'string'; + break; + default: + lensType = undefined; + } + return lensType; +} + +function getColumnsAndLayer( + fieldType: FieldVisConfig['type'], + item: FieldVisConfig, + defaultIndexPattern: IIndexPattern +): ColumnsAndLayer | undefined { + if (item.fieldName === undefined) return; + + if (fieldType === ML_JOB_FIELD_TYPES.DATE) { + return getDateSettings(item); + } + if (fieldType === ML_JOB_FIELD_TYPES.NUMBER) { + return getNumberSettings(item, defaultIndexPattern); + } + if (fieldType === ML_JOB_FIELD_TYPES.IP || fieldType === ML_JOB_FIELD_TYPES.KEYWORD) { + return getKeywordSettings(item); + } + if (fieldType === ML_JOB_FIELD_TYPES.BOOLEAN) { + return getBooleanSettings(item); + } +} +// Get formatted Lens visualization format depending on field type +// currently only supports the following types: +// 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip' +export function getLensAttributes( + defaultIndexPattern: IIndexPattern | undefined, + combinedQuery: CombinedQuery, + item: FieldVisConfig +): TypedLensByValueInput['attributes'] | undefined { + if (defaultIndexPattern === undefined || item.type === undefined || item.fieldName === undefined) + return; + + const presets = getColumnsAndLayer(item.type, item, defaultIndexPattern); + + if (!presets) return; + + return { + visualizationType: 'lnsXY', + title: i18n.translate('xpack.ml.dataVisualizer.lensChart.chartTitle', { + defaultMessage: 'Lens for {fieldName}', + values: { fieldName: item.fieldName }, + }), + references: [ + { + id: defaultIndexPattern.id!, + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: defaultIndexPattern.id!, + name: 'indexpattern-datasource-layer-layer1', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer1: { + columnOrder: ['col1', 'col2'], + columns: presets.columns, + }, + }, + }, + }, + filters: [], + query: { language: combinedQuery.searchQueryLanguage, query: combinedQuery.searchString }, + visualization: { + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + layers: [presets.layer], + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + valueLabels: 'hide', + }, + }, + }; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 6ea85c354d88..6bc1970bc615 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -19,6 +19,8 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; +import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types'; +import { FormattedMessage } from '@kbn/i18n/react'; import { IFieldType, KBN_FIELD_TYPES, @@ -32,9 +34,6 @@ import { NavigationMenu } from '../../components/navigation_menu'; import { DatePickerWrapper } from '../../components/navigation_menu/date_picker_wrapper'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../../../common/constants/search'; -import { isFullLicense } from '../../license'; -import { checkPermission } from '../../capabilities/check_capabilities'; -import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useMlContext } from '../../contexts/ml'; @@ -63,6 +62,7 @@ import type { MetricFieldsStats, TotalFieldsStats, } from '../stats_table/components/field_count_stats'; +import { getActions } from './components/field_data_row/action_menu/actions'; interface DataVisualizerPageState { overallStats: OverallStats; @@ -116,6 +116,10 @@ export const getDefaultDataVisualizerListState = (): Required { const mlContext = useMlContext(); const restorableDefaults = getDefaultDataVisualizerListState(); + const { + services: { lens: lensPlugin, docLinks }, + } = useMlKibana(); + const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState( ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, restorableDefaults @@ -167,12 +171,6 @@ export const Page: FC = () => { const defaults = getDefaultPageState(); - const showActionsPanel = - isFullLicense() && - checkPermission('canCreateJob') && - mlNodesAvailable() && - currentIndexPattern.timeFieldName !== undefined; - const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { const searchData = extractSearchData(currentSavedSearch); if (searchData === undefined || dataVisualizerListState.searchString !== '') { @@ -686,9 +684,27 @@ export const Page: FC = () => { [currentIndexPattern, searchQuery] ); - const { - services: { docLinks }, - } = useMlKibana(); + // Inject custom action column for the index based visualizer + const extendedColumns = useMemo(() => { + if (lensPlugin === undefined) { + // eslint-disable-next-line no-console + console.error('Lens plugin not available'); + return; + } + const actionColumn: EuiTableActionsColumnType = { + name: ( + + ), + actions: getActions(currentIndexPattern, lensPlugin, { searchQueryLanguage, searchString }), + width: '100px', + }; + + return [actionColumn]; + }, [currentIndexPattern, lensPlugin, searchQueryLanguage, searchString]); + const helpLink = docLinks.links.ml.guide; return ( @@ -766,14 +782,17 @@ export const Page: FC = () => { pageState={dataVisualizerListState} updatePageState={setDataVisualizerListState} getItemIdToExpandedRowMap={getItemIdToExpandedRowMap} + extendedColumns={extendedColumns} /> - {showActionsPanel === true && ( - - - - )} + + + diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/data_visualizer_stats_table.tsx index 82e807fa61e6..2a6a681c6321 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/data_visualizer_stats_table.tsx @@ -9,6 +9,7 @@ import React, { useMemo, useState } from 'react'; import { CENTER_ALIGNMENT, + EuiBasicTableColumn, EuiButtonIcon, EuiFlexItem, EuiIcon, @@ -52,6 +53,7 @@ interface DataVisualizerTableProps { update: Partial ) => void; getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap; + extendedColumns?: Array>; } export const DataVisualizerTable = ({ @@ -59,11 +61,12 @@ export const DataVisualizerTable = ({ pageState, updatePageState, getItemIdToExpandedRowMap, + extendedColumns, }: DataVisualizerTableProps) => { const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [expandAll, toggleExpandAll] = useState(false); - const { onTableChange, pagination, sorting } = useTableSettings( + const { onTableChange, pagination, sorting } = useTableSettings( items, pageState, updatePageState @@ -136,7 +139,7 @@ export const DataVisualizerTable = ({ 'data-test-subj': 'mlDataVisualizerTableColumnDetailsToggle', }; - return [ + const baseColumns = [ expanderColumn, { field: 'type', @@ -236,7 +239,8 @@ export const DataVisualizerTable = ({ 'data-test-subj': 'mlDataVisualizerTableColumnDistribution', }, ]; - }, [expandAll, showDistributions, updatePageState]); + return extendedColumns ? [...baseColumns, ...extendedColumns] : baseColumns; + }, [expandAll, showDistributions, updatePageState, extendedColumns]); const itemIdToExpandedRowMap = useMemo(() => { let itemIds = expandedRowItemIds; @@ -248,7 +252,7 @@ export const DataVisualizerTable = ({ return ( - + className={'mlDataVisualizer'} items={items} itemId={FIELD_NAME} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index bfbc04943273..9fd245a7e16b 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -46,6 +46,7 @@ import { registerFeature } from './register_feature'; // Not importing from `ml_url_generator/index` here to avoid importing unnecessary code import { registerUrlGenerator } from './ml_url_generator/ml_url_generator'; import type { MapsStartApi } from '../../maps/public'; +import { LensPublicStart } from '../../lens/public'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -55,6 +56,7 @@ export interface MlStartDependencies { spaces?: SpacesPluginStart; embeddable: EmbeddableStart; maps?: MapsStartApi; + lens?: LensPublicStart; } export interface MlSetupDependencies { security?: SecurityPluginSetup; @@ -106,6 +108,7 @@ export class MlPlugin implements Plugin { embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable }, maps: pluginsStart.maps, uiActions: pluginsStart.uiActions, + lens: pluginsStart.lens, kibanaVersion, }, params diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 113bcbe71047..2caf88de1b76 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -28,6 +28,7 @@ { "path": "../license_management/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, + { "path": "../lens/tsconfig.json" }, { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, ] diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index c09bb0c55532..65bc68db25aa 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -222,6 +222,7 @@ export default function ({ getService }: FtrProviderContext) { fieldRow.fieldName, fieldRow.docCountFormatted, fieldRow.topValuesCount, + false, false ); } @@ -230,7 +231,8 @@ export default function ({ getService }: FtrProviderContext) { fieldRow.type, fieldRow.fieldName!, fieldRow.docCountFormatted, - fieldRow.exampleCount + fieldRow.exampleCount, + false ); } 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 ffd22dd176ed..609cf05dad54 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 @@ -13,11 +13,13 @@ interface MetricFieldVisConfig extends FieldVisConfig { statsMaxDecimalPlaces: number; docCountFormatted: string; topValuesCount: number; + viewableInLens: boolean; } interface NonMetricFieldVisConfig extends FieldVisConfig { docCountFormatted: string; exampleCount: number; + viewableInLens: boolean; } interface TestData { @@ -69,6 +71,7 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, topValuesCount: 10, + viewableInLens: true, }, ], nonMetricFields: [ @@ -80,6 +83,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, docCountFormatted: '5000 (100%)', exampleCount: 2, + viewableInLens: true, }, { fieldName: '@version', @@ -89,6 +93,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: '@version.keyword', @@ -98,6 +103,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'airline', @@ -107,6 +113,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 10, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'type', @@ -116,6 +123,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: 'type.keyword', @@ -125,6 +133,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, ], emptyFields: ['sourcetype'], @@ -158,6 +167,7 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, topValuesCount: 10, + viewableInLens: true, }, ], nonMetricFields: [ @@ -169,6 +179,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, docCountFormatted: '5000 (100%)', exampleCount: 2, + viewableInLens: true, }, { fieldName: '@version', @@ -178,6 +189,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: '@version.keyword', @@ -187,6 +199,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'airline', @@ -196,6 +209,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 5, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'type', @@ -205,6 +219,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: 'type.keyword', @@ -214,6 +229,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, ], emptyFields: ['sourcetype'], @@ -247,6 +263,7 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, topValuesCount: 10, + viewableInLens: true, }, ], nonMetricFields: [ @@ -258,6 +275,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, docCountFormatted: '5000 (100%)', exampleCount: 2, + viewableInLens: true, }, { fieldName: '@version', @@ -267,6 +285,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: '@version.keyword', @@ -276,6 +295,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'airline', @@ -285,6 +305,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 5, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'type', @@ -294,6 +315,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: 'type.keyword', @@ -303,6 +325,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, ], emptyFields: ['sourcetype'], @@ -334,6 +357,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, docCountFormatted: '408 (100%)', exampleCount: 10, + viewableInLens: false, }, ], emptyFields: [], @@ -417,7 +441,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataVisualizerTable.assertNumberFieldContents( fieldRow.fieldName, fieldRow.docCountFormatted, - fieldRow.topValuesCount + fieldRow.topValuesCount, + fieldRow.viewableInLens ); } @@ -426,7 +451,8 @@ export default function ({ getService }: FtrProviderContext) { fieldRow.type, fieldRow.fieldName!, fieldRow.docCountFormatted, - fieldRow.exampleCount + fieldRow.exampleCount, + fieldRow.viewableInLens ); } diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts index 6e2e9cfb858c..ce00ee79e907 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts @@ -11,7 +11,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('index based actions panel', function () { + describe('index based actions panel on trial license', function () { this.tags(['mlqa']); const indexPatternName = 'ft_farequote'; @@ -28,6 +28,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded(indexPatternName, '@timestamp'); + await ml.testResources.createSavedSearchFarequoteKueryIfNeeded(); await ml.testResources.setKibanaTimeZoneToUTC(); await ml.securityUI.loginAsMlPowerUser(); @@ -59,5 +60,38 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardAdvanced.assertDatafeedQueryEditorValue(advancedJobWizardDatafeedQuery); }); }); + + describe('view in discover page action', function () { + const savedSearch = 'ft_farequote_kuery'; + const expectedQuery = 'airline: A* and responsetime > 5'; + const docCountFormatted = '34,415'; + + it('loads the source data in the data visualizer', async () => { + await ml.testExecution.logTestStep('loads the data visualizer selector page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep('loads the saved search selection page'); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + + await ml.testExecution.logTestStep('loads the index data visualizer page'); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(savedSearch); + + await ml.testExecution.logTestStep(`loads data for full time range`); + await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); + await ml.dataVisualizerIndexBased.clickUseFullDataButton(docCountFormatted); + }); + + it('navigates to Discover page', async () => { + await ml.testExecution.logTestStep('displays the actions panel with view in Discover card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('retains the query in Discover page'); + await ml.dataVisualizerIndexBased.clickViewInDiscoverButton(); + await ml.dataVisualizerIndexBased.assertDiscoverPageQuery(expectedQuery); + await ml.dataVisualizerIndexBased.assertDiscoverHitCount(docCountFormatted); + }); + }); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 261e0547210f..7b4c646f379d 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -357,8 +357,13 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display the data visualizer table'); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - await ml.testExecution.logTestStep('should display the actions panel with cards'); + await ml.testExecution.logTestStep( + 'should display the actions panel with Discover card' + ); await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('should display job cards'); await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardExists(); await ml.dataVisualizerIndexBased.assertRecognizerCardExists(ecExpectedModuleId); }); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 98b743192c16..69ae3961dfd4 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -99,6 +99,7 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; + const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( __dirname, @@ -349,8 +350,15 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display the data visualizer table'); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - await ml.testExecution.logTestStep('should not display the actions panel'); - await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); + await ml.testExecution.logTestStep( + 'should display the actions panel with Discover card' + ); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('should not display job cards'); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); + await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); }); it('should display elements on File Data Visualizer page correctly', async () => { diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 373b1aa20a4b..d8ec8ed49f01 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -10,9 +10,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataVisualizerIndexBasedProvider({ getService, + getPageObjects, }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const PageObjects = getPageObjects(['discover']); + const queryBar = getService('queryBar'); return { async assertTimeRangeSelectorSectionExists() { @@ -149,5 +152,42 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ async clickCreateAdvancedJobButton() { await testSubjects.clickWhenNotDisabled('mlDataVisualizerCreateAdvancedJobCard'); }, + + async assertViewInDiscoverCardExists() { + await testSubjects.existOrFail('mlDataVisualizerViewInDiscoverCard'); + }, + + async assertViewInDiscoverCardNotExists() { + await testSubjects.missingOrFail('mlDataVisualizerViewInDiscoverCard'); + }, + + async clickViewInDiscoverButton() { + await retry.tryForTime(5000, async () => { + await testSubjects.clickWhenNotDisabled('mlDataVisualizerViewInDiscoverCard'); + await PageObjects.discover.waitForDiscoverAppOnScreen(); + }); + }, + + async assertDiscoverPageQuery(expectedQueryString: string) { + await PageObjects.discover.waitForDiscoverAppOnScreen(); + await retry.tryForTime(5000, async () => { + const queryString = await queryBar.getQueryString(); + expect(queryString).to.eql( + expectedQueryString, + `Expected Discover global query bar to have query '${expectedQueryString}', got '${queryString}'` + ); + }); + }, + + async assertDiscoverHitCount(expectedHitCountFormatted: string) { + await PageObjects.discover.waitForDiscoverAppOnScreen(); + await retry.tryForTime(5000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.eql( + expectedHitCountFormatted, + `Expected Discover hit count to be '${expectedHitCountFormatted}' (got '${hitCount}')` + ); + }); + }, }; } 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 36f5b94dc52d..3bd3b7e2e783 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -133,6 +133,17 @@ export function MachineLearningDataVisualizerTableProvider( ); } + public async assertViewInLensActionEnabled(fieldName: string) { + const actionButton = this.rowSelector(fieldName, 'mlActionButtonViewInLens'); + await testSubjects.existOrFail(actionButton); + await testSubjects.isEnabled(actionButton); + } + + public async assertViewInLensActionNotExists(fieldName: string) { + const actionButton = this.rowSelector(fieldName, 'mlActionButtonViewInLens'); + await testSubjects.missingOrFail(actionButton); + } + public async assertFieldDistinctValuesExist(fieldName: string) { const selector = this.rowSelector(fieldName, 'mlDataVisualizerTableColumnDistinctValues'); await testSubjects.existOrFail(selector); @@ -249,6 +260,7 @@ export function MachineLearningDataVisualizerTableProvider( fieldName: string, docCountFormatted: string, topValuesCount: number, + viewableInLens: boolean, checkDistributionPreviewExist = true ) { await this.assertRowExists(fieldName); @@ -263,6 +275,11 @@ export function MachineLearningDataVisualizerTableProvider( if (checkDistributionPreviewExist) { await this.assertDistributionPreviewExist(fieldName); } + if (viewableInLens) { + await this.assertViewInLensActionEnabled(fieldName); + } else { + await this.assertViewInLensActionNotExists(fieldName); + } await this.ensureDetailsClosed(fieldName); } @@ -307,6 +324,7 @@ export function MachineLearningDataVisualizerTableProvider( ) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); await this.assertExamplesList(fieldName, expectedExamplesCount); @@ -320,6 +338,7 @@ export function MachineLearningDataVisualizerTableProvider( ) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); await this.assertExamplesList(fieldName, expectedExamplesCount); @@ -332,6 +351,7 @@ export function MachineLearningDataVisualizerTableProvider( public async assertUnknownFieldContents(fieldName: string, docCountFormatted: string) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlDVDocumentStatsContent')); @@ -343,7 +363,8 @@ export function MachineLearningDataVisualizerTableProvider( fieldType: string, fieldName: string, docCountFormatted: string, - exampleCount: number + exampleCount: number, + viewableInLens: boolean ) { // Currently the data used in the data visualizer tests only contains these field types. if (fieldType === ML_JOB_FIELD_TYPES.DATE) { @@ -357,6 +378,12 @@ export function MachineLearningDataVisualizerTableProvider( } else if (fieldType === ML_JOB_FIELD_TYPES.UNKNOWN) { await this.assertUnknownFieldContents(fieldName, docCountFormatted); } + + if (viewableInLens) { + await this.assertViewInLensActionEnabled(fieldName); + } else { + await this.assertViewInLensActionNotExists(fieldName); + } } public async ensureNumRowsPerPage(n: 10 | 25 | 50) { 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 007b8be272f5..57a44a0b7952 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 @@ -15,9 +15,10 @@ export default function ({ loadTestFile }: FtrProviderContext) { ); // The data visualizer should work the same as with a trial license, except the missing create actions - // That's why 'index_data_visualizer_actions_panel' is not loaded here + // That's why the 'basic' version of 'index_data_visualizer_actions_panel' is loaded here loadTestFile( require.resolve('../../../../functional/apps/ml/data_visualizer/index_data_visualizer') ); + loadTestFile(require.resolve('./index_data_visualizer_actions_panel')); }); } diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts new file mode 100644 index 000000000000..8a59d6ed3ce2 --- /dev/null +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts @@ -0,0 +1,57 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('index based actions panel on basic license', function () { + this.tags(['mlqa']); + + const indexPatternName = 'ft_farequote'; + const savedSearch = 'ft_farequote_kuery'; + const expectedQuery = 'airline: A* and responsetime > 5'; + + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded(indexPatternName, '@timestamp'); + await ml.testResources.createSavedSearchFarequoteKueryIfNeeded(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + }); + + describe('view in discover page action', function () { + it('loads the source data in the data visualizer', async () => { + await ml.testExecution.logTestStep('loads the data visualizer selector page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep('loads the saved search selection page'); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + + await ml.testExecution.logTestStep('loads the index data visualizer page'); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(savedSearch); + }); + + it('navigates to Discover page', async () => { + await ml.testExecution.logTestStep('should not display create job card'); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); + + await ml.testExecution.logTestStep('displays the actions panel with view in Discover card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('retains the query in Discover page'); + await ml.dataVisualizerIndexBased.clickViewInDiscoverButton(); + await ml.dataVisualizerIndexBased.assertDiscoverPageQuery(expectedQuery); + }); + }); + }); +} diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index 36cc1b1771e8..b09270b1d0f7 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -127,8 +127,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display the data visualizer table'); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - await ml.testExecution.logTestStep('should not display the actions panel with cards'); - await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); + await ml.testExecution.logTestStep('should display the actions panel with Discover card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('should not display job cards'); await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); }); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index f302be40a0e9..14cc4e93b37a 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -127,8 +127,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display the data visualizer table'); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - await ml.testExecution.logTestStep('should not display the actions panel with cards'); - await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); + await ml.testExecution.logTestStep('should display the actions panel with Discover card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('should not display job cards'); await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); });