[ML] Add Lens and Discover integration to index based Data Visualizer (#89471)

This commit is contained in:
Quynh Nguyen 2021-02-05 11:58:57 -06:00 committed by GitHub
parent be53a06925
commit 70d61436bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 805 additions and 102 deletions

View file

@ -25,7 +25,8 @@
"spaces",
"management",
"licenseManagement",
"maps"
"maps",
"lens"
],
"server": true,
"ui": true,

View file

@ -77,6 +77,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
data: deps.data,
security: deps.security,
licenseManagement: deps.licenseManagement,
lens: deps.lens,
storage: localStorage,
embeddable: deps.embeddable,
maps: deps.maps,

View file

@ -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;

View file

@ -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 & {

View file

@ -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;
}

View file

@ -6,3 +6,4 @@
*/
export { FieldHistogramRequestConfig, FieldRequestConfig } from './request';
export type { CombinedQuery } from './combined_query';

View file

@ -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<Props> = ({ indexPattern }) => {
export const ActionsPanel: FC<Props> = ({ 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<Props> = ({ 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<void> => {
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 (
<div data-test-subj="mlDataVisualizerActionsPanel">
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.ml.datavisualizer.actionsPanel.createJobTitle"
defaultMessage="Create Job"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<div style={recognizerResultsCount === 0 ? { display: 'none' } : {}}>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.ml.datavisualizer.actionsPanel.selectKnownConfigurationDescription"
defaultMessage="Select known configurations for recognized data:"
{showCreateJob && (
<>
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.ml.datavisualizer.actionsPanel.createJobTitle"
defaultMessage="Create Job"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<div hidden={recognizerResultsCount === 0}>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.ml.datavisualizer.actionsPanel.selectKnownConfigurationDescription"
defaultMessage="Select known configurations for recognized data:"
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="l" responsive={true} wrap={true}>
<DataRecognizer
indexPattern={indexPattern}
savedSearch={null}
results={recognizerResults}
/>
</EuiFlexGroup>
<EuiSpacer size="l" />
</div>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.ml.datavisualizer.actionsPanel.createJobDescription"
defaultMessage="Use the Advanced job wizard to create a job to find anomalies in this data:"
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<Link to={createJobLink}>
<CreateJobLinkCard
icon="createAdvancedJob"
title={i18n.translate('xpack.ml.datavisualizer.actionsPanel.advancedTitle', {
defaultMessage: 'Advanced',
})}
description={i18n.translate(
'xpack.ml.datavisualizer.actionsPanel.advancedDescription',
{
defaultMessage:
'Use the full range of options to create a job for more advanced use cases',
}
)}
data-test-subj="mlDataVisualizerCreateAdvancedJobCard"
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="l" responsive={true} wrap={true}>
<DataRecognizer
indexPattern={indexPattern}
savedSearch={null}
results={recognizerResults}
/>
</EuiFlexGroup>
<EuiSpacer size="l" />
</div>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.ml.datavisualizer.actionsPanel.createJobDescription"
defaultMessage="Use the Advanced job wizard to create a job to find anomalies in this data:"
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<Link to={createJobLink}>
<CreateJobLinkCard
icon="createAdvancedJob"
title={i18n.translate('xpack.ml.datavisualizer.actionsPanel.advancedTitle', {
defaultMessage: 'Advanced',
})}
description={i18n.translate('xpack.ml.datavisualizer.actionsPanel.advancedDescription', {
defaultMessage:
'Use the full range of options to create a job for more advanced use cases',
})}
data-test-subj="mlDataVisualizerCreateAdvancedJobCard"
/>
</Link>
</Link>
<EuiSpacer size="m" />
</>
)}
{discoverLink && (
<>
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.ml.datavisualizer.actionsPanel.exploreTitle"
defaultMessage="Explore"
/>
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexItem>
<EuiCard
data-test-subj="mlDataVisualizerViewInDiscoverCard"
icon={<EuiIcon size="xxl" type={`discoverApp`} />}
description={i18n.translate(
'xpack.ml.datavisualizer.actionsPanel.viewIndexInDiscoverDescription',
{
defaultMessage: 'Explore index in Discover',
}
)}
title={
<FormattedMessage
id="xpack.ml.datavisualizer.actionsPanel.discoverAppTitle"
defaultMessage="Discover"
/>
}
href={discoverLink}
/>
</EuiFlexItem>
</>
)}
</div>
);
};

View file

@ -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,

View file

@ -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;

View file

@ -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<Action<FieldVisConfig>> {
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',
},
];
}

View file

@ -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';

View file

@ -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<string, IndexPatternColumn>;
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<string, IndexPatternColumn> = {
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<string, IndexPatternColumn> = {
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<string, IndexPatternColumn> = {
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<string, IndexPatternColumn> = {
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<string, IndexPatternColumn> = {
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',
},
},
};
}

View file

@ -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<DataVisualizerInde
export const Page: FC = () => {
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<FieldVisConfig> = {
name: (
<FormattedMessage
id="xpack.ml.dataVisualizer.indexBasedDataGrid.actionsColumnLabel"
defaultMessage="Actions"
/>
),
actions: getActions(currentIndexPattern, lensPlugin, { searchQueryLanguage, searchString }),
width: '100px',
};
return [actionColumn];
}, [currentIndexPattern, lensPlugin, searchQueryLanguage, searchString]);
const helpLink = docLinks.links.ml.guide;
return (
<Fragment>
@ -766,14 +782,17 @@ export const Page: FC = () => {
pageState={dataVisualizerListState}
updatePageState={setDataVisualizerListState}
getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
extendedColumns={extendedColumns}
/>
</EuiPanel>
</EuiFlexItem>
{showActionsPanel === true && (
<EuiFlexItem grow={false} style={{ width: wizardPanelWidth }}>
<ActionsPanel indexPattern={currentIndexPattern} />
</EuiFlexItem>
)}
<EuiFlexItem grow={false} style={{ width: wizardPanelWidth }}>
<ActionsPanel
indexPattern={currentIndexPattern}
searchQueryLanguage={searchQueryLanguage}
searchString={searchString}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageBody>

View file

@ -9,6 +9,7 @@ import React, { useMemo, useState } from 'react';
import {
CENTER_ALIGNMENT,
EuiBasicTableColumn,
EuiButtonIcon,
EuiFlexItem,
EuiIcon,
@ -52,6 +53,7 @@ interface DataVisualizerTableProps<T> {
update: Partial<DataVisualizerIndexBasedAppState | DataVisualizerFileBasedAppState>
) => void;
getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap;
extendedColumns?: Array<EuiBasicTableColumn<T>>;
}
export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
@ -59,11 +61,12 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
pageState,
updatePageState,
getItemIdToExpandedRowMap,
extendedColumns,
}: DataVisualizerTableProps<T>) => {
const [expandedRowItemIds, setExpandedRowItemIds] = useState<string[]>([]);
const [expandAll, toggleExpandAll] = useState<boolean>(false);
const { onTableChange, pagination, sorting } = useTableSettings<DataVisualizerTableItem>(
const { onTableChange, pagination, sorting } = useTableSettings<T>(
items,
pageState,
updatePageState
@ -136,7 +139,7 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
'data-test-subj': 'mlDataVisualizerTableColumnDetailsToggle',
};
return [
const baseColumns = [
expanderColumn,
{
field: 'type',
@ -236,7 +239,8 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
'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 = <T extends DataVisualizerTableItem>({
return (
<EuiFlexItem data-test-subj="mlDataVisualizerTableContainer">
<EuiInMemoryTable<DataVisualizerTableItem>
<EuiInMemoryTable<T>
className={'mlDataVisualizer'}
items={items}
itemId={FIELD_NAME}

View file

@ -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<MlPluginSetup, MlPluginStart> {
embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable },
maps: pluginsStart.maps,
uiActions: pluginsStart.uiActions,
lens: pluginsStart.lens,
kibanaVersion,
},
params

View file

@ -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" },
]

View file

@ -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
);
}

View file

@ -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
);
}

View file

@ -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);
});
});
});
}

View file

@ -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);
});

View file

@ -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 () => {

View file

@ -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}')`
);
});
},
};
}

View file

@ -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) {

View file

@ -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'));
});
}

View file

@ -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);
});
});
});
}

View file

@ -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);
});

View file

@ -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);
});