mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Add embedded map to geo_point fields for Data Visualizer (#88880)
This commit is contained in:
parent
723dd32693
commit
da9ad2ade4
46 changed files with 842 additions and 223 deletions
|
@ -68,7 +68,7 @@ import {
|
|||
MapEmbeddableInput,
|
||||
MapEmbeddableOutput,
|
||||
} from './types';
|
||||
export { MapEmbeddableInput };
|
||||
export { MapEmbeddableInput, MapEmbeddableOutput };
|
||||
|
||||
export class MapEmbeddable
|
||||
extends Embeddable<MapEmbeddableInput, MapEmbeddableOutput>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'ml_embedded_map';
|
|
@ -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
|
||||
}
|
|
@ -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';
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 & {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -1 +1 @@
|
|||
@import 'file_datavisualizer_view'
|
||||
@import 'file_datavisualizer_view';
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
@import 'number_content';
|
||||
|
||||
.mlDataVisualizerExpandedRow {
|
||||
@include euiBreakpoint('xs', 's', 'm') {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.mlDataVisualizerColumnHeaderIcon {
|
||||
max-width: $euiSizeM;
|
||||
}
|
|
@ -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'}>
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": "日付タイプ",
|
||||
|
|
|
@ -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": "日期类型",
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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,
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue