[ML] Add Lens and Discover integration to index based Data Visualizer (#89471)
This commit is contained in:
parent
be53a06925
commit
70d61436bc
|
@ -25,7 +25,8 @@
|
|||
"spaces",
|
||||
"management",
|
||||
"licenseManagement",
|
||||
"maps"
|
||||
"maps",
|
||||
"lens"
|
||||
],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 & {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export { FieldHistogramRequestConfig, FieldRequestConfig } from './request';
|
||||
export type { CombinedQuery } from './combined_query';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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';
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" },
|
||||
]
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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}')`
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue