[ML] Add embedded map to geo_point fields for Data Visualizer (#88880)

This commit is contained in:
Quynh Nguyen 2021-01-27 11:39:50 -06:00 committed by GitHub
parent 723dd32693
commit da9ad2ade4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 842 additions and 223 deletions

View file

@ -68,7 +68,7 @@ import {
MapEmbeddableInput,
MapEmbeddableOutput,
} from './types';
export { MapEmbeddableInput };
export { MapEmbeddableInput, MapEmbeddableOutput };
export class MapEmbeddable
extends Embeddable<MapEmbeddableInput, MapEmbeddableOutput>

View file

@ -24,7 +24,8 @@
"security",
"spaces",
"management",
"licenseManagement"
"licenseManagement",
"maps"
],
"server": true,
"ui": true,
@ -35,7 +36,8 @@
"dashboard",
"savedObjects",
"home",
"spaces"
"spaces",
"maps"
],
"extraPublicDirs": [
"common"

View file

@ -30,6 +30,7 @@
@import 'components/navigation_menu/index';
@import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly
@import 'components/stats_bar/index';
@import 'components/ml_embedded_map/index';
// Hacks are last so they can overwrite anything above if needed
@import 'hacks';

View file

@ -77,6 +77,8 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
security: deps.security,
licenseManagement: deps.licenseManagement,
storage: localStorage,
embeddable: deps.embeddable,
maps: deps.maps,
...coreStart,
};
@ -118,6 +120,7 @@ export const renderApp = (
http: coreStart.http,
security: deps.security,
urlGenerators: deps.share.urlGenerators,
maps: deps.maps,
});
appMountParams.onAppLeave((actions) => actions.default());

View file

@ -0,0 +1 @@
@import 'ml_embedded_map';

View file

@ -0,0 +1,8 @@
.mlEmbeddedMapContent {
width: 100%;
height: 100%;
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0; // Absolute must for Firefox to scroll contents
}

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { MlEmbeddedMapComponent } from './ml_embedded_map';

View file

@ -0,0 +1,160 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useRef, useState } from 'react';
import { htmlIdGenerator } from '@elastic/eui';
import { LayerDescriptor } from '../../../../../maps/common/descriptor_types';
import {
MapEmbeddable,
MapEmbeddableInput,
MapEmbeddableOutput,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../maps/public/embeddable';
import { MAP_SAVED_OBJECT_TYPE, RenderTooltipContentParams } from '../../../../../maps/public';
import {
EmbeddableFactory,
ErrorEmbeddable,
isErrorEmbeddable,
ViewMode,
} from '../../../../../../../src/plugins/embeddable/public';
import { useMlKibana } from '../../contexts/kibana';
export function MlEmbeddedMapComponent({
layerList,
mapEmbeddableInput,
renderTooltipContent,
}: {
layerList: LayerDescriptor[];
mapEmbeddableInput?: MapEmbeddableInput;
renderTooltipContent?: (params: RenderTooltipContentParams) => JSX.Element;
}) {
const [embeddable, setEmbeddable] = useState<ErrorEmbeddable | MapEmbeddable | undefined>();
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
const baseLayers = useRef<LayerDescriptor[]>();
const {
services: { embeddable: embeddablePlugin, maps: mapsPlugin },
} = useMlKibana();
const factory:
| EmbeddableFactory<MapEmbeddableInput, MapEmbeddableOutput, MapEmbeddable>
| undefined = embeddablePlugin
? embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE)
: undefined;
// Update the layer list with updated geo points upon refresh
useEffect(() => {
async function updateIndexPatternSearchLayer() {
if (
embeddable &&
!isErrorEmbeddable(embeddable) &&
Array.isArray(layerList) &&
Array.isArray(baseLayers.current)
) {
embeddable.setLayerList([...baseLayers.current, ...layerList]);
}
}
updateIndexPatternSearchLayer();
}, [embeddable, layerList]);
useEffect(() => {
async function setupEmbeddable() {
if (!factory) {
// eslint-disable-next-line no-console
console.error('Map embeddable not found.');
return;
}
const input: MapEmbeddableInput = {
id: htmlIdGenerator()(),
attributes: { title: '' },
filters: [],
hidePanelTitles: true,
refreshConfig: {
value: 0,
pause: false,
},
viewMode: ViewMode.VIEW,
isLayerTOCOpen: false,
hideFilterActions: true,
// Zoom Lat/Lon values are set to make sure map is in center in the panel
// It will also omit Greenland/Antarctica etc. NOTE: Can be removed when initialLocation is set
mapCenter: {
lon: 11,
lat: 20,
zoom: 1,
},
// can use mapSettings to center map on anomalies
mapSettings: {
disableInteractive: false,
hideToolbarOverlay: false,
hideLayerControl: false,
hideViewControl: false,
// Doesn't currently work with GEO_JSON. Will uncomment when https://github.com/elastic/kibana/pull/88294 is in
// initialLocation: INITIAL_LOCATION.AUTO_FIT_TO_BOUNDS, // this will startup based on data-extent
autoFitToDataBounds: true, // this will auto-fit when there are changes to the filter and/or query
},
};
const embeddableObject = await factory.create(input);
if (embeddableObject && !isErrorEmbeddable(embeddableObject)) {
const basemapLayerDescriptor = mapsPlugin
? await mapsPlugin.createLayerDescriptors.createBasemapLayerDescriptor()
: null;
if (basemapLayerDescriptor) {
baseLayers.current = [basemapLayerDescriptor];
await embeddableObject.setLayerList(baseLayers.current);
}
}
setEmbeddable(embeddableObject);
}
setupEmbeddable();
// we want this effect to execute exactly once after the component mounts
}, []);
useEffect(() => {
if (embeddable && !isErrorEmbeddable(embeddable) && mapEmbeddableInput !== undefined) {
embeddable.updateInput(mapEmbeddableInput);
}
}, [embeddable, mapEmbeddableInput]);
useEffect(() => {
if (embeddable && !isErrorEmbeddable(embeddable) && renderTooltipContent !== undefined) {
embeddable.setRenderTooltipContent(renderTooltipContent);
}
}, [embeddable, renderTooltipContent]);
// We can only render after embeddable has already initialized
useEffect(() => {
if (embeddableRoot.current && embeddable) {
embeddable.render(embeddableRoot.current);
}
}, [embeddable, embeddableRoot]);
if (!embeddablePlugin) {
// eslint-disable-next-line no-console
console.error('Embeddable start plugin not found');
return null;
}
if (!mapsPlugin) {
// eslint-disable-next-line no-console
console.error('Maps start plugin not found');
return null;
}
return (
<div
data-test-subj="mlEmbeddedMapContent"
className="mlEmbeddedMapContent"
ref={embeddableRoot}
/>
);
}

View file

@ -15,12 +15,16 @@ import { LicenseManagementUIPluginSetup } from '../../../../../license_managemen
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';
interface StartPlugins {
data: DataPublicPluginStart;
security?: SecurityPluginSetup;
licenseManagement?: LicenseManagementUIPluginSetup;
share: SharePluginStart;
embeddable: EmbeddableStart;
maps?: MapsStartApi;
}
export type StartServices = CoreStart &
StartPlugins & {

View file

@ -8,13 +8,13 @@ import React from 'react';
import {
BooleanContent,
DateContent,
GeoPointContent,
IpContent,
KeywordContent,
OtherContent,
TextContent,
NumberContent,
} from '../../../stats_table/components/field_data_expanded_row';
import { GeoPointContent } from './geo_point_content/geo_point_content';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import type { FileBasedFieldVisConfig } from '../../../stats_table/types/field_vis_config';

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Feature, Point } from 'geojson';
import { euiPaletteColorBlind } from '@elastic/eui';
import { DEFAULT_GEO_REGEX } from './geo_point_content';
import { SOURCE_TYPES } from '../../../../../../../../maps/common/constants';
export const convertWKTGeoToLonLat = (
value: string | number
): { lat: number; lon: number } | undefined => {
if (typeof value === 'string') {
const trimmedValue = value.trim().replace('POINT (', '').replace(')', '');
const regExpSerializer = DEFAULT_GEO_REGEX;
const parsed = regExpSerializer.exec(trimmedValue.trim());
if (parsed?.groups?.lat != null && parsed?.groups?.lon != null) {
return {
lat: parseFloat(parsed.groups.lat.trim()),
lon: parseFloat(parsed.groups.lon.trim()),
};
}
}
};
export const DEFAULT_POINT_COLOR = euiPaletteColorBlind()[0];
export const getGeoPointsLayer = (
features: Array<Feature<Point>>,
pointColor: string = DEFAULT_POINT_COLOR
) => {
return {
id: 'geo_points',
label: 'Geo points',
sourceDescriptor: {
type: SOURCE_TYPES.GEOJSON_FILE,
__featureCollection: {
features,
type: 'FeatureCollection',
},
},
visible: true,
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: {
color: pointColor,
},
},
lineColor: {
type: 'STATIC',
options: {
color: '#fff',
},
},
lineWidth: {
type: 'STATIC',
options: {
size: 2,
},
},
iconSize: {
type: 'STATIC',
options: {
size: 6,
},
},
},
},
type: 'VECTOR',
};
};

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useMemo } from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { Feature, Point } from 'geojson';
import type { FieldDataRowProps } from '../../../../stats_table/types/field_data_row';
import { DocumentStatsTable } from '../../../../stats_table/components/field_data_expanded_row/document_stats';
import { MlEmbeddedMapComponent } from '../../../../../components/ml_embedded_map';
import { convertWKTGeoToLonLat, getGeoPointsLayer } from './format_utils';
import { ExpandedRowContent } from '../../../../stats_table/components/field_data_expanded_row/expanded_row_content';
import { ExamplesList } from '../../../../index_based/components/field_data_row/examples_list';
export const DEFAULT_GEO_REGEX = RegExp('(?<lat>.+) (?<lon>.+)');
export const GeoPointContent: FC<FieldDataRowProps> = ({ config }) => {
const formattedResults = useMemo(() => {
const { stats } = config;
if (stats === undefined || stats.topValues === undefined) return null;
if (Array.isArray(stats.topValues)) {
const geoPointsFeatures: Array<Feature<Point>> = [];
// reformatting the top values from POINT (-2.359207 51.37837) to (-2.359207, 51.37837)
const formattedExamples = [];
for (let i = 0; i < stats.topValues.length; i++) {
const value = stats.topValues[i];
const coordinates = convertWKTGeoToLonLat(value.key);
if (coordinates) {
const formattedGeoPoint = `(${coordinates.lat}, ${coordinates.lon})`;
formattedExamples.push(coordinates);
geoPointsFeatures.push({
type: 'Feature',
id: `ml-${config.fieldName}-${i}`,
geometry: {
type: 'Point',
coordinates: [coordinates.lat, coordinates.lon],
},
properties: {
value: formattedGeoPoint,
count: value.doc_count,
},
});
}
}
if (geoPointsFeatures.length > 0) {
return {
examples: formattedExamples,
layerList: [getGeoPointsLayer(geoPointsFeatures)],
};
}
}
}, [config]);
return (
<ExpandedRowContent dataTestSubj={'mlDVGeoPointContent'}>
<DocumentStatsTable config={config} />
{formattedResults && Array.isArray(formattedResults.examples) && (
<EuiFlexItem>
<ExamplesList examples={formattedResults.examples} />
</EuiFlexItem>
)}
{formattedResults && Array.isArray(formattedResults.layerList) && (
<EuiFlexItem
className={'mlDataVisualizerMapWrapper'}
data-test-subj={'mlDataVisualizerEmbeddedMap'}
>
<MlEmbeddedMapComponent layerList={formattedResults.layerList} />
</EuiFlexItem>
)}
</ExpandedRowContent>
);
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { GeoPointContent } from './geo_point_content';

View file

@ -1 +1 @@
@import 'file_datavisualizer_view'
@import 'file_datavisualizer_view';

View file

@ -6,23 +6,31 @@
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,
GeoPointContent,
IpContent,
KeywordContent,
NumberContent,
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 { 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';
export const IndexBasedDataVisualizerExpandedRow = ({ item }: { item: FieldVisConfig }) => {
export const IndexBasedDataVisualizerExpandedRow = ({
item,
indexPattern,
combinedQuery,
}: {
item: FieldVisConfig;
indexPattern: IndexPattern | undefined;
combinedQuery: CombinedQuery;
}) => {
const config = item;
const { loading, type, existsInDocs, fieldName } = config;
@ -42,7 +50,13 @@ export const IndexBasedDataVisualizerExpandedRow = ({ item }: { item: FieldVisCo
return <DateContent config={config} />;
case ML_JOB_FIELD_TYPES.GEO_POINT:
return <GeoPointContent config={config} />;
return (
<GeoPointContent
config={config}
indexPattern={indexPattern}
combinedQuery={combinedQuery}
/>
);
case ML_JOB_FIELD_TYPES.IP:
return <IpContent config={config} />;

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
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';
export interface CombinedQuery {
searchString: string | { [key: string]: any };
searchQueryLanguage: string;
}
export const GeoPointContent: FC<{
config: FieldVisConfig;
indexPattern: IndexPattern | undefined;
combinedQuery: CombinedQuery;
}> = ({ config, indexPattern, combinedQuery }) => {
const { stats } = config;
const [layerList, setLayerList] = useState<LayerDescriptor[]>([]);
const {
services: { maps: mapsPlugin },
} = useMlKibana();
// Update the layer list with updated geo points upon refresh
useEffect(() => {
async function updateIndexPatternSearchLayer() {
if (
indexPattern?.id !== undefined &&
config !== undefined &&
config.fieldName !== undefined &&
config.type === ML_JOB_FIELD_TYPES.GEO_POINT
) {
const params = {
indexPatternId: indexPattern.id,
geoFieldName: config.fieldName,
geoFieldType: config.type as ES_GEO_FIELD_TYPE.GEO_POINT,
query: {
query: combinedQuery.searchString,
language: combinedQuery.searchQueryLanguage,
},
};
const searchLayerDescriptor = mapsPlugin
? await mapsPlugin.createLayerDescriptors.createESSearchSourceLayerDescriptor(params)
: null;
if (searchLayerDescriptor) {
setLayerList([...layerList, searchLayerDescriptor]);
}
}
}
updateIndexPatternSearchLayer();
}, [indexPattern, config.fieldName, combinedQuery]);
if (stats?.examples === undefined) return null;
return (
<ExpandedRowContent dataTestSubj={'mlDVIndexBasedMapContent'}>
<DocumentStatsTable config={config} />
<EuiFlexItem>
<ExamplesList examples={stats.examples} />
</EuiFlexItem>
<EuiFlexItem className={'mlDataVisualizerMapWrapper'}>
<MlEmbeddedMapComponent layerList={layerList} />
</EuiFlexItem>
</ExpandedRowContent>
);
};

View file

@ -15,25 +15,29 @@ interface Props {
}
export const ExamplesList: FC<Props> = ({ examples }) => {
if (
examples === undefined ||
examples === null ||
!Array.isArray(examples) ||
examples.length === 0
) {
if (examples === undefined || examples === null || !Array.isArray(examples)) {
return null;
}
const examplesContent = examples.map((example, i) => {
return (
<EuiListGroupItem
className="mlFieldDataCard__codeContent"
size="s"
key={`example_${i}`}
label={typeof example === 'string' ? example : JSON.stringify(example)}
let examplesContent;
if (examples.length === 0) {
examplesContent = (
<FormattedMessage
id="xpack.ml.fieldDataCard.examplesList.noExamplesMessage"
defaultMessage="No examples were obtained for this field"
/>
);
});
} else {
examplesContent = examples.map((example, i) => {
return (
<EuiListGroupItem
className="mlFieldDataCard__codeContent"
size="s"
key={`example_${i}`}
label={typeof example === 'string' ? example : JSON.stringify(example)}
/>
);
});
}
return (
<div data-test-subj="mlFieldDataExamplesList">

View file

@ -19,9 +19,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import classNames from 'classnames';
import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format';
import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place';
import { ExpandedRowFieldHeader } from '../../../../stats_table/components/expanded_row_field_header';
import { FieldVisStats } from '../../../../stats_table/types';
interface Props {
stats: any;
stats: FieldVisStats | undefined;
fieldFormat?: any;
barColor?: 'primary' | 'secondary' | 'danger' | 'subdued' | 'accent';
compressed?: boolean;
@ -37,6 +39,7 @@ function getPercentLabel(docCount: number, topValuesSampleSize: number): string
}
export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed }) => {
if (stats === undefined) return null;
const {
topValues,
topValuesSampleSize,
@ -46,51 +49,64 @@ export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed
} = stats;
const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count;
return (
<div data-test-subj="mlFieldDataTopValues" className={'mlFieldDataTopValuesContainer'}>
{Array.isArray(topValues) &&
topValues.map((value: any) => (
<EuiFlexGroup gutterSize="xs" alignItems="center" key={value.key}>
<EuiFlexItem
grow={false}
className={classNames(
'eui-textTruncate',
'mlTopValuesValueLabelContainer',
`mlTopValuesValueLabelContainer--${compressed === true ? 'small' : 'large'}`
<EuiFlexItem data-test-subj={'mlTopValues'}>
<ExpandedRowFieldHeader>
<FormattedMessage id="xpack.ml.fieldDataCard.topValuesLabel" defaultMessage="Top values" />
</ExpandedRowFieldHeader>
<div data-test-subj="mlFieldDataTopValues" className={'mlFieldDataTopValuesContainer'}>
{Array.isArray(topValues) &&
topValues.map((value) => (
<EuiFlexGroup gutterSize="xs" alignItems="center" key={value.key}>
<EuiFlexItem
grow={false}
className={classNames(
'eui-textTruncate',
'mlTopValuesValueLabelContainer',
`mlTopValuesValueLabelContainer--${compressed === true ? 'small' : 'large'}`
)}
>
<EuiToolTip content={kibanaFieldFormat(value.key, fieldFormat)} position="right">
<EuiText size="xs" textAlign={'right'} color="subdued">
{kibanaFieldFormat(value.key, fieldFormat)}
</EuiText>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem data-test-subj="mlFieldDataTopValueBar">
<EuiProgress
value={value.doc_count}
max={progressBarMax}
color={barColor}
size="m"
/>
</EuiFlexItem>
{progressBarMax !== undefined && (
<EuiFlexItem
grow={false}
className={classNames('eui-textTruncate', 'mlTopValuesPercentLabelContainer')}
>
<EuiText size="xs" textAlign="left" color="subdued">
{getPercentLabel(value.doc_count, progressBarMax)}
</EuiText>
</EuiFlexItem>
)}
>
<EuiToolTip content={kibanaFieldFormat(value.key, fieldFormat)} position="right">
<EuiText size="xs" textAlign={'right'} color="subdued">
{kibanaFieldFormat(value.key, fieldFormat)}
</EuiText>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem data-test-subj="mlFieldDataTopValueBar">
<EuiProgress value={value.doc_count} max={progressBarMax} color={barColor} size="m" />
</EuiFlexItem>
<EuiFlexItem
grow={false}
className={classNames('eui-textTruncate', 'mlTopValuesPercentLabelContainer')}
>
<EuiText size="xs" textAlign="left" color="subdued">
{getPercentLabel(value.doc_count, progressBarMax)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
))}
{isTopValuesSampled === true && (
<Fragment>
<EuiSpacer size="xs" />
<EuiText size="xs" textAlign={'left'}>
<FormattedMessage
id="xpack.ml.fieldDataCard.topValues.calculatedFromSampleDescription"
defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
values={{
topValuesSamplerShardSize,
}}
/>
</EuiText>
</Fragment>
)}
</div>
</EuiFlexGroup>
))}
{isTopValuesSampled === true && (
<Fragment>
<EuiSpacer size="xs" />
<EuiText size="xs" textAlign={'left'}>
<FormattedMessage
id="xpack.ml.fieldDataCard.topValues.calculatedFromSampleDescription"
defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
values={{
topValuesSamplerShardSize,
}}
/>
</EuiText>
</Fragment>
)}
</div>
</EuiFlexItem>
);
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useEffect, useMemo, useState } from 'react';
import React, { FC, Fragment, useEffect, useMemo, useState, useCallback } from 'react';
import { merge } from 'rxjs';
import {
EuiFlexGroup,
@ -112,19 +112,6 @@ export const getDefaultDataVisualizerListState = (): Required<DataVisualizerInde
showEmptyFields: false,
});
function getItemIdToExpandedRowMap(
itemIds: string[],
items: FieldVisConfig[]
): ItemIdToExpandedRowMap {
return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => {
const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName);
if (item !== undefined) {
m[fieldName] = <IndexBasedDataVisualizerExpandedRow item={item} />;
}
return m;
}, {} as ItemIdToExpandedRowMap);
}
export const Page: FC = () => {
const mlContext = useMlContext();
const restorableDefaults = getDefaultDataVisualizerListState();
@ -678,6 +665,26 @@ export const Page: FC = () => {
}
return { visibleFieldsCount: _visibleFieldsCount, totalFieldsCount: _totalFieldsCount };
}, [overallStats, showEmptyFields]);
const getItemIdToExpandedRowMap = useCallback(
function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap {
return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => {
const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName);
if (item !== undefined) {
m[fieldName] = (
<IndexBasedDataVisualizerExpandedRow
item={item}
indexPattern={currentIndexPattern}
combinedQuery={{ searchQueryLanguage, searchString }}
/>
);
}
return m;
}, {} as ItemIdToExpandedRowMap);
},
[currentIndexPattern, searchQuery]
);
const {
services: { docLinks },
} = useMlKibana();

View file

@ -60,6 +60,21 @@
@include euiCodeFont;
}
.mlFieldDataCard__geoContent {
z-index: auto;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
.embPanel__content {
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0; // Absolute must for Firefox to scroll contents
}
}
.mlFieldDataCard__stats {
padding: $euiSizeS $euiSizeS 0 $euiSizeS;
text-align: center;

View file

@ -1,5 +1,6 @@
@import 'components/field_data_expanded_row/number_content';
@import 'components/field_data_expanded_row/index';
@import 'components/field_count_stats/index';
@import 'components/field_data_row/index';
.mlDataVisualizerFieldExpandedRow {
padding-left: $euiSize * 4;
@ -37,6 +38,7 @@
}
.mlDataVisualizerSummaryTable {
max-width: 350px;
min-width: 250px;
.euiTableRow > .euiTableRowCell {
border-bottom: 0;
}
@ -45,6 +47,10 @@
}
}
.mlDataVisualizerSummaryTableWrapper {
max-width: 350px;
max-width: 300px;
}
.mlDataVisualizerMapWrapper {
min-height: 300px;
min-width: 600px;
}
}

View file

@ -1 +1,7 @@
@import 'number_content';
.mlDataVisualizerExpandedRow {
@include euiBreakpoint('xs', 's', 'm') {
flex-direction: column;
}
}

View file

@ -5,7 +5,7 @@
*/
import React, { FC, ReactNode, useMemo } from 'react';
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { EuiBasicTable, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { Axis, BarSeries, Chart, Settings } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n/react';
@ -16,6 +16,7 @@ import { getTFPercentage } from '../../utils';
import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place';
import { useDataVizChartTheme } from '../../hooks';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
function getPercentLabel(value: number): string {
if (value === 0) {
@ -85,7 +86,7 @@ export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
);
return (
<EuiFlexGroup data-test-subj={'mlDVBooleanContent'} gutterSize={'xl'}>
<ExpandedRowContent dataTestSubj={'mlDVBooleanContent'}>
<DocumentStatsTable config={config} />
<EuiFlexItem className={'mlDataVisualizerSummaryTableWrapper'}>
@ -138,6 +139,6 @@ export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
/>
</Chart>
</EuiFlexItem>
</EuiFlexGroup>
</ExpandedRowContent>
);
};

View file

@ -5,7 +5,7 @@
*/
import React, { FC, ReactNode } from 'react';
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiBasicTable, EuiFlexItem } from '@elastic/eui';
// @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format';
import { FormattedMessage } from '@kbn/i18n/react';
@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS';
interface SummaryTableItem {
function: string;
@ -66,7 +67,7 @@ export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
];
return (
<EuiFlexGroup data-test-subj={'mlDVDateContent'} gutterSize={'xl'}>
<ExpandedRowContent dataTestSubj={'mlDVDateContent'}>
<DocumentStatsTable config={config} />
<EuiFlexItem className={'mlDataVisualizerSummaryTableWrapper'}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
@ -80,6 +81,6 @@ export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
tableLayout="auto"
/>
</EuiFlexItem>
</EuiFlexGroup>
</ExpandedRowContent>
);
};

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { EuiBasicTable, EuiFlexItem } from '@elastic/eui';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { FieldDataRowProps } from '../../types';
import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place';
const metaTableColumns = [
{
@ -59,7 +60,7 @@ export const DocumentStatsTable: FC<FieldDataRowProps> = ({ config }) => {
defaultMessage="percentage"
/>
),
value: `${(count / sampleCount) * 100}%`,
value: `${roundToDecimalPlace((count / sampleCount) * 100)}%`,
},
{
function: 'distinctValues',

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, ReactNode } from 'react';
import { EuiFlexGroup } from '@elastic/eui';
interface Props {
children: ReactNode;
dataTestSubj: string;
}
export const ExpandedRowContent: FC<Props> = ({ children, dataTestSubj }) => {
return (
<EuiFlexGroup
data-test-subj={dataTestSubj}
gutterSize={'xl'}
className={'mlDataVisualizerExpandedRow'}
>
{children}
</EuiFlexGroup>
);
};

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list';
import { DocumentStatsTable } from './document_stats';
import { TopValues } from '../../../index_based/components/field_data_row/top_values';
export const GeoPointContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined || (stats?.examples === undefined && stats?.topValues === undefined))
return null;
return (
<EuiFlexGroup data-test-subj={'mlDVGeoPointContent'} gutterSize={'xl'}>
<DocumentStatsTable config={config} />
{Array.isArray(stats.examples) && (
<EuiFlexItem>
<ExamplesList examples={stats.examples!} />
</EuiFlexItem>
)}
{Array.isArray(stats.topValues) && (
<EuiFlexItem>
<TopValues stats={stats} barColor="secondary" />
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

View file

@ -6,7 +6,7 @@
export { BooleanContent } from './boolean_content';
export { DateContent } from './date_content';
export { GeoPointContent } from './geo_point_content';
export { GeoPointContent } from '../../../file_based/components/expanded_row/geo_point_content/geo_point_content';
export { KeywordContent } from './keyword_content';
export { IpContent } from './ip_content';
export { NumberContent } from './number_content';

View file

@ -5,14 +5,10 @@
*/
import React, { FC } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { TopValues } from '../../../index_based/components/field_data_row/top_values';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
export const IpContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
@ -22,17 +18,9 @@ export const IpContent: FC<FieldDataRowProps> = ({ config }) => {
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
return (
<EuiFlexGroup gutterSize={'xl'}>
<ExpandedRowContent dataTestSubj={'mlDVIPContent'}>
<DocumentStatsTable config={config} />
<EuiFlexItem>
<ExpandedRowFieldHeader>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardIp.topValuesLabel"
defaultMessage="Top values"
/>
</ExpandedRowFieldHeader>
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
</EuiFlexItem>
</EuiFlexGroup>
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
</ExpandedRowContent>
);
};

View file

@ -5,31 +5,20 @@
*/
import React, { FC } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { TopValues } from '../../../index_based/components/field_data_row/top_values';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
export const KeywordContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
return (
<EuiFlexGroup data-test-subj={'mlDVKeywordContent'} gutterSize={'xl'}>
<ExpandedRowContent dataTestSubj={'mlDVKeywordContent'}>
<DocumentStatsTable config={config} />
<EuiFlexItem>
<ExpandedRowFieldHeader>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardKeyword.topValuesLabel"
defaultMessage="Top values"
/>
</ExpandedRowFieldHeader>
<EuiSpacer size="xs" />
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
</EuiFlexItem>
</EuiFlexGroup>
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
</ExpandedRowContent>
);
};

View file

@ -5,7 +5,7 @@
*/
import React, { FC, ReactNode, useEffect, useState } from 'react';
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { EuiBasicTable, EuiFlexItem, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -20,6 +20,7 @@ import {
import { TopValues } from '../../../index_based/components/field_data_row/top_values';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
const METRIC_DISTRIBUTION_CHART_WIDTH = 325;
const METRIC_DISTRIBUTION_CHART_HEIGHT = 200;
@ -97,7 +98,7 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
}
);
return (
<EuiFlexGroup data-test-subj={'mlDVNumberContent'} gutterSize={'xl'}>
<ExpandedRowContent dataTestSubj={'mlDVNumberContent'}>
<DocumentStatsTable config={config} />
<EuiFlexItem className={'mlDataVisualizerSummaryTableWrapper'}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
@ -112,24 +113,7 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
</EuiFlexItem>
{stats && (
<EuiFlexItem data-test-subj={'mlTopValues'}>
<EuiFlexItem grow={false}>
<ExpandedRowFieldHeader>
<FormattedMessage
id="xpack.ml.fieldDataCardExpandedRow.numberContent.topValuesTitle"
defaultMessage="Top values"
/>
</ExpandedRowFieldHeader>
</EuiFlexItem>
<EuiFlexItem>
<TopValues
stats={stats}
fieldFormat={fieldFormat}
barColor="secondary"
compressed={true}
/>
</EuiFlexItem>
</EuiFlexItem>
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" compressed={true} />
)}
{distribution && (
<EuiFlexItem data-test-subj={'mlMetricDistribution'}>
@ -164,6 +148,6 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
</EuiFlexItem>
</EuiFlexItem>
)}
</EuiFlexGroup>
</ExpandedRowContent>
);
};

View file

@ -5,18 +5,18 @@
*/
import React, { FC } from 'react';
import { EuiFlexGroup } from '@elastic/eui';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list';
import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content';
export const OtherContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
return (
<EuiFlexGroup gutterSize={'xl'} data-test-subj={'mlDVOtherContent'}>
<ExpandedRowContent dataTestSubj={'mlDVOtherContent'}>
<DocumentStatsTable config={config} />
{Array.isArray(stats.examples) && <ExamplesList examples={stats.examples} />}
</EuiFlexGroup>
</ExpandedRowContent>
);
};

View file

@ -5,13 +5,14 @@
*/
import React, { FC, Fragment } from 'react';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { EuiCallOut, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list';
import { ExpandedRowContent } from './expanded_row_content';
export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
@ -23,7 +24,7 @@ export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
const numExamples = examples.length;
return (
<EuiFlexGroup gutterSize={'xl'} data-test-subj={'mlDVTextContent'}>
<ExpandedRowContent dataTestSubj={'mlDVTextContent'}>
<EuiFlexItem>
{numExamples > 0 && <ExamplesList examples={examples} />}
{numExamples === 0 && (
@ -59,6 +60,6 @@ export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
</Fragment>
)}
</EuiFlexItem>
</EuiFlexGroup>
</ExpandedRowContent>
);
};

View file

@ -0,0 +1,3 @@
.mlDataVisualizerColumnHeaderIcon {
max-width: $euiSizeM;
}

View file

@ -12,7 +12,7 @@ export const DistinctValues = ({ cardinality }: { cardinality?: number }) => {
if (cardinality === undefined) return null;
return (
<EuiFlexGroup alignItems={'center'}>
<EuiFlexItem style={{ maxWidth: 10 }}>
<EuiFlexItem className={'mlDataVisualizerColumnHeaderIcon'}>
<EuiIcon type="database" size={'s'} />
</EuiFlexItem>
<EuiText size={'s'}>

View file

@ -21,7 +21,7 @@ export const DocumentStat = ({ config }: FieldDataRowProps) => {
return (
<EuiFlexGroup alignItems={'center'}>
<EuiFlexItem style={{ maxWidth: 10 }}>
<EuiFlexItem className={'mlDataVisualizerColumnHeaderIcon'}>
<EuiIcon type="document" size={'s'} />
</EuiFlexItem>
<EuiText size={'s'}>

View file

@ -14,6 +14,7 @@ import {
} from '../metric_distribution_chart';
import { formatSingleValue } from '../../../../formatters/format_value';
import { FieldVisConfig } from '../../types';
import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format';
const METRIC_DISTRIBUTION_CHART_WIDTH = 150;
const METRIC_DISTRIBUTION_CHART_HEIGHT = 80;
@ -59,14 +60,16 @@ export const IndexBasedNumberContentPreview: FC<NumberContentPreviewProps> = ({
<>
<EuiSpacer size="s" />
<EuiFlexGroup direction={'row'} data-test-subj={`${dataTestSubj}-legend`}>
<EuiFlexItem className={'mlDataGridChart__legend'}>{legendText.min}</EuiFlexItem>
<EuiFlexItem className={'mlDataGridChart__legend'}>
{kibanaFieldFormat(legendText.min, fieldFormat)}
</EuiFlexItem>
<EuiFlexItem
className={classNames(
'mlDataGridChart__legend',
'mlDataGridChart__legend--numeric'
)}
>
{legendText.max}
{kibanaFieldFormat(legendText.max, fieldFormat)}
</EuiFlexItem>
</EuiFlexGroup>
</>

View file

@ -21,6 +21,7 @@ import type {
import type { IndexPatternsContract, DataPublicPluginStart } from 'src/plugins/data/public';
import type { SharePluginStart } from 'src/plugins/share/public';
import type { SecurityPluginSetup } from '../../../../security/public';
import type { MapsStartApi } from '../../../../maps/public';
export interface DependencyCache {
timefilter: DataPublicPluginSetup['query']['timefilter'] | null;
@ -40,6 +41,7 @@ export interface DependencyCache {
security: SecurityPluginSetup | undefined | null;
i18n: I18nStart | null;
urlGenerators: SharePluginStart['urlGenerators'] | null;
maps: MapsStartApi | null;
}
const cache: DependencyCache = {
@ -60,6 +62,7 @@ const cache: DependencyCache = {
security: null,
i18n: null,
urlGenerators: null,
maps: null,
};
export function setDependencyCache(deps: Partial<DependencyCache>) {

View file

@ -25,7 +25,7 @@ import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import type { DataPublicPluginStart } from 'src/plugins/data/public';
import type { HomePublicPluginSetup } from 'src/plugins/home/public';
import type { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public';
import type { EmbeddableSetup } from 'src/plugins/embeddable/public';
import type { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public';
import type { SpacesPluginStart } from '../../spaces/public';
import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
@ -45,6 +45,7 @@ import { setDependencyCache } from './application/util/dependency_cache';
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';
export interface MlStartDependencies {
data: DataPublicPluginStart;
@ -52,6 +53,8 @@ export interface MlStartDependencies {
kibanaLegacy: KibanaLegacyStart;
uiActions: UiActionsStart;
spaces?: SpacesPluginStart;
embeddable: EmbeddableStart;
maps?: MapsStartApi;
}
export interface MlSetupDependencies {
security?: SecurityPluginSetup;
@ -102,7 +105,8 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
usageCollection: pluginsSetup.usageCollection,
licenseManagement: pluginsSetup.licenseManagement,
home: pluginsSetup.home,
embeddable: pluginsSetup.embeddable,
embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable },
maps: pluginsStart.maps,
uiActions: pluginsStart.uiActions,
kibanaVersion,
},

View file

@ -1189,7 +1189,8 @@ export class DataVisualizer {
});
const searchBody = {
_source: field,
fields: [field],
_source: false,
query: {
bool: {
filter: filterCriteria,
@ -1209,16 +1210,16 @@ export class DataVisualizer {
if (body.hits.total.value > 0) {
const hits = body.hits.hits;
for (let i = 0; i < hits.length; i++) {
// Look in the _source for the field value.
// If the field is not in the _source (as will happen if the
// field is populated using copy_to in the index mapping),
// there will be no example to add.
// Use lodash get() to support field names containing dots.
const example: any = get(hits[i]._source, field);
if (example !== undefined && stats.examples.indexOf(example) === -1) {
stats.examples.push(example);
if (stats.examples.length === maxExamples) {
break;
const doc: object[] | undefined = get(hits[i].fields, field);
// the results from fields query is always an array
if (Array.isArray(doc) && doc.length > 0) {
const example = doc[0];
if (example !== undefined && stats.examples.indexOf(example) === -1) {
stats.examples.push(example);
if (stats.examples.length === maxExamples) {
break;
}
}
}
}

View file

@ -13073,8 +13073,6 @@
"xpack.ml.featureRegistry.mlFeatureName": "機械学習",
"xpack.ml.fieldDataCard.cardBoolean.valuesLabel": "値",
"xpack.ml.fieldDataCard.cardDate.summaryTableTitle": "まとめ",
"xpack.ml.fieldDataCard.cardIp.topValuesLabel": "トップの値",
"xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "トップの値",
"xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "たとえば、ドキュメントマッピングで {copyToParam} パラメーターを使ったり、{includesParam} と {excludesParam} パラメーターを使用してインデックスした後に {sourceParam} フィールドから切り取ったりして入力される場合があります。",
"xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "このフィールドはクエリが実行されたドキュメントの {sourceParam} フィールドにありませんでした。",
"xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "このフィールドの例が取得されませんでした",
@ -13092,7 +13090,6 @@
"xpack.ml.fieldDataCardExpandedRow.numberContent.medianLabel": "中間",
"xpack.ml.fieldDataCardExpandedRow.numberContent.minLabel": "分",
"xpack.ml.fieldDataCardExpandedRow.numberContent.summaryTableTitle": "まとめ",
"xpack.ml.fieldDataCardExpandedRow.numberContent.topValuesTitle": "トップの値",
"xpack.ml.fieldTitleBar.documentCountLabel": "ドキュメントカウント",
"xpack.ml.fieldTypeIcon.booleanTypeAriaLabel": "ブールタイプ",
"xpack.ml.fieldTypeIcon.dateTypeAriaLabel": "日付タイプ",

View file

@ -13104,8 +13104,6 @@
"xpack.ml.featureRegistry.mlFeatureName": "Machine Learning",
"xpack.ml.fieldDataCard.cardBoolean.valuesLabel": "值",
"xpack.ml.fieldDataCard.cardDate.summaryTableTitle": "摘要",
"xpack.ml.fieldDataCard.cardIp.topValuesLabel": "排名最前值",
"xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "排名最前值",
"xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "例如,可以使用文档映射中的 {copyToParam} 参数进行填充,也可以在索引后通过使用 {includesParam} 和 {excludesParam} 参数从 {sourceParam} 字段中修剪。",
"xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "查询的文档的 {sourceParam} 字段中不存在此字段。",
"xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "没有获取此字段的示例",
@ -13123,7 +13121,6 @@
"xpack.ml.fieldDataCardExpandedRow.numberContent.medianLabel": "中值",
"xpack.ml.fieldDataCardExpandedRow.numberContent.minLabel": "最小值",
"xpack.ml.fieldDataCardExpandedRow.numberContent.summaryTableTitle": "摘要",
"xpack.ml.fieldDataCardExpandedRow.numberContent.topValuesTitle": "排名最前值",
"xpack.ml.fieldTitleBar.documentCountLabel": "文档计数",
"xpack.ml.fieldTypeIcon.booleanTypeAriaLabel": "布尔类型",
"xpack.ml.fieldTypeIcon.dateTypeAriaLabel": "日期类型",

View file

@ -112,6 +112,47 @@ export default function ({ getService }: FtrProviderContext) {
fieldNameFiltersResultCount: 1,
},
},
{
suiteSuffix: 'with a file containing geo field',
filePath: path.join(__dirname, 'files_to_import', 'geo_file.csv'),
indexName: 'user-import_2',
createIndexPattern: false,
fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT],
fieldNameFilters: ['Coordinates'],
expected: {
results: {
title: 'geo_file.csv',
numberOfFields: 3,
},
metricFields: [],
nonMetricFields: [
{
fieldName: 'Context',
type: ML_JOB_FIELD_TYPES.UNKNOWN,
docCountFormatted: '0 (0%)',
exampleCount: 0,
},
{
fieldName: 'Coordinates',
type: ML_JOB_FIELD_TYPES.GEO_POINT,
docCountFormatted: '13 (100%)',
exampleCount: 7,
},
{
fieldName: 'Location',
type: ML_JOB_FIELD_TYPES.KEYWORD,
docCountFormatted: '13 (100%)',
exampleCount: 7,
},
],
visibleMetricFieldsCount: 0,
totalMetricFieldsCount: 0,
populatedFieldsCount: 3,
totalFieldsCount: 3,
fieldTypeFiltersResultCount: 1,
fieldNameFiltersResultCount: 1,
},
},
];
const testDataListNegative = [

View file

@ -0,0 +1,14 @@
Coordinates,Location,Context
POINT (-2.516919 51.423683),On or near A4175,
POINT (-2.515072 51.419357),On or near Stockwood Hill,
POINT (-2.509126 51.416137),On or near St Francis Road,
POINT (-2.509384 51.40959),On or near Barnard Walk,
POINT (-2.509126 51.416137),On or near St Francis Road,
POINT (-2.516919 51.423683),On or near A4175,
POINT (-2.511571 51.414895),On or near Orchard Close,
POINT (-2.534338 51.417697),On or near Scotland Lane,
POINT (-2.509384 51.40959),On or near Barnard Walk,
POINT (-2.495055 51.422132),On or near Cross Street,
POINT (-2.509384 51.40959),On or near Barnard Walk,
POINT (-2.495055 51.422132),On or near Cross Street,
POINT (-2.509126 51.416137),On or near St Francis Road,
1 Coordinates Location Context
2 POINT (-2.516919 51.423683) On or near A4175
3 POINT (-2.515072 51.419357) On or near Stockwood Hill
4 POINT (-2.509126 51.416137) On or near St Francis Road
5 POINT (-2.509384 51.40959) On or near Barnard Walk
6 POINT (-2.509126 51.416137) On or near St Francis Road
7 POINT (-2.516919 51.423683) On or near A4175
8 POINT (-2.511571 51.414895) On or near Orchard Close
9 POINT (-2.534338 51.417697) On or near Scotland Lane
10 POINT (-2.509384 51.40959) On or near Barnard Walk
11 POINT (-2.495055 51.422132) On or near Cross Street
12 POINT (-2.509384 51.40959) On or near Barnard Walk
13 POINT (-2.495055 51.422132) On or near Cross Street
14 POINT (-2.509126 51.416137) On or near St Francis Road

View file

@ -24,6 +24,11 @@ interface TestData {
sourceIndexOrSavedSearch: string;
fieldNameFilters: string[];
fieldTypeFilters: string[];
rowsPerPage?: 10 | 25 | 50;
sampleSizeValidations: Array<{
size: number;
expected: { field: string; docCountFormatted: string };
}>;
expected: {
totalDocCountFormatted: string;
metricFields?: MetricFieldVisConfig[];
@ -47,6 +52,10 @@ export default function ({ getService }: FtrProviderContext) {
sourceIndexOrSavedSearch: 'ft_farequote',
fieldNameFilters: ['airline', '@timestamp'],
fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD],
sampleSizeValidations: [
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
],
expected: {
totalDocCountFormatted: '86,274',
metricFields: [
@ -132,6 +141,10 @@ export default function ({ getService }: FtrProviderContext) {
sourceIndexOrSavedSearch: 'ft_farequote_kuery',
fieldNameFilters: ['@version'],
fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT],
sampleSizeValidations: [
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
],
expected: {
totalDocCountFormatted: '34,415',
metricFields: [
@ -217,6 +230,10 @@ export default function ({ getService }: FtrProviderContext) {
sourceIndexOrSavedSearch: 'ft_farequote_lucene',
fieldNameFilters: ['@version.keyword', 'type'],
fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER],
sampleSizeValidations: [
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
],
expected: {
totalDocCountFormatted: '34,416',
metricFields: [
@ -297,6 +314,41 @@ export default function ({ getService }: FtrProviderContext) {
},
};
const sampleLogTestData: TestData = {
suiteTitle: 'geo point field',
sourceIndexOrSavedSearch: 'ft_module_sample_logs',
fieldNameFilters: ['geo.coordinates'],
fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT],
rowsPerPage: 50,
expected: {
totalDocCountFormatted: '408',
metricFields: [],
// only testing the geo_point fields
nonMetricFields: [
{
fieldName: 'geo.coordinates',
type: ML_JOB_FIELD_TYPES.GEO_POINT,
existsInDocs: true,
aggregatable: true,
loading: false,
docCountFormatted: '408 (100%)',
exampleCount: 10,
},
],
emptyFields: [],
visibleMetricFieldsCount: 4,
totalMetricFieldsCount: 5,
populatedFieldsCount: 35,
totalFieldsCount: 36,
fieldNameFiltersResultCount: 1,
fieldTypeFiltersResultCount: 1,
},
sampleSizeValidations: [
{ size: 1000, expected: { field: 'geo.coordinates', docCountFormatted: '408 (100%)' } },
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '408 (100%)' } },
],
};
function runTests(testData: TestData) {
it(`${testData.suiteTitle} loads the source data in the data visualizer`, async () => {
await ml.testExecution.logTestStep(
@ -332,6 +384,10 @@ export default function ({ getService }: FtrProviderContext) {
);
await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist();
if (testData.rowsPerPage) {
await ml.dataVisualizerTable.ensureNumRowsPerPage(testData.rowsPerPage);
}
await ml.dataVisualizerTable.assertSearchPanelExist();
await ml.dataVisualizerTable.assertSampleSizeInputExists();
await ml.dataVisualizerTable.assertFieldTypeInputExists();
@ -376,8 +432,14 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
`${testData.suiteTitle} sample size control changes non-metric fields`
);
await ml.dataVisualizerTable.setSampleSizeInputValue(1000, 'airline', '1000 (100%)');
await ml.dataVisualizerTable.setSampleSizeInputValue(5000, '@timestamp', '5000 (100%)');
for (const sampleSizeCase of testData.sampleSizeValidations) {
const { size, expected } = sampleSizeCase;
await ml.dataVisualizerTable.setSampleSizeInputValue(
size,
expected.field,
expected.docCountFormatted
);
}
await ml.testExecution.logTestStep('sets and resets field type filter correctly');
await ml.dataVisualizerTable.setFieldTypeFilter(
@ -411,7 +473,10 @@ export default function ({ getService }: FtrProviderContext) {
this.tags(['mlqa']);
before(async () => {
await esArchiver.loadIfNeeded('ml/farequote');
await esArchiver.loadIfNeeded('ml/module_sample_logs');
await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
await ml.testResources.createIndexPatternIfNeeded('ft_module_sample_logs', '@timestamp');
await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded();
await ml.testResources.createSavedSearchFarequoteKueryIfNeeded();
await ml.testResources.setKibanaTimeZoneToUTC();
@ -447,5 +512,15 @@ export default function ({ getService }: FtrProviderContext) {
runTests(farequoteLuceneSearchTestData);
});
describe('with module_sample_logs ', function () {
// Run tests on full farequote index.
it(`${sampleLogTestData.suiteTitle} loads the data visualizer selector page`, async () => {
// Start navigation from the base of the ML app.
await ml.navigation.navigateToMl();
await ml.navigation.navigateToDataVisualizer();
});
runTests(sampleLogTestData);
});
});
}

View file

@ -288,6 +288,16 @@ export function MachineLearningDataVisualizerTableProvider(
await this.ensureDetailsClosed(fieldName);
}
public async assertExamplesList(fieldName: string, expectedExamplesCount: number) {
const examplesList = await testSubjects.find(
this.detailsSelector(fieldName, 'mlFieldDataExamplesList')
);
const examplesListItems = await examplesList.findAllByTagName('li');
expect(examplesListItems).to.have.length(
expectedExamplesCount,
`Expected example list item count for field '${fieldName}' to be '${expectedExamplesCount}' (got '${examplesListItems.length}')`
);
}
public async assertTextFieldContents(
fieldName: string,
docCountFormatted: string,
@ -297,14 +307,33 @@ export function MachineLearningDataVisualizerTableProvider(
await this.assertFieldDocCount(fieldName, docCountFormatted);
await this.ensureDetailsOpen(fieldName);
const examplesList = await testSubjects.find(
this.detailsSelector(fieldName, 'mlFieldDataExamplesList')
);
const examplesListItems = await examplesList.findAllByTagName('li');
expect(examplesListItems).to.have.length(
expectedExamplesCount,
`Expected example list item count for field '${fieldName}' to be '${expectedExamplesCount}' (got '${examplesListItems.length}')`
);
await this.assertExamplesList(fieldName, expectedExamplesCount);
await this.ensureDetailsClosed(fieldName);
}
public async assertGeoPointFieldContents(
fieldName: string,
docCountFormatted: string,
expectedExamplesCount: number
) {
await this.assertRowExists(fieldName);
await this.assertFieldDocCount(fieldName, docCountFormatted);
await this.ensureDetailsOpen(fieldName);
await this.assertExamplesList(fieldName, expectedExamplesCount);
await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlEmbeddedMapContent'));
await this.ensureDetailsClosed(fieldName);
}
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'));
await this.ensureDetailsClosed(fieldName);
}
@ -321,10 +350,14 @@ export function MachineLearningDataVisualizerTableProvider(
await this.assertKeywordFieldContents(fieldName, docCountFormatted, exampleCount);
} else if (fieldType === ML_JOB_FIELD_TYPES.TEXT) {
await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount);
} else if (fieldType === ML_JOB_FIELD_TYPES.GEO_POINT) {
await this.assertGeoPointFieldContents(fieldName, docCountFormatted, exampleCount);
} else if (fieldType === ML_JOB_FIELD_TYPES.UNKNOWN) {
await this.assertUnknownFieldContents(fieldName, docCountFormatted);
}
}
public async ensureNumRowsPerPage(n: 10 | 25 | 100) {
public async ensureNumRowsPerPage(n: 10 | 25 | 50) {
const paginationButton = 'mlDataVisualizerTable > tablePaginationPopoverButton';
await retry.tryForTime(10000, async () => {
await testSubjects.existOrFail(paginationButton);