[ML] Data Visualizer: Remove duplicated geo examples, support 'version' type, add filters for boolean fields, and add sticky header to Discover (#136236)

* [ML] Add filter to boolean content

* [ML] Change to version type if detected from estypes

* [ML] Remove duplicated geo examples

* [ML] Change duplicated geo util to duplicated generic util

* [ML] Use name for data view instead of title

* [ML] Add sticky header for field stats table in Discover

* [ML] Move unknown to bottom, rename JOB_FIELD_TYPES
This commit is contained in:
Quynh Nguyen 2022-07-14 15:18:00 -05:00 committed by GitHub
parent 4f13ea8435
commit 2a6f8e8130
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 433 additions and 192 deletions

View file

@ -29,16 +29,17 @@ export const FILE_FORMATS = {
// XML: 'xml',
};
export const JOB_FIELD_TYPES = {
export const SUPPORTED_FIELD_TYPES = {
BOOLEAN: 'boolean',
DATE: 'date',
GEO_POINT: 'geo_point',
GEO_SHAPE: 'geo_shape',
HISTOGRAM: 'histogram',
IP: 'ip',
KEYWORD: 'keyword',
NUMBER: 'number',
TEXT: 'text',
HISTOGRAM: 'histogram',
VERSION: 'version',
UNKNOWN: 'unknown',
} as const;

View file

@ -29,6 +29,16 @@ export interface DocumentCounts {
interval?: number;
}
export interface LatLongExample {
lat: number;
lon: number;
}
export interface GeoPointExample {
coordinates: number[];
type?: string;
}
export interface FieldVisStats {
error?: Error;
cardinality?: number;
@ -56,7 +66,7 @@ export interface FieldVisStats {
topValues?: Array<{ key: number | string; doc_count: number }>;
topValuesSampleSize?: number;
topValuesSamplerShardSize?: number;
examples?: Array<string | object>;
examples?: Array<string | GeoPointExample | object>;
timeRangeEarliest?: number;
timeRangeLatest?: number;
}

View file

@ -5,5 +5,5 @@
* 2.0.
*/
import { JOB_FIELD_TYPES } from '../constants';
export type JobFieldType = typeof JOB_FIELD_TYPES[keyof typeof JOB_FIELD_TYPES];
import { SUPPORTED_FIELD_TYPES } from '../constants';
export type JobFieldType = typeof SUPPORTED_FIELD_TYPES[keyof typeof SUPPORTED_FIELD_TYPES];

View file

@ -10,12 +10,19 @@ import React, { FC } from 'react';
import { EuiListGroup, EuiListGroupItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { GeoPointExample } from '../../../../../common/types/field_request_config';
import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header';
import { ExpandedRowPanel } from '../stats_table/components/field_data_expanded_row/expanded_row_panel';
interface Props {
examples: Array<string | object>;
examples: Array<string | GeoPointExample | object>;
}
const EMPTY_EXAMPLE = i18n.translate(
'xpack.dataVisualizer.dataGrid.field.examplesList.emptyExampleMessage',
{ defaultMessage: '(empty)' }
);
export const ExamplesList: FC<Props> = ({ examples }) => {
if (examples === undefined || examples === null || !Array.isArray(examples)) {
return null;
@ -34,7 +41,13 @@ export const ExamplesList: FC<Props> = ({ examples }) => {
<EuiListGroupItem
size="xs"
key={`example_${i}`}
label={typeof example === 'string' ? example : JSON.stringify(example)}
label={
typeof example === 'string'
? example === ''
? EMPTY_EXAMPLE
: example
: JSON.stringify(example)
}
/>
);
});

View file

@ -16,7 +16,7 @@ import {
NumberContent,
} from '../stats_table/components/field_data_expanded_row';
import { GeoPointContent } from './geo_point_content/geo_point_content';
import { JOB_FIELD_TYPES } from '../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config';
export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFieldVisConfig }) => {
@ -25,25 +25,26 @@ export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFi
function getCardContent() {
switch (type) {
case JOB_FIELD_TYPES.NUMBER:
case SUPPORTED_FIELD_TYPES.NUMBER:
return <NumberContent config={config} />;
case JOB_FIELD_TYPES.BOOLEAN:
case SUPPORTED_FIELD_TYPES.BOOLEAN:
return <BooleanContent config={config} />;
case JOB_FIELD_TYPES.DATE:
case SUPPORTED_FIELD_TYPES.DATE:
return <DateContent config={config} />;
case JOB_FIELD_TYPES.GEO_POINT:
case SUPPORTED_FIELD_TYPES.GEO_POINT:
return <GeoPointContent config={config} />;
case JOB_FIELD_TYPES.IP:
case SUPPORTED_FIELD_TYPES.IP:
return <IpContent config={config} />;
case JOB_FIELD_TYPES.KEYWORD:
case SUPPORTED_FIELD_TYPES.KEYWORD:
case SUPPORTED_FIELD_TYPES.VERSION:
return <KeywordContent config={config} />;
case JOB_FIELD_TYPES.TEXT:
case SUPPORTED_FIELD_TYPES.TEXT:
return <TextContent config={config} />;
default:

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FC, useEffect, useState } from 'react';
import { DataView } from '@kbn/data-views-plugin/public';
import { ES_GEO_FIELD_TYPE, LayerDescriptor } from '@kbn/maps-plugin/common';
@ -14,7 +13,7 @@ import { DocumentStatsTable } from '../../stats_table/components/field_data_expa
import { ExamplesList } from '../../examples_list';
import { FieldVisConfig } from '../../stats_table/types';
import { useDataVisualizerKibana } from '../../../../kibana_context';
import { JOB_FIELD_TYPES } from '../../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants';
import { EmbeddedMapComponent } from '../../embedded_map';
import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel';
@ -36,7 +35,8 @@ export const GeoPointContentWithMap: FC<{
dataView?.id !== undefined &&
config !== undefined &&
config.fieldName !== undefined &&
(config.type === JOB_FIELD_TYPES.GEO_POINT || config.type === JOB_FIELD_TYPES.GEO_SHAPE)
(config.type === SUPPORTED_FIELD_TYPES.GEO_POINT ||
config.type === SUPPORTED_FIELD_TYPES.GEO_SHAPE)
) {
const params = {
indexPatternId: dataView.id,
@ -64,7 +64,7 @@ export const GeoPointContentWithMap: FC<{
return (
<ExpandedRowContent dataTestSubj={'dataVisualizerIndexBasedMapContent'}>
<DocumentStatsTable config={config} />
<ExamplesList examples={stats.examples} />
<ExamplesList examples={stats?.examples} />
<ExpandedRowPanel className={'dvPanel__wrapper dvMap__wrapper'} grow={true}>
<EmbeddedMapComponent layerList={layerList} />
</ExpandedRowPanel>

View file

@ -8,7 +8,7 @@
import React from 'react';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { GeoPointContentWithMap } from './geo_point_content_with_map';
import { JOB_FIELD_TYPES } from '../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
import {
BooleanContent,
DateContent,
@ -51,17 +51,17 @@ export const IndexBasedDataVisualizerExpandedRow = ({
}
switch (type) {
case JOB_FIELD_TYPES.NUMBER:
case SUPPORTED_FIELD_TYPES.NUMBER:
return <NumberContent config={config} onAddFilter={onAddFilter} />;
case JOB_FIELD_TYPES.BOOLEAN:
return <BooleanContent config={config} />;
case SUPPORTED_FIELD_TYPES.BOOLEAN:
return <BooleanContent config={config} onAddFilter={onAddFilter} />;
case JOB_FIELD_TYPES.DATE:
case SUPPORTED_FIELD_TYPES.DATE:
return <DateContent config={config} />;
case JOB_FIELD_TYPES.GEO_POINT:
case JOB_FIELD_TYPES.GEO_SHAPE:
case SUPPORTED_FIELD_TYPES.GEO_POINT:
case SUPPORTED_FIELD_TYPES.GEO_SHAPE:
return (
<GeoPointContentWithMap
config={config}
@ -70,13 +70,14 @@ export const IndexBasedDataVisualizerExpandedRow = ({
/>
);
case JOB_FIELD_TYPES.IP:
case SUPPORTED_FIELD_TYPES.IP:
return <IpContent config={config} onAddFilter={onAddFilter} />;
case JOB_FIELD_TYPES.KEYWORD:
case SUPPORTED_FIELD_TYPES.KEYWORD:
case SUPPORTED_FIELD_TYPES.VERSION:
return <KeywordContent config={config} onAddFilter={onAddFilter} />;
case JOB_FIELD_TYPES.TEXT:
case SUPPORTED_FIELD_TYPES.TEXT:
return <TextContent config={config} />;
default:

View file

@ -18,7 +18,7 @@ import {
dataVisualizerRefresh$,
Refresh,
} from '../../../../index_data_visualizer/services/timefilter_refresh_service';
import { JOB_FIELD_TYPES } from '../../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants';
import { APP_ID } from '../../../../../../common/constants';
export function getActions(
@ -80,7 +80,10 @@ export function getActions(
type: 'icon',
icon: 'gisApp',
available: (item: FieldVisConfig) => {
return item.type === JOB_FIELD_TYPES.GEO_POINT || item.type === JOB_FIELD_TYPES.GEO_SHAPE;
return (
item.type === SUPPORTED_FIELD_TYPES.GEO_POINT ||
item.type === SUPPORTED_FIELD_TYPES.GEO_SHAPE
);
},
onClick: async (item: FieldVisConfig) => {
if (services?.uiActions && dataView) {

View file

@ -19,7 +19,7 @@ import type {
import { DOCUMENT_FIELD_NAME as RECORDS_FIELD } from '@kbn/lens-plugin/common/constants';
import type { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import { FieldVisConfig } from '../../stats_table/types';
import { JOB_FIELD_TYPES } from '../../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants';
interface ColumnsAndLayer {
columns: Record<string, GenericIndexPatternColumn>;
@ -200,19 +200,20 @@ export function getBooleanSettings(item: FieldVisConfig) {
export function getCompatibleLensDataType(type: FieldVisConfig['type']): string | undefined {
let lensType: string | undefined;
switch (type) {
case JOB_FIELD_TYPES.KEYWORD:
case SUPPORTED_FIELD_TYPES.KEYWORD:
case SUPPORTED_FIELD_TYPES.VERSION:
lensType = 'string';
break;
case JOB_FIELD_TYPES.DATE:
case SUPPORTED_FIELD_TYPES.DATE:
lensType = 'date';
break;
case JOB_FIELD_TYPES.NUMBER:
case SUPPORTED_FIELD_TYPES.NUMBER:
lensType = 'number';
break;
case JOB_FIELD_TYPES.IP:
case SUPPORTED_FIELD_TYPES.IP:
lensType = 'ip';
break;
case JOB_FIELD_TYPES.BOOLEAN:
case SUPPORTED_FIELD_TYPES.BOOLEAN:
lensType = 'string';
break;
default:
@ -228,16 +229,20 @@ function getColumnsAndLayer(
): ColumnsAndLayer | undefined {
if (item.fieldName === undefined) return;
if (fieldType === JOB_FIELD_TYPES.DATE) {
if (fieldType === SUPPORTED_FIELD_TYPES.DATE) {
return getDateSettings(item);
}
if (fieldType === JOB_FIELD_TYPES.NUMBER) {
if (fieldType === SUPPORTED_FIELD_TYPES.NUMBER) {
return getNumberSettings(item, defaultDataView);
}
if (fieldType === JOB_FIELD_TYPES.IP || fieldType === JOB_FIELD_TYPES.KEYWORD) {
if (
fieldType === SUPPORTED_FIELD_TYPES.IP ||
fieldType === SUPPORTED_FIELD_TYPES.KEYWORD ||
fieldType === SUPPORTED_FIELD_TYPES.VERSION
) {
return getKeywordSettings(item);
}
if (fieldType === JOB_FIELD_TYPES.BOOLEAN) {
if (fieldType === SUPPORTED_FIELD_TYPES.BOOLEAN) {
return getBooleanSettings(item);
}
}

View file

@ -9,12 +9,12 @@ import React from 'react';
import { mount, shallow } from 'enzyme';
import { FieldTypeIcon } from './field_type_icon';
import { JOB_FIELD_TYPES } from '../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
describe('FieldTypeIcon', () => {
test(`render component when type matches a field type`, () => {
const typeIconComponent = shallow(
<FieldTypeIcon type={JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} />
<FieldTypeIcon type={SUPPORTED_FIELD_TYPES.KEYWORD} tooltipEnabled={true} />
);
expect(typeIconComponent).toMatchSnapshot();
});
@ -24,7 +24,7 @@ describe('FieldTypeIcon', () => {
jest.useFakeTimers();
const typeIconComponent = mount(
<FieldTypeIcon type={JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} />
<FieldTypeIcon type={SUPPORTED_FIELD_TYPES.KEYWORD} tooltipEnabled={true} />
);
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);

View file

@ -8,7 +8,7 @@
import { FindFileStructureResponse } from '@kbn/file-upload-plugin/common';
import { getFieldNames, getSupportedFieldType } from './get_field_names';
import { FileBasedFieldVisConfig } from '../stats_table/types';
import { JOB_FIELD_TYPES } from '../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
import { roundToDecimalPlace } from '../utils';
export function createFields(results: FindFileStructureResponse) {
@ -28,20 +28,20 @@ export function createFields(results: FindFileStructureResponse) {
if (fieldStats[name] !== undefined) {
const field: FileBasedFieldVisConfig = {
fieldName: name,
type: JOB_FIELD_TYPES.UNKNOWN,
type: SUPPORTED_FIELD_TYPES.UNKNOWN,
};
const f = fieldStats[name];
const m = mappings.properties[name];
// sometimes the timestamp field is not in the mappings, and so our
// collection of fields will be missing a time field with a type of date
if (name === timestampField && field.type === JOB_FIELD_TYPES.UNKNOWN) {
field.type = JOB_FIELD_TYPES.DATE;
if (name === timestampField && field.type === SUPPORTED_FIELD_TYPES.UNKNOWN) {
field.type = SUPPORTED_FIELD_TYPES.DATE;
}
if (m !== undefined) {
field.type = getSupportedFieldType(m.type);
if (field.type === JOB_FIELD_TYPES.NUMBER) {
if (field.type === SUPPORTED_FIELD_TYPES.NUMBER) {
numericFieldsCount += 1;
}
if (m.format !== undefined) {
@ -71,7 +71,7 @@ export function createFields(results: FindFileStructureResponse) {
}
if (f.top_hits !== undefined) {
if (field.type === JOB_FIELD_TYPES.TEXT) {
if (field.type === SUPPORTED_FIELD_TYPES.TEXT) {
_stats = {
..._stats,
examples: f.top_hits.map((hit) => hit.value),
@ -84,7 +84,7 @@ export function createFields(results: FindFileStructureResponse) {
}
}
if (field.type === JOB_FIELD_TYPES.DATE) {
if (field.type === SUPPORTED_FIELD_TYPES.DATE) {
_stats = {
..._stats,
earliest: f.earliest,
@ -99,9 +99,9 @@ export function createFields(results: FindFileStructureResponse) {
// this could be the message field for a semi-structured log file or a
// field which the endpoint has not been able to work out any information for
const type =
mappings.properties[name] && mappings.properties[name].type === JOB_FIELD_TYPES.TEXT
? JOB_FIELD_TYPES.TEXT
: JOB_FIELD_TYPES.UNKNOWN;
mappings.properties[name] && mappings.properties[name].type === SUPPORTED_FIELD_TYPES.TEXT
? SUPPORTED_FIELD_TYPES.TEXT
: SUPPORTED_FIELD_TYPES.UNKNOWN;
return {
fieldName: name,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { JOB_FIELD_TYPES } from '../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
interface CommonFieldConfig {
type: string;
@ -32,6 +32,6 @@ export function filterFields<T extends CommonFieldConfig>(
return {
filteredFields: items,
visibleFieldsCount: items.length,
visibleMetricsCount: items.filter((d) => d.type === JOB_FIELD_TYPES.NUMBER).length,
visibleMetricsCount: items.filter((d) => d.type === SUPPORTED_FIELD_TYPES.NUMBER).length,
};
}

View file

@ -9,7 +9,7 @@ import { difference } from 'lodash';
import { ES_FIELD_TYPES } from '@kbn/data-plugin/common';
import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common';
import type { JobFieldType } from '../../../../../common/types';
import { JOB_FIELD_TYPES } from '../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
export function getFieldNames(results: FindFileStructureResponse) {
const { mappings, field_stats: fieldStats, column_names: columnNames } = results;
@ -44,11 +44,11 @@ export function getSupportedFieldType(type: string): JobFieldType {
case ES_FIELD_TYPES.LONG:
case ES_FIELD_TYPES.SHORT:
case ES_FIELD_TYPES.UNSIGNED_LONG:
return JOB_FIELD_TYPES.NUMBER;
return SUPPORTED_FIELD_TYPES.NUMBER;
case ES_FIELD_TYPES.DATE:
case ES_FIELD_TYPES.DATE_NANOS:
return JOB_FIELD_TYPES.DATE;
return SUPPORTED_FIELD_TYPES.DATE;
default:
return type as JobFieldType;

View file

@ -31,9 +31,8 @@ $panelWidthL: #{'max(40%, 450px)'};
}
.euiTableRow > .euiTableRowCell {
border-bottom: 0;
border-top: $euiBorderThin;
border-top: 0;
border-bottom: $euiBorderThin;
}
.euiTableCellContent {

View file

@ -5,18 +5,13 @@
* 2.0.
*/
import React, { FC, ReactNode, useMemo } from 'react';
import {
EuiBasicTable,
EuiSpacer,
RIGHT_ALIGNMENT,
LEFT_ALIGNMENT,
HorizontalAlignment,
} from '@elastic/eui';
import React, { FC, useMemo } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { Axis, BarSeries, Chart, Settings, ScaleType } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { TopValues } from '../../../top_values';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { getTFPercentage } from '../../utils';
@ -44,72 +39,42 @@ function getFormattedValue(value: number, totalCount: number): string {
const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 70;
export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
export const BooleanContent: FC<FieldDataRowProps> = ({ config, onAddFilter }) => {
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
const formattedPercentages = useMemo(() => getTFPercentage(config), [config]);
const theme = useDataVizChartTheme();
if (!formattedPercentages) return null;
const { trueCount, falseCount, count } = formattedPercentages;
const summaryTableItems = [
{
function: 'true',
display: (
<FormattedMessage
id="xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.trueCountLabel"
defaultMessage="true"
/>
),
value: getFormattedValue(trueCount, count),
},
{
function: 'false',
display: (
<FormattedMessage
id="xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.falseCountLabel"
defaultMessage="false"
/>
),
value: getFormattedValue(falseCount, count),
},
];
const summaryTableColumns = [
{
field: 'function',
name: '',
render: (_: string, summaryItem: { display: ReactNode }) => summaryItem.display,
width: '25px',
align: LEFT_ALIGNMENT as HorizontalAlignment,
},
{
field: 'value',
name: '',
render: (v: string) => <strong>{v}</strong>,
align: RIGHT_ALIGNMENT as HorizontalAlignment,
},
];
const summaryTableTitle = i18n.translate(
'xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.summaryTableTitle',
{
defaultMessage: 'Summary',
}
);
const stats = {
...config.stats,
topValues: [
{
key: i18n.translate(
'xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.trueCountLabel',
{ defaultMessage: 'true' }
),
doc_count: trueCount ?? 0,
},
{
key: i18n.translate(
'xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.falseCountLabel',
{ defaultMessage: 'false' }
),
doc_count: falseCount ?? 0,
},
],
};
return (
<ExpandedRowContent dataTestSubj={'dataVisualizerBooleanContent'}>
<DocumentStatsTable config={config} />
<ExpandedRowPanel className={'dvSummaryTable__wrapper dvPanel__wrapper'}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable
className={'dvSummaryTable'}
compressed
items={summaryTableItems}
columns={summaryTableColumns}
tableCaption={summaryTableTitle}
/>
</ExpandedRowPanel>
<TopValues
stats={stats}
fieldFormat={fieldFormat}
barColor="success"
onAddFilter={onAddFilter}
/>
<ExpandedRowPanel className={'dvPanel__wrapper dvPanel--uniform'}>
<ExpandedRowFieldHeader>

View file

@ -20,11 +20,13 @@ import {
RIGHT_ALIGNMENT,
EuiResizeObserver,
EuiLoadingSpinner,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiTableComputedColumnType } from '@elastic/eui/src/components/basic_table/table_types';
import { throttle } from 'lodash';
import { JOB_FIELD_TYPES } from '../../../../../common/constants';
import { css } from '@emotion/react';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
import type { JobFieldType, DataVisualizerTableState } from '../../../../../common/types';
import { DocumentStat } from './components/field_data_row/document_stats';
import { IndexBasedNumberContentPreview } from './components/field_data_row/number_content_preview';
@ -70,6 +72,8 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
onChange,
loading,
}: DataVisualizerTableProps<T>) => {
const { euiTheme } = useEuiTheme();
const [expandedRowItemIds, setExpandedRowItemIds] = useState<string[]>([]);
const [expandAll, setExpandAll] = useState<boolean>(false);
@ -289,13 +293,14 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
}
if (
(item.type === JOB_FIELD_TYPES.KEYWORD || item.type === JOB_FIELD_TYPES.IP) &&
(item.type === SUPPORTED_FIELD_TYPES.KEYWORD ||
item.type === SUPPORTED_FIELD_TYPES.IP) &&
item.stats?.topValues !== undefined
) {
return <TopValuesPreview config={item} />;
}
if (item.type === JOB_FIELD_TYPES.NUMBER) {
if (item.type === SUPPORTED_FIELD_TYPES.NUMBER) {
if (isIndexBasedFieldVisConfig(item) && item.stats?.distribution !== undefined) {
// If the cardinality is only low, show the top values instead of a distribution chart
return item.stats?.distribution?.percentiles.length <= 2 ? (
@ -308,7 +313,7 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
}
}
if (item.type === JOB_FIELD_TYPES.BOOLEAN) {
if (item.type === SUPPORTED_FIELD_TYPES.BOOLEAN) {
return <BooleanContentPreview config={item} />;
}
@ -361,6 +366,18 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
rowProps={(item) => ({
'data-test-subj': `dataVisualizerRow row-${item.fieldName}`,
})}
css={css`
thead {
position: sticky;
inset-block-start: 0;
z-index: 1;
background-color: ${euiTheme.colors.emptyShade};
box-shadow: inset 0 0px 0, inset 0 -1px 0 ${euiTheme.border.color};
}
.euiTableRow > .euiTableRowCel {
border-top: 0px;
}
`}
/>
</div>
)}

View file

@ -0,0 +1,153 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getUniqGeoOrStrExamples } from './example_utils';
describe('example utils', () => {
describe('getUniqGeoOrStrExamples', () => {
test('should remove duplicated strings up to maxExamples', () => {
expect(
getUniqGeoOrStrExamples(
[
'deb',
'',
'css',
'deb',
'',
'',
'deb',
'gz',
'',
'gz',
'',
'deb',
'gz',
'deb',
'',
'deb',
'deb',
'',
'gz',
'gz',
],
20
)
).toMatchObject(['deb', '', 'css', 'gz']);
expect(
getUniqGeoOrStrExamples(
[
'deb',
'',
'css',
'deb',
'',
'',
'deb',
'gz',
'',
'gz',
'',
'deb',
'gz',
'deb',
'',
'deb',
'deb',
'',
'gz',
'gz',
],
2
)
).toMatchObject(['deb', '']);
});
test('should remove duplicated coordinates up to maxExamples', () => {
expect(
getUniqGeoOrStrExamples([
{ coordinates: [0.1, 2343], type: 'Point' },
{ coordinates: [0.1, 2343], type: 'Point' },
{ coordinates: [0.1, 2343], type: 'Point' },
{ coordinates: [0.1, 2343], type: 'Shape' },
{ coordinates: [0.1, 2343] },
{ coordinates: [4321, 2343], type: 'Point' },
{ coordinates: [4321, 2343], type: 'Point' },
])
).toMatchObject([
{
coordinates: [0.1, 2343],
type: 'Point',
},
{
coordinates: [0.1, 2343],
type: 'Shape',
},
{
coordinates: [0.1, 2343],
},
{
coordinates: [4321, 2343],
type: 'Point',
},
]);
expect(
getUniqGeoOrStrExamples([
{ coordinates: [1, 2, 3], type: 'Point' },
{ coordinates: [1, 2, 3], type: 'Point' },
{ coordinates: [1, 2, 3], type: 'Point' },
{ coordinates: [1, 2, 3, 4], type: 'Shape' },
{ coordinates: [1, 2, 3, 4] },
])
).toMatchObject([
{
coordinates: [1, 2, 3],
type: 'Point',
},
{ coordinates: [1, 2, 3, 4], type: 'Shape' },
{ coordinates: [1, 2, 3, 4] },
]);
});
test('should remove duplicated lon/lat coordinates up to maxExamples', () => {
expect(
getUniqGeoOrStrExamples([
{ lon: 0.1, lat: 2343 },
{ lon: 0.1, lat: 2343 },
{ lon: 0.1, lat: 2343 },
{ lon: 0.1, lat: 2343 },
{ lon: 0.1, lat: 2343 },
{ lon: 4321, lat: 2343 },
{ lon: 4321, lat: 2343 },
])
).toMatchObject([
{ lon: 0.1, lat: 2343 },
{ lon: 4321, lat: 2343 },
]);
expect(
getUniqGeoOrStrExamples(
[
{ lon: 1, lat: 2 },
{ lon: 1, lat: 2 },
{ lon: 2, lat: 3 },
{ lon: 2, lat: 3 },
{ lon: 3, lat: 4 },
{ lon: 3, lat: 4 },
{ lon: 4, lat: 5 },
{ lon: 4, lat: 5 },
{ lon: 5, lat: 6 },
{ lon: 5, lat: 6 },
],
3
)
).toMatchObject([
{ lon: 1, lat: 2 },
{ lon: 2, lat: 3 },
{ lon: 3, lat: 4 },
]);
});
});
});

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from './is_defined';
import { GeoPointExample, LatLongExample } from '../../../../common/types/field_request_config';
export function isGeoPointExample(arg: unknown): arg is GeoPointExample {
return isPopulatedObject(arg, ['coordinates']) && Array.isArray(arg.coordinates);
}
export function isLonLatExample(arg: unknown): arg is LatLongExample {
return isPopulatedObject(arg, ['lon', 'lat']);
}
export function getUniqGeoOrStrExamples(
examples: Array<string | GeoPointExample | LatLongExample | object> | undefined,
maxExamples = 10
): Array<string | GeoPointExample | LatLongExample | object> {
const uniqueCoordinates: Array<string | GeoPointExample | LatLongExample | object> = [];
if (!isDefined(examples)) return uniqueCoordinates;
for (let i = 0; i < examples.length; i++) {
const example = examples[i];
if (typeof example === 'string' && uniqueCoordinates.indexOf(example) === -1) {
uniqueCoordinates.push(example);
} else {
if (
isGeoPointExample(example) &&
uniqueCoordinates.findIndex(
(c) =>
isGeoPointExample(c) &&
c.type === example.type &&
example.coordinates.every((coord, idx) => coord === c.coordinates[idx])
) === -1
) {
uniqueCoordinates.push(example);
}
if (
isLonLatExample(example) &&
uniqueCoordinates.findIndex(
(c) => isLonLatExample(c) && example.lon === c.lon && example.lat === c.lat
) === -1
) {
uniqueCoordinates.push(example);
}
}
if (uniqueCoordinates.length === maxExamples) {
return uniqueCoordinates;
}
}
return uniqueCoordinates;
}

View file

@ -5,24 +5,26 @@
* 2.0.
*/
import { JOB_FIELD_TYPES } from '../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../common/constants';
import { getJobTypeLabel, jobTypeLabels } from './field_types_utils';
describe('field type utils', () => {
describe('getJobTypeLabel: Getting a field type aria label by passing what it is stored in constants', () => {
test('should returns all JOB_FIELD_TYPES labels exactly as it is for each correct value', () => {
const keys = Object.keys(JOB_FIELD_TYPES);
test('should returns all SUPPORTED_FIELD_TYPES labels exactly as it is for each correct value', () => {
const keys = Object.keys(SUPPORTED_FIELD_TYPES);
const receivedLabels: Record<string, string | null> = {};
const testStorage = jobTypeLabels;
keys.forEach((key) => {
const constant = key as keyof typeof JOB_FIELD_TYPES;
receivedLabels[JOB_FIELD_TYPES[constant]] = getJobTypeLabel(JOB_FIELD_TYPES[constant]);
const constant = key as keyof typeof SUPPORTED_FIELD_TYPES;
receivedLabels[SUPPORTED_FIELD_TYPES[constant]] = getJobTypeLabel(
SUPPORTED_FIELD_TYPES[constant]
);
});
expect(receivedLabels).toEqual(testStorage);
});
test('should returns NULL as JOB_FIELD_TYPES does not contain such a keyword', () => {
expect(getJobTypeLabel('JOB_FIELD_TYPES')).toBe(null);
test('should returns NULL as SUPPORTED_FIELD_TYPES does not contain such a keyword', () => {
expect(getJobTypeLabel('SUPPORTED_FIELD_TYPES')).toBe(null);
});
});
});

View file

@ -8,52 +8,70 @@
import { i18n } from '@kbn/i18n';
import { DataViewField } from '@kbn/data-views-plugin/public';
import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common';
import { JOB_FIELD_TYPES } from '../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../common/constants';
export const getJobTypeLabel = (type: string) => {
return type in jobTypeLabels ? jobTypeLabels[type as keyof typeof jobTypeLabels] : null;
};
export const jobTypeLabels = {
[JOB_FIELD_TYPES.BOOLEAN]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.booleanTypeLabel', {
defaultMessage: 'Boolean',
}),
[JOB_FIELD_TYPES.DATE]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.dateTypeLabel', {
[SUPPORTED_FIELD_TYPES.BOOLEAN]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.booleanTypeLabel',
{
defaultMessage: 'Boolean',
}
),
[SUPPORTED_FIELD_TYPES.DATE]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.dateTypeLabel', {
defaultMessage: 'Date',
}),
[JOB_FIELD_TYPES.GEO_POINT]: i18n.translate(
[SUPPORTED_FIELD_TYPES.GEO_POINT]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.geoPointTypeLabel',
{
defaultMessage: 'Geo point',
}
),
[JOB_FIELD_TYPES.GEO_SHAPE]: i18n.translate(
[SUPPORTED_FIELD_TYPES.GEO_SHAPE]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeLabel',
{
defaultMessage: 'Geo shape',
}
),
[JOB_FIELD_TYPES.IP]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeLabel', {
[SUPPORTED_FIELD_TYPES.IP]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeLabel', {
defaultMessage: 'IP',
}),
[JOB_FIELD_TYPES.KEYWORD]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.keywordTypeLabel', {
defaultMessage: 'Keyword',
}),
[JOB_FIELD_TYPES.NUMBER]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeLabel', {
defaultMessage: 'Number',
}),
[JOB_FIELD_TYPES.HISTOGRAM]: i18n.translate(
[SUPPORTED_FIELD_TYPES.KEYWORD]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.keywordTypeLabel',
{
defaultMessage: 'Keyword',
}
),
[SUPPORTED_FIELD_TYPES.NUMBER]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.numberTypeLabel',
{
defaultMessage: 'Number',
}
),
[SUPPORTED_FIELD_TYPES.HISTOGRAM]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.histogramTypeLabel',
{
defaultMessage: 'Histogram',
}
),
[JOB_FIELD_TYPES.TEXT]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeLabel', {
[SUPPORTED_FIELD_TYPES.TEXT]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeLabel', {
defaultMessage: 'Text',
}),
[JOB_FIELD_TYPES.UNKNOWN]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.unknownTypeLabel', {
defaultMessage: 'Unknown',
}),
[SUPPORTED_FIELD_TYPES.UNKNOWN]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.unknownTypeLabel',
{
defaultMessage: 'Unknown',
}
),
[SUPPORTED_FIELD_TYPES.VERSION]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.versionTypeLabel',
{
defaultMessage: 'Version',
}
),
};
// convert kibana types to ML Job types
@ -62,30 +80,35 @@ export const jobTypeLabels = {
export function kbnTypeToJobType(field: DataViewField) {
// Return undefined if not one of the supported data visualizer field types.
let type;
switch (field.type) {
case KBN_FIELD_TYPES.STRING:
type = field.aggregatable ? JOB_FIELD_TYPES.KEYWORD : JOB_FIELD_TYPES.TEXT;
type = field.aggregatable ? SUPPORTED_FIELD_TYPES.KEYWORD : SUPPORTED_FIELD_TYPES.TEXT;
if (field.esTypes?.includes(SUPPORTED_FIELD_TYPES.VERSION)) {
type = SUPPORTED_FIELD_TYPES.VERSION;
}
break;
case KBN_FIELD_TYPES.NUMBER:
type = JOB_FIELD_TYPES.NUMBER;
type = SUPPORTED_FIELD_TYPES.NUMBER;
break;
case KBN_FIELD_TYPES.DATE:
type = JOB_FIELD_TYPES.DATE;
type = SUPPORTED_FIELD_TYPES.DATE;
break;
case KBN_FIELD_TYPES.IP:
type = JOB_FIELD_TYPES.IP;
type = SUPPORTED_FIELD_TYPES.IP;
break;
case KBN_FIELD_TYPES.BOOLEAN:
type = JOB_FIELD_TYPES.BOOLEAN;
type = SUPPORTED_FIELD_TYPES.BOOLEAN;
break;
case KBN_FIELD_TYPES.GEO_POINT:
type = JOB_FIELD_TYPES.GEO_POINT;
type = SUPPORTED_FIELD_TYPES.GEO_POINT;
break;
case KBN_FIELD_TYPES.GEO_SHAPE:
type = JOB_FIELD_TYPES.GEO_SHAPE;
type = SUPPORTED_FIELD_TYPES.GEO_SHAPE;
break;
case KBN_FIELD_TYPES.HISTOGRAM:
type = JOB_FIELD_TYPES.HISTOGRAM;
type = SUPPORTED_FIELD_TYPES.HISTOGRAM;
break;
default:

View file

@ -399,7 +399,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
<EuiPageContentHeaderSection>
<div className="dataViewTitleHeader">
<EuiTitle size={'s'}>
<h2>{currentDataView.title}</h2>
<h2>{currentDataView.getName()}</h2>
</EuiTitle>
<DataVisualizerDataViewManagement
currentDataView={currentDataView}

View file

@ -20,7 +20,7 @@ import { dataVisualizerRefresh$ } from '../services/timefilter_refresh_service';
import { TimeBuckets } from '../../../../common/services/time_buckets';
import { FieldVisConfig } from '../../common/components/stats_table/types';
import {
JOB_FIELD_TYPES,
SUPPORTED_FIELD_TYPES,
NON_AGGREGATABLE_FIELD_TYPES,
OMIT_FIELDS,
} from '../../../../common/constants';
@ -319,7 +319,7 @@ export const useDataVisualizerGridData = (
const metricConfig: FieldVisConfig = {
...fieldData,
fieldFormat: currentDataView.getFormatterForField(field),
type: JOB_FIELD_TYPES.NUMBER,
type: SUPPORTED_FIELD_TYPES.NUMBER,
loading: fieldData?.existsInDocs ?? true,
aggregatable: true,
deletable: field.runtimeField !== undefined,
@ -394,7 +394,6 @@ export const useDataVisualizerGridData = (
nonMetricFieldsToShow.forEach((field) => {
const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
const nonMetricConfig: Partial<FieldVisConfig> = {
...(fieldData ? fieldData : {}),
fieldFormat: currentDataView.getFormatterForField(field),

View file

@ -15,6 +15,8 @@ import type {
ISearchStart,
} from '@kbn/data-plugin/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getUniqGeoOrStrExamples } from '../../../common/util/example_utils';
import { buildBaseFilterCriteria } from '../../../../../common/utils/query_utils';
import type {
Field,
@ -90,20 +92,11 @@ export const fetchFieldsExamples = (
if (body.hits.total > 0) {
const hits = body.hits.hits;
for (let i = 0; i < hits.length; i++) {
// Use lodash get() to support field names containing dots.
const doc: object[] | undefined = get(hits[i].fields, field.fieldName);
// 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;
}
}
}
}
const processedDocs = hits.map((hit: SearchHit) => {
const doc: object[] | undefined = get(hit.fields, field.fieldName);
return Array.isArray(doc) && doc.length > 0 ? doc[0] : doc;
});
stats.examples = getUniqGeoOrStrExamples(processedDocs, maxExamples);
}
return stats;

View file

@ -11,7 +11,7 @@ import { ISearchStart } from '@kbn/data-plugin/public';
import type { FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats';
import type { FieldStatsError } from '../../../../../common/types/field_stats';
import type { FieldStats } from '../../../../../common/types/field_stats';
import { JOB_FIELD_TYPES } from '../../../../../common/constants';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
import { fetchDateFieldsStats } from './get_date_field_stats';
import { fetchBooleanFieldsStats } from './get_boolean_field_stats';
import { fetchFieldsExamples } from './get_field_examples';
@ -31,16 +31,17 @@ export const getFieldsStats = (
): Observable<FieldStats[] | FieldStatsError> | undefined => {
const fieldType = fields[0].type;
switch (fieldType) {
case JOB_FIELD_TYPES.NUMBER:
case SUPPORTED_FIELD_TYPES.NUMBER:
return fetchNumericFieldsStats(dataSearch, params, fields, options);
case JOB_FIELD_TYPES.KEYWORD:
case JOB_FIELD_TYPES.IP:
case SUPPORTED_FIELD_TYPES.KEYWORD:
case SUPPORTED_FIELD_TYPES.IP:
case SUPPORTED_FIELD_TYPES.VERSION:
return fetchStringFieldsStats(dataSearch, params, fields, options);
case JOB_FIELD_TYPES.DATE:
case SUPPORTED_FIELD_TYPES.DATE:
return fetchDateFieldsStats(dataSearch, params, fields, options);
case JOB_FIELD_TYPES.BOOLEAN:
case SUPPORTED_FIELD_TYPES.BOOLEAN:
return fetchBooleanFieldsStats(dataSearch, params, fields, options);
case JOB_FIELD_TYPES.TEXT:
case SUPPORTED_FIELD_TYPES.TEXT:
return fetchFieldsExamples(dataSearch, params, fields, options);
default:
// Use an exists filter on the the field name to get

View file

@ -10516,7 +10516,6 @@
"xpack.dataVisualizer.dataGrid.field.topValues.calculatedFromSampleDescription": "Calculé à partir d'un échantillon de {topValuesSamplerShardSize} documents par partition",
"xpack.dataVisualizer.dataGrid.field.topValuesLabel": "Valeurs les plus élevées",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.falseCountLabel": "faux",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.summaryTableTitle": "Résumé",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.trueCountLabel": "vrai",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.choroplethMapTopValues.calculatedFromSampleDescription": "Calculé à partir d'un échantillon de {topValuesSamplerShardSize} documents par partition",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.documentStatsTable.countLabel": "compte",

View file

@ -10508,7 +10508,6 @@
"xpack.dataVisualizer.dataGrid.field.topValues.calculatedFromSampleDescription": "1 つのシャードにつき {topValuesSamplerShardSize} のドキュメントのサンプルで計算されています",
"xpack.dataVisualizer.dataGrid.field.topValuesLabel": "トップの値",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.falseCountLabel": "false",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.summaryTableTitle": "まとめ",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.trueCountLabel": "true",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.choroplethMapTopValues.calculatedFromSampleDescription": "1 つのシャードにつき {topValuesSamplerShardSize} のドキュメントのサンプルで計算されています",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.documentStatsTable.countLabel": "カウント",

View file

@ -10523,7 +10523,6 @@
"xpack.dataVisualizer.dataGrid.field.topValues.calculatedFromSampleDescription": "基于每个分片的 {topValuesSamplerShardSize} 文档样例计算",
"xpack.dataVisualizer.dataGrid.field.topValuesLabel": "排名最前值",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.falseCountLabel": "false",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.summaryTableTitle": "摘要",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.booleanContent.trueCountLabel": "true",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.choroplethMapTopValues.calculatedFromSampleDescription": "基于每个分片的 {topValuesSamplerShardSize} 文档样例计算",
"xpack.dataVisualizer.dataGrid.fieldExpandedRow.documentStatsTable.countLabel": "计数",