[ML] Redesign file-based Data Visualizer (#87598)

This commit is contained in:
Quynh Nguyen 2021-01-20 10:52:03 -06:00 committed by GitHub
parent 86789dabb5
commit 27c77adf60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
98 changed files with 1721 additions and 1199 deletions

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common';
export interface InputOverrides {
[key: string]: string;
}
@ -29,15 +31,27 @@ export interface FindFileStructureResponse {
count: number;
cardinality: number;
top_hits: Array<{ count: number; value: any }>;
mean_value?: number;
median_value?: number;
max_value?: number;
min_value?: number;
earliest?: string;
latest?: string;
};
};
sample_start: string;
num_messages_analyzed: number;
mappings: {
[fieldName: string]: {
type: string;
properties: {
[fieldName: string]: {
// including all possible Elasticsearch types
// since find_file_structure API can be enhanced to include new fields in the future
type: Exclude<
ES_FIELD_TYPES,
ES_FIELD_TYPES._ID | ES_FIELD_TYPES._INDEX | ES_FIELD_TYPES._SOURCE | ES_FIELD_TYPES._TYPE
>;
format?: string;
};
};
};
quote: string;

View file

@ -42,11 +42,7 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState {
[key: string]: any;
}
export interface DataVisualizerIndexBasedAppState {
pageIndex: number;
pageSize: number;
sortField: string;
sortDirection: string;
export interface DataVisualizerIndexBasedAppState extends Omit<ListingPageUrlState, 'queryText'> {
searchString?: Query['query'];
searchQuery?: Query['query'];
searchQueryLanguage?: SearchQueryLanguage;
@ -57,6 +53,13 @@ export interface DataVisualizerIndexBasedAppState {
showAllFields?: boolean;
showEmptyFields?: boolean;
}
export interface DataVisualizerFileBasedAppState extends Omit<ListingPageUrlState, 'queryText'> {
visibleFieldTypes?: string[];
visibleFieldNames?: string[];
showDistributions?: boolean;
}
export type MlGenericUrlState = MLPageState<
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB

View file

@ -22,6 +22,7 @@
.mlDataGridChart__legendBoolean {
width: 100%;
min-width: $euiButtonMinWidth;
td { text-align: center }
}

View file

@ -20,7 +20,7 @@ import { NON_AGGREGATABLE } from './common';
export const hoveredRow$ = new BehaviorSubject<any | null>(null);
const BAR_COLOR = euiPaletteColorBlind()[0];
export const BAR_COLOR = euiPaletteColorBlind()[0];
const BAR_COLOR_BLUR = euiPaletteColorBlind({ rotations: 2 })[10];
const MAX_CHART_COLUMNS = 20;

View file

@ -11,11 +11,15 @@ import { EuiText, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FieldTypeIcon } from '../field_type_icon';
import { FieldVisConfig } from '../../datavisualizer/index_based/common';
import { getMLJobTypeAriaLabel } from '../../util/field_types_utils';
import {
FieldVisConfig,
FileBasedFieldVisConfig,
isIndexBasedFieldVisConfig,
} from '../../datavisualizer/stats_table/types/field_vis_config';
interface Props {
card: FieldVisConfig;
card: FieldVisConfig | FileBasedFieldVisConfig;
}
export const FieldTitleBar: FC<Props> = ({ card }) => {
@ -30,13 +34,13 @@ export const FieldTitleBar: FC<Props> = ({ card }) => {
if (card.fieldName === undefined) {
classNames.push('document_count');
} else if (card.isUnsupportedType === true) {
} else if (isIndexBasedFieldVisConfig(card) && card.isUnsupportedType === true) {
classNames.push('type-other');
} else {
classNames.push(card.type);
}
if (card.isUnsupportedType !== true) {
if (isIndexBasedFieldVisConfig(card) && card.isUnsupportedType !== true) {
// All the supported field types have aria labels.
cardTitleAriaLabel.unshift(getMLJobTypeAriaLabel(card.type)!);
}

View file

@ -7,7 +7,10 @@
import { Direction, EuiBasicTableProps, Pagination, PropertySort } from '@elastic/eui';
import { useCallback, useMemo } from 'react';
import { ListingPageUrlState } from '../../../../../../../common/types/common';
import { DataVisualizerIndexBasedAppState } from '../../../../../../../common/types/ml_url_generator';
import {
DataVisualizerFileBasedAppState,
DataVisualizerIndexBasedAppState,
} from '../../../../../../../common/types/ml_url_generator';
const PAGE_SIZE_OPTIONS = [10, 25, 50];
@ -38,7 +41,10 @@ interface UseTableSettingsReturnValue<T> {
export function useTableSettings<TypeOfItem>(
items: TypeOfItem[],
pageState: ListingPageUrlState | DataVisualizerIndexBasedAppState,
pageState:
| ListingPageUrlState
| DataVisualizerIndexBasedAppState
| DataVisualizerFileBasedAppState,
updatePageState: (update: Partial<ListingPageUrlState>) => void
): UseTableSettingsReturnValue<TypeOfItem> {
const { pageIndex, pageSize, sortField, sortDirection } = pageState;

View file

@ -1,3 +1,3 @@
@import 'file_based/index';
@import 'index_based/index';
@import 'stats_datagrid/index';
@import 'stats_table/index';

View file

@ -1,7 +1,6 @@
@import 'file_datavisualizer_view/index';
@import 'results_view/index';
@import 'analysis_summary/index';
@import 'fields_stats/index';
@import 'about_panel/index';
@import 'import_summary/index';
@import 'experimental_badge/index';

View file

@ -4,45 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiPanel } from '@elastic/eui';
import React, { FC } from 'react';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import { FieldVisConfig } from '../../common';
import { FieldTitleBar } from '../../../../components/field_title_bar/index';
import React from 'react';
import {
BooleanContent,
DateContent,
GeoPointContent,
IpContent,
KeywordContent,
NotInDocsContent,
NumberContent,
OtherContent,
TextContent,
} from './content_types';
import { LoadingIndicator } from './loading_indicator';
NumberContent,
} from '../../../stats_table/components/field_data_expanded_row';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import type { FileBasedFieldVisConfig } from '../../../stats_table/types/field_vis_config';
export interface FieldDataCardProps {
config: FieldVisConfig;
}
export const FieldDataCard: FC<FieldDataCardProps> = ({ config }) => {
const { fieldName, loading, type, existsInDocs } = config;
export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFieldVisConfig }) => {
const config = item;
const { type, fieldName } = config;
function getCardContent() {
if (existsInDocs === false) {
return <NotInDocsContent />;
}
switch (type) {
case ML_JOB_FIELD_TYPES.NUMBER:
if (fieldName !== undefined) {
return <NumberContent config={config} />;
} else {
return null;
}
return <NumberContent config={config} />;
case ML_JOB_FIELD_TYPES.BOOLEAN:
return <BooleanContent config={config} />;
@ -68,15 +51,11 @@ export const FieldDataCard: FC<FieldDataCardProps> = ({ config }) => {
}
return (
<EuiPanel
data-test-subj={`mlFieldDataCard ${fieldName} ${type}`}
className="mlFieldDataCard"
hasShadow={false}
<div
className="mlDataVisualizerFieldExpandedRow"
data-test-subj={`mlDataVisualizerFieldExpandedRow-${fieldName}`}
>
<FieldTitleBar card={config} />
<div className="mlFieldDataCard__content" data-test-subj="mlFieldDataCardContent">
{loading === true ? <LoadingIndicator /> : getCardContent()}
</div>
</EuiPanel>
{getCardContent()}
</div>
);
};

View file

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

View file

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

View file

@ -0,0 +1,55 @@
/*
* 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 from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FileBasedFieldVisConfig } from '../../../stats_table/types';
export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFieldVisConfig }) => {
const stats = config.stats;
if (
stats === undefined ||
stats.min === undefined ||
stats.median === undefined ||
stats.max === undefined
)
return null;
return (
<EuiFlexGroup direction={'column'} gutterSize={'xs'}>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<b>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.minTitle"
defaultMessage="min"
/>
</b>
</EuiFlexItem>
<EuiFlexItem>
<b>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.medianTitle"
defaultMessage="median"
/>
</b>
</EuiFlexItem>
<EuiFlexItem>
<b>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.maxTitle"
defaultMessage="max"
/>
</b>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>{stats.min}</EuiFlexItem>
<EuiFlexItem>{stats.median}</EuiFlexItem>
<EuiFlexItem>{stats.max}</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,46 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { MultiSelectPicker } from '../../../../components/multi_select_picker';
import type {
FileBasedFieldVisConfig,
FileBasedUnknownFieldVisConfig,
} from '../../../stats_table/types/field_vis_config';
interface Props {
fields: Array<FileBasedFieldVisConfig | FileBasedUnknownFieldVisConfig>;
setVisibleFieldNames(q: string[]): void;
visibleFieldNames: string[];
}
export const DataVisualizerFieldNamesFilter: FC<Props> = ({
fields,
setVisibleFieldNames,
visibleFieldNames,
}) => {
const fieldNameTitle = useMemo(
() =>
i18n.translate('xpack.ml.dataVisualizer.fileBased.fieldNameSelect', {
defaultMessage: 'Field name',
}),
[]
);
const options = useMemo(
() => fields.filter((d) => d.fieldName !== undefined).map((d) => ({ value: d.fieldName! })),
[fields]
);
return (
<MultiSelectPicker
title={fieldNameTitle}
options={options}
onChange={setVisibleFieldNames}
checkedOptions={visibleFieldNames}
dataTestSubj={'mlDataVisualizerFieldNameSelect'}
/>
);
};

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { FieldDataCard, FieldDataCardProps } from './field_data_card';
export { DataVisualizerFieldNamesFilter } from './field_names_filter';

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { MultiSelectPicker, Option } from '../../../../components/multi_select_picker';
import type {
FileBasedFieldVisConfig,
FileBasedUnknownFieldVisConfig,
} from '../../../stats_table/types/field_vis_config';
import { FieldTypeIcon } from '../../../../components/field_type_icon';
import { ML_JOB_FIELD_TYPES_OPTIONS } from '../../../index_based/components/search_panel/field_type_filter';
interface Props {
fields: Array<FileBasedFieldVisConfig | FileBasedUnknownFieldVisConfig>;
setVisibleFieldTypes(q: string[]): void;
visibleFieldTypes: string[];
}
export const DataVisualizerFieldTypesFilter: FC<Props> = ({
fields,
setVisibleFieldTypes,
visibleFieldTypes,
}) => {
const fieldNameTitle = useMemo(
() =>
i18n.translate('xpack.ml.dataVisualizer.fileBased.fieldTypeSelect', {
defaultMessage: 'Field type',
}),
[]
);
const options = useMemo(() => {
const fieldTypesTracker = new Set();
const fieldTypes: Option[] = [];
fields.forEach(({ type }) => {
if (
type !== undefined &&
!fieldTypesTracker.has(type) &&
ML_JOB_FIELD_TYPES_OPTIONS[type] !== undefined
) {
const item = ML_JOB_FIELD_TYPES_OPTIONS[type];
fieldTypesTracker.add(type);
fieldTypes.push({
value: type,
name: (
<EuiFlexGroup>
<EuiFlexItem grow={true}> {item.name}</EuiFlexItem>
{type && (
<EuiFlexItem grow={false}>
<FieldTypeIcon
type={type}
fieldName={item.name}
tooltipEnabled={false}
needsAria={true}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
),
});
}
});
return fieldTypes;
}, [fields]);
return (
<MultiSelectPicker
title={fieldNameTitle}
options={options}
onChange={setVisibleFieldTypes}
checkedOptions={visibleFieldTypes}
dataTestSubj={'mlDataVisualizerFieldTypeSelect'}
/>
);
};

View file

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

View file

@ -1,2 +0,0 @@
@import 'fields_stats';
@import 'field_stats_card';

View file

@ -1,184 +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 from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiProgress,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldTypeIcon } from '../../../../components/field_type_icon';
import { DisplayValue } from '../../../../components/display_value';
import { getMLJobTypeAriaLabel } from '../../../../util/field_types_utils';
export function FieldStatsCard({ field }) {
let type = field.type;
if (type === 'double' || type === 'long') {
type = 'number';
}
const typeAriaLabel = getMLJobTypeAriaLabel(type);
const cardTitleAriaLabel = [field.name];
if (typeAriaLabel) {
cardTitleAriaLabel.unshift(typeAriaLabel);
}
return (
<EuiPanel hasShadow={false} className="mlFieldDataCard">
<div className="ml-field-data-card" data-test-subj="mlPageFileDataVisFieldDataCard">
<div className={`ml-field-title-bar ${type}`}>
<FieldTypeIcon type={type} needsAria={false} fieldName={field.name} />
<div className="field-name" tabIndex="0" aria-label={`${cardTitleAriaLabel.join(', ')}`}>
{field.name}
</div>
</div>
<div className="mlFieldDataCard__content">
{field.count > 0 && (
<React.Fragment>
<div className="stats">
<EuiFlexGroup
gutterSize="xs"
alignItems="center"
justifyContent="center"
className="stat"
>
<EuiFlexItem grow={false}>
<EuiIcon type="document" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.documentsCountDescription"
defaultMessage="{fieldCount, plural, zero {# document} one {# document} other {# documents}} ({fieldPercent}%)"
values={{
fieldCount: field.count,
fieldPercent: field.percent,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
gutterSize="xs"
alignItems="center"
justifyContent="center"
className="stat"
>
<EuiFlexItem grow={false}>
<EuiIcon type="database" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.distinctCountDescription"
defaultMessage="{fieldCardinality} distinct {fieldCardinality, plural, zero {value} one {value} other {values}}"
values={{
fieldCardinality: field.cardinality,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
{field.median_value && (
<React.Fragment>
<div>
<div className="stat min heading">
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.minTitle"
defaultMessage="min"
/>
</div>
<div className="stat median heading">
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.medianTitle"
defaultMessage="median"
/>
</div>
<div className="stat max heading">
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.maxTitle"
defaultMessage="max"
/>
</div>
</div>
<div>
<div className="stat min value">
<DisplayValue value={field.min_value} />
</div>
<div className="stat median value">
<DisplayValue value={field.median_value} />
</div>
<div className="stat max value">
<DisplayValue value={field.max_value} />
</div>
</div>
</React.Fragment>
)}
</div>
{field.top_hits && (
<React.Fragment>
<EuiSpacer size="s" />
<div className="stats">
<div className="stat">
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.topStatsValuesDescription"
defaultMessage="top values"
/>
</div>
{field.top_hits.map(({ count, value }) => {
const pcnt = Math.round((count / field.count) * 100 * 100) / 100;
return (
<EuiFlexGroup gutterSize="xs" alignItems="center" key={value}>
<EuiFlexItem
grow={false}
style={{ width: 100 }}
className="eui-textTruncate"
>
<EuiText size="xs" textAlign="right" color="subdued">
{value}&nbsp;
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiProgress value={count} max={field.count} color="primary" size="m" />
</EuiFlexItem>
<EuiFlexItem
grow={false}
style={{ width: 70 }}
className="eui-textTruncate"
>
<EuiText size="xs" textAlign="left" color="subdued">
{pcnt}%
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
})}
</div>
</React.Fragment>
)}
</React.Fragment>
)}
{field.count === 0 && (
<div className="stats">
<div className="stat">
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fieldStatsCard.noFieldInformationAvailableDescription"
defaultMessage="No field information available"
/>
</div>
</div>
)}
</div>
</div>
</EuiPanel>
);
}

View file

@ -1,113 +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, { Component } from 'react';
import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import { FieldStatsCard } from './field_stats_card';
import { getFieldNames } from './get_field_names';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place';
export class FieldsStats extends Component {
constructor(props) {
super(props);
this.state = {
fields: [],
};
}
componentDidMount() {
this.setState({
fields: createFields(this.props.results),
});
}
render() {
return (
<div className="fields-stats">
<EuiFlexGrid gutterSize="m">
{this.state.fields.map((f) => (
<EuiFlexItem key={f.name} style={{ width: '360px' }}>
<FieldStatsCard field={f} />
</EuiFlexItem>
))}
</EuiFlexGrid>
</div>
);
}
}
function createFields(results) {
const {
mappings,
field_stats: fieldStats,
num_messages_analyzed: numMessagesAnalyzed,
timestamp_field: timestampField,
} = results;
if (mappings && mappings.properties && fieldStats) {
const fieldNames = getFieldNames(results);
return fieldNames.map((name) => {
if (fieldStats[name] !== undefined) {
const field = { name };
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 === undefined) {
field.type = ML_JOB_FIELD_TYPES.DATE;
}
if (f !== undefined) {
Object.assign(field, f);
}
if (m !== undefined) {
field.type = m.type;
if (m.format !== undefined) {
field.format = m.format;
}
}
const percent = (field.count / numMessagesAnalyzed) * 100;
field.percent = roundToDecimalPlace(percent);
// round min, max, median, mean to 2dp.
if (field.median_value !== undefined) {
field.median_value = roundToDecimalPlace(field.median_value);
field.mean_value = roundToDecimalPlace(field.mean_value);
field.min_value = roundToDecimalPlace(field.min_value);
field.max_value = roundToDecimalPlace(field.max_value);
}
return field;
} else {
// field is not in the field stats
// 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 === ML_JOB_FIELD_TYPES.TEXT
? ML_JOB_FIELD_TYPES.TEXT
: ML_JOB_FIELD_TYPES.UNKNOWN;
return {
name,
type,
mean_value: 0,
count: 0,
cardinality: 0,
percent: 0,
};
}
});
}
return [];
}

View file

@ -0,0 +1,126 @@
/*
* 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 { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
import { getFieldNames, getSupportedFieldType } from './get_field_names';
import { FileBasedFieldVisConfig } from '../../../stats_table/types';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place';
export function createFields(results: FindFileStructureResponse) {
const {
mappings,
field_stats: fieldStats,
num_messages_analyzed: numMessagesAnalyzed,
timestamp_field: timestampField,
} = results;
let numericFieldsCount = 0;
if (mappings && mappings.properties && fieldStats) {
const fieldNames = getFieldNames(results);
const items = fieldNames.map((name) => {
if (fieldStats[name] !== undefined) {
const field: FileBasedFieldVisConfig = {
fieldName: name,
type: ML_JOB_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 === ML_JOB_FIELD_TYPES.UNKNOWN) {
field.type = ML_JOB_FIELD_TYPES.DATE;
}
if (m !== undefined) {
field.type = getSupportedFieldType(m.type);
if (field.type === ML_JOB_FIELD_TYPES.NUMBER) {
numericFieldsCount += 1;
}
if (m.format !== undefined) {
field.format = m.format;
}
}
let _stats = {};
// round min, max, median, mean to 2dp.
if (f.median_value !== undefined) {
_stats = {
..._stats,
median: roundToDecimalPlace(f.median_value),
mean: roundToDecimalPlace(f.mean_value),
min: roundToDecimalPlace(f.min_value),
max: roundToDecimalPlace(f.max_value),
};
}
if (f.cardinality !== undefined) {
_stats = {
..._stats,
cardinality: f.cardinality,
count: f.count,
sampleCount: numMessagesAnalyzed,
};
}
if (f.top_hits !== undefined) {
if (field.type === ML_JOB_FIELD_TYPES.TEXT) {
_stats = {
..._stats,
examples: f.top_hits.map((hit) => hit.value),
};
} else {
_stats = {
..._stats,
topValues: f.top_hits.map((hit) => ({ key: hit.value, doc_count: hit.count })),
};
}
}
if (field.type === ML_JOB_FIELD_TYPES.DATE) {
_stats = {
..._stats,
earliest: f.earliest,
latest: f.latest,
};
}
field.stats = _stats;
return field;
} else {
// field is not in the field stats
// 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 === ML_JOB_FIELD_TYPES.TEXT
? ML_JOB_FIELD_TYPES.TEXT
: ML_JOB_FIELD_TYPES.UNKNOWN;
return {
fieldName: name,
type,
stats: {
mean: 0,
count: 0,
sampleCount: numMessagesAnalyzed,
cardinality: 0,
},
};
}
});
return {
fields: items,
totalFieldsCount: items.length,
totalMetricFieldsCount: numericFieldsCount,
};
}
return { fields: [], totalFieldsCount: 0, totalMetricFieldsCount: 0 };
}

View file

@ -0,0 +1,124 @@
/*
* 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, { useMemo, FC } from 'react';
import { EuiFlexGroup, EuiSpacer } from '@elastic/eui';
import type { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
import { DataVisualizerTable, ItemIdToExpandedRowMap } from '../../../stats_table';
import type { FileBasedFieldVisConfig } from '../../../stats_table/types/field_vis_config';
import { FileBasedDataVisualizerExpandedRow } from '../expanded_row';
import { DataVisualizerFieldNamesFilter } from '../field_names_filter';
import { DataVisualizerFieldTypesFilter } from '../field_types_filter';
import { createFields } from './create_fields';
import { filterFields } from './filter_fields';
import { usePageUrlState } from '../../../../util/url_state';
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
import {
MetricFieldsCount,
TotalFieldsCount,
} from '../../../stats_table/components/field_count_stats';
import type { DataVisualizerFileBasedAppState } from '../../../../../../common/types/ml_url_generator';
interface Props {
results: FindFileStructureResponse;
}
export const getDefaultDataVisualizerListState = (): Required<DataVisualizerFileBasedAppState> => ({
pageIndex: 0,
pageSize: 10,
sortField: 'fieldName',
sortDirection: 'asc',
visibleFieldTypes: [],
visibleFieldNames: [],
showDistributions: true,
});
function getItemIdToExpandedRowMap(
itemIds: string[],
items: FileBasedFieldVisConfig[]
): ItemIdToExpandedRowMap {
return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => {
const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName);
if (item !== undefined) {
m[fieldName] = <FileBasedDataVisualizerExpandedRow item={item} />;
}
return m;
}, {} as ItemIdToExpandedRowMap);
}
export const FieldsStatsGrid: FC<Props> = ({ results }) => {
const restorableDefaults = getDefaultDataVisualizerListState();
const [
dataVisualizerListState,
setDataVisualizerListState,
] = usePageUrlState<DataVisualizerFileBasedAppState>(
ML_PAGES.DATA_VISUALIZER_FILE,
restorableDefaults
);
const visibleFieldTypes =
dataVisualizerListState.visibleFieldTypes ?? restorableDefaults.visibleFieldTypes;
const setVisibleFieldTypes = (values: string[]) => {
setDataVisualizerListState({ ...dataVisualizerListState, visibleFieldTypes: values });
};
const visibleFieldNames =
dataVisualizerListState.visibleFieldNames ?? restorableDefaults.visibleFieldNames;
const setVisibleFieldNames = (values: string[]) => {
setDataVisualizerListState({ ...dataVisualizerListState, visibleFieldNames: values });
};
const { fields, totalFieldsCount, totalMetricFieldsCount } = useMemo(
() => createFields(results),
[results, visibleFieldNames, visibleFieldTypes]
);
const { filteredFields, visibleFieldsCount, visibleMetricsCount } = useMemo(
() => filterFields(fields, visibleFieldNames, visibleFieldTypes),
[results, visibleFieldNames, visibleFieldTypes]
);
const fieldsCountStats = { visibleFieldsCount, totalFieldsCount };
const metricsStats = { visibleMetricsCount, totalMetricFieldsCount };
return (
<div>
<EuiSpacer size="m" />
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
style={{ marginLeft: 4 }}
data-test-subj="mlDataVisualizerFieldCountPanel"
>
<TotalFieldsCount fieldsCountStats={fieldsCountStats} />
<MetricFieldsCount metricsStats={metricsStats} />
<EuiFlexGroup
gutterSize="xs"
data-test-subj="mlDataVisualizerFieldCountPanel"
justifyContent={'flexEnd'}
>
<DataVisualizerFieldNamesFilter
fields={fields}
setVisibleFieldNames={setVisibleFieldNames}
visibleFieldNames={visibleFieldNames}
/>
<DataVisualizerFieldTypesFilter
fields={fields}
setVisibleFieldTypes={setVisibleFieldTypes}
visibleFieldTypes={visibleFieldTypes}
/>
</EuiFlexGroup>
</EuiFlexGroup>
<EuiSpacer size="m" />
<DataVisualizerTable<FileBasedFieldVisConfig>
items={filteredFields}
pageState={dataVisualizerListState}
updatePageState={setDataVisualizerListState}
getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
/>
</div>
);
};

View file

@ -0,0 +1,36 @@
/*
* 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 { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import type {
FileBasedFieldVisConfig,
FileBasedUnknownFieldVisConfig,
} from '../../../stats_table/types/field_vis_config';
export function filterFields(
fields: Array<FileBasedFieldVisConfig | FileBasedUnknownFieldVisConfig>,
visibleFieldNames: string[],
visibleFieldTypes: string[]
) {
let items = fields;
if (visibleFieldTypes && visibleFieldTypes.length > 0) {
items = items.filter(
(config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1
);
}
if (visibleFieldNames && visibleFieldNames.length > 0) {
items = items.filter((config) => {
return visibleFieldNames.findIndex((field) => field === config.fieldName) > -1;
});
}
return {
filteredFields: items,
visibleFieldsCount: items.length,
visibleMetricsCount: items.filter((d) => d.type === ML_JOB_FIELD_TYPES.NUMBER).length,
};
}

View file

@ -5,8 +5,11 @@
*/
import { difference } from 'lodash';
export function getFieldNames(results) {
import type { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
import { MlJobFieldType } from '../../../../../../common/types/field_types';
import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types';
import { ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common';
export function getFieldNames(results: FindFileStructureResponse) {
const { mappings, field_stats: fieldStats, column_names: columnNames } = results;
// if columnNames exists (i.e delimited) use it for the field list
@ -29,3 +32,24 @@ export function getFieldNames(results) {
}
return tempFields;
}
export function getSupportedFieldType(type: string): MlJobFieldType {
switch (type) {
case ES_FIELD_TYPES.FLOAT:
case ES_FIELD_TYPES.HALF_FLOAT:
case ES_FIELD_TYPES.SCALED_FLOAT:
case ES_FIELD_TYPES.DOUBLE:
case ES_FIELD_TYPES.INTEGER:
case ES_FIELD_TYPES.LONG:
case ES_FIELD_TYPES.SHORT:
case ES_FIELD_TYPES.UNSIGNED_LONG:
return ML_JOB_FIELD_TYPES.NUMBER;
case ES_FIELD_TYPES.DATE:
case ES_FIELD_TYPES.DATE_NANOS:
return ML_JOB_FIELD_TYPES.DATE;
default:
return type as MlJobFieldType;
}
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { FieldsStats } from './fields_stats';
export { FieldsStatsGrid } from './fields_stats_grid';

View file

@ -228,7 +228,6 @@ export class FileDataVisualizerView extends Component {
};
setOverrides = (overrides) => {
console.log('setOverrides', overrides);
this.setState(
{
loading: true,

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC } from 'react';
@ -15,7 +14,6 @@ import {
EuiPageBody,
EuiPageContentHeader,
EuiPanel,
EuiTabbedContent,
EuiSpacer,
EuiTitle,
EuiFlexGroup,
@ -25,8 +23,7 @@ import { FindFileStructureResponse } from '../../../../../../common/types/file_d
import { FileContents } from '../file_contents';
import { AnalysisSummary } from '../analysis_summary';
// @ts-ignore
import { FieldsStats } from '../fields_stats';
import { FieldsStatsGrid } from '../fields_stats_grid';
interface Props {
data: string;
@ -45,16 +42,6 @@ export const ResultsView: FC<Props> = ({
showExplanationFlyout,
disableButtons,
}) => {
const tabs = [
{
id: 'file-stats',
name: i18n.translate('xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName', {
defaultMessage: 'File stats',
}),
content: <FieldsStats results={results} />,
},
];
return (
<EuiPage data-test-subj="mlPageFileDataVisResults">
<EuiPageBody>
@ -103,7 +90,16 @@ export const ResultsView: FC<Props> = ({
<EuiSpacer size="m" />
<EuiPanel data-test-subj="mlFileDataVisFileStatsPanel">
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} onTabClick={() => {}} />
<EuiTitle size="s">
<h2 data-test-subj="mlFileDataVisStatsTitle">
<FormattedMessage
id="xpack.ml.fileDatavisualizer.resultsView.fileStatsName"
defaultMessage="File stats"
/>
</h2>
</EuiTitle>
<FieldsStatsGrid results={results} />
</EuiPanel>
</div>
</EuiPageBody>

View file

@ -1 +1 @@
@import 'components/field_data_card/index';
@import 'components/field_data_row/index';

View file

@ -4,5 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { FieldVisConfig } from './field_vis_config';
export { FieldHistogramRequestConfig, FieldRequestConfig } from './request';

View file

@ -6,22 +6,23 @@
import React from 'react';
import { FieldVisConfig } from '../index_based/common';
import { FieldVisConfig } from '../../../stats_table/types';
import {
BooleanContent,
DateContent,
GeoPointContent,
IpContent,
KeywordContent,
NotInDocsContent,
NumberContent,
OtherContent,
TextContent,
} from '../index_based/components/field_data_card/content_types';
import { NumberContent } from './components/field_data_expanded_row/number_content';
import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types';
import { LoadingIndicator } from '../index_based/components/field_data_card/loading_indicator';
} from '../../../stats_table/components/field_data_expanded_row';
export const DataVisualizerFieldExpandedRow = ({ item }: { item: FieldVisConfig }) => {
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 }) => {
const config = item;
const { loading, type, existsInDocs, fieldName } = config;

View file

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

View file

@ -4,19 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiNotificationBadge, EuiSwitch, EuiText } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC } from 'react';
import {
MetricFieldsCount,
TotalFieldsCount,
} from '../../../stats_table/components/field_count_stats';
import type {
TotalFieldsCountProps,
MetricFieldsCountProps,
} from '../../../stats_table/components/field_count_stats';
interface Props {
metricsStats?: {
visibleMetricFields: number;
totalMetricFields: number;
};
fieldsCountStats?: {
visibleFieldsCount: number;
totalFieldsCount: number;
};
interface Props extends TotalFieldsCountProps, MetricFieldsCountProps {
showEmptyFields: boolean;
toggleShowEmptyFields: () => void;
}
@ -33,83 +33,8 @@ export const FieldCountPanel: FC<Props> = ({
style={{ marginLeft: 4 }}
data-test-subj="mlDataVisualizerFieldCountPanel"
>
{fieldsCountStats && (
<EuiFlexGroup
gutterSize="s"
alignItems="center"
style={{ maxWidth: 250 }}
data-test-subj="mlDataVisualizerFieldsSummary"
>
<EuiFlexItem grow={false}>
<EuiText>
<h5>
<FormattedMessage
id="xpack.ml.dataVisualizer.searchPanel.allFieldsLabel"
defaultMessage="All fields"
/>
</h5>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge
color="subdued"
size="m"
data-test-subj="mlDataVisualizerVisibleFieldsCount"
>
<strong>{fieldsCountStats.visibleFieldsCount}</strong>
</EuiNotificationBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s" data-test-subj="mlDataVisualizerTotalFieldsCount">
<FormattedMessage
id="xpack.ml.dataVisualizer.searchPanel.ofFieldsTotal"
defaultMessage="of {totalCount} total"
values={{ totalCount: fieldsCountStats.totalFieldsCount }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
{metricsStats && (
<EuiFlexGroup
gutterSize="s"
alignItems="center"
style={{ maxWidth: 250 }}
data-test-subj="mlDataVisualizerMetricFieldsSummary"
>
<EuiFlexItem grow={false}>
<EuiText>
<h5>
<FormattedMessage
id="xpack.ml.dataVisualizer.searchPanel.numberFieldsLabel"
defaultMessage="Number fields"
/>
</h5>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge
color="subdued"
size="m"
data-test-subj="mlDataVisualizerVisibleMetricFieldsCount"
>
<strong>{metricsStats.visibleMetricFields}</strong>
</EuiNotificationBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s" data-test-subj="mlDataVisualizerMetricFieldsCount">
<FormattedMessage
id="xpack.ml.dataVisualizer.searchPanel.ofFieldsTotal"
defaultMessage="of {totalCount} total"
values={{ totalCount: metricsStats.totalMetricFields }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
<TotalFieldsCount fieldsCountStats={fieldsCountStats} />
<MetricFieldsCount metricsStats={metricsStats} />
<EuiFlexItem>
<EuiSwitch
data-test-subj="mlDataVisualizerShowEmptyFieldsSwitch"

View file

@ -1,84 +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 { EuiSpacer } from '@elastic/eui';
import { Axis, BarSeries, Chart, Settings } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldDataCardProps } from '../field_data_card';
import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place';
import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header';
function getPercentLabel(value: number): string {
if (value === 0) {
return '0%';
}
if (value >= 0.1) {
return `${value}%`;
} else {
return '< 0.1%';
}
}
export const BooleanContent: FC<FieldDataCardProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
const { count, trueCount, falseCount } = stats;
if (count === undefined || trueCount === undefined || falseCount === undefined) return null;
return (
<div className="mlFieldDataCard__stats">
<ExpandedRowFieldHeader>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardBoolean.valuesLabel"
defaultMessage="Values"
/>
</ExpandedRowFieldHeader>
<EuiSpacer size="xs" />
<Chart renderer="canvas" className="story-chart" size={{ height: 200 }}>
<Axis id="bottom" position="bottom" showOverlappingTicks />
<Settings
showLegend={false}
theme={{
barSeriesStyle: {
displayValue: {
fill: '#000',
fontSize: 12,
fontStyle: 'normal',
offsetX: 0,
offsetY: -5,
padding: 0,
},
},
}}
/>
<BarSeries
id={config.fieldName || config.fieldFormat}
data={[
{ x: 'true', y: roundToDecimalPlace((trueCount / count) * 100) },
{ x: 'false', y: roundToDecimalPlace((falseCount / count) * 100) },
]}
displayValueSettings={{
hideClippedValue: true,
isAlternatingValueLabel: true,
valueFormatter: getPercentLabel,
isValueContainedInElement: false,
showValueLabel: true,
}}
color={['rgba(230, 194, 32, 0.5)', 'rgba(224, 187, 20, 0.71)']}
splitSeriesAccessors={['x']}
stackAccessors={['x']}
xAccessor="x"
xScaleType="ordinal"
yAccessors={['y']}
yScaleType="linear"
/>
</Chart>
</div>
);
};

View file

@ -1,41 +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 { FieldDataCardProps } from '../field_data_card';
import { ExamplesList } from '../examples_list';
export const GeoPointContent: FC<FieldDataCardProps> = ({ config }) => {
// TODO - adjust server-side query to get examples using:
// GET /filebeat-apache-2019.01.30/_search
// {
// "size":10,
// "_source": false,
// "docvalue_fields": ["source.geo.location"],
// "query": {
// "bool":{
// "must":[
// {
// "exists":{
// "field":"source.geo.location"
// }
// }
// ]
// }
// }
// }
const { stats } = config;
if (stats?.examples === undefined) return null;
return (
<div className="mlFieldDataCard__stats">
<ExamplesList examples={stats.examples} />
</div>
);
};

View file

@ -1,34 +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 { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldDataCardProps } from '../field_data_card';
import { TopValues } from '../top_values';
import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header';
export const IpContent: FC<FieldDataCardProps> = ({ config }) => {
const { stats, fieldFormat } = config;
if (stats === undefined) return null;
const { count, sampleCount, cardinality } = stats;
if (count === undefined || sampleCount === undefined || cardinality === undefined) return null;
return (
<div className="mlFieldDataCard__stats">
<ExpandedRowFieldHeader>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardIp.topValuesLabel"
defaultMessage="Top values"
/>
</ExpandedRowFieldHeader>
<EuiSpacer size="xs" />
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
</div>
);
};

View file

@ -1,29 +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 { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldDataCardProps } from '../field_data_card';
import { TopValues } from '../top_values';
import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header';
export const KeywordContent: FC<FieldDataCardProps> = ({ config }) => {
const { stats, fieldFormat } = config;
return (
<div className="mlFieldDataCard__stats">
<ExpandedRowFieldHeader>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardKeyword.topValuesLabel"
defaultMessage="Top values"
/>
</ExpandedRowFieldHeader>
<EuiSpacer size="xs" />
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
</div>
);
};

View file

@ -1,200 +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, Fragment, useEffect, useState } from 'react';
import {
EuiButtonGroup,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FieldDataCardProps } from '../field_data_card';
import { DisplayValue } from '../../../../../components/display_value';
import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format';
import { numberAsOrdinal } from '../../../../../formatters/number_as_ordinal';
import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place';
import {
MetricDistributionChart,
MetricDistributionChartData,
buildChartDataFromStats,
} from '../metric_distribution_chart';
import { TopValues } from '../top_values';
const DETAILS_MODE = {
DISTRIBUTION: 'distribution',
TOP_VALUES: 'top_values',
} as const;
type DetailsModeType = typeof DETAILS_MODE[keyof typeof DETAILS_MODE];
const METRIC_DISTRIBUTION_CHART_WIDTH = 325;
const METRIC_DISTRIBUTION_CHART_HEIGHT = 210;
const DEFAULT_TOP_VALUES_THRESHOLD = 100;
export const NumberContent: FC<FieldDataCardProps> = ({ config }) => {
const { stats, fieldFormat } = config;
useEffect(() => {
const chartData = buildChartDataFromStats(stats, METRIC_DISTRIBUTION_CHART_WIDTH);
setDistributionChartData(chartData);
}, []);
const [detailsMode, setDetailsMode] = useState(
stats?.cardinality ?? 0 <= DEFAULT_TOP_VALUES_THRESHOLD
? DETAILS_MODE.TOP_VALUES
: DETAILS_MODE.DISTRIBUTION
);
const defaultChartData: MetricDistributionChartData[] = [];
const [distributionChartData, setDistributionChartData] = useState(defaultChartData);
if (stats === undefined) return null;
const { count, sampleCount, cardinality, min, median, max, distribution } = stats;
if (count === undefined || sampleCount === undefined) return null;
const docsPercent = roundToDecimalPlace((count / sampleCount) * 100);
const detailsOptions = [
{
id: DETAILS_MODE.TOP_VALUES,
label: i18n.translate('xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel', {
defaultMessage: 'Top values',
}),
},
{
id: DETAILS_MODE.DISTRIBUTION,
label: i18n.translate('xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel', {
defaultMessage: 'Distribution',
}),
},
];
return (
<div className="mlFieldDataCard__stats">
<div>
<EuiText size="xs" color="subdued" data-test-subj="mlFieldDataCardDocCount">
<EuiIcon type="document" />
&nbsp;
<FormattedMessage
id="xpack.ml.fieldDataCard.cardNumber.documentsCountDescription"
defaultMessage="{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)"
values={{
count,
docsPercent,
}}
/>
</EuiText>
</div>
<EuiSpacer size="xs" />
<div>
<EuiText size="xs" color="subdued" data-test-subj="mlFieldDataCardCardinality">
<EuiIcon type="database" />
&nbsp;
<FormattedMessage
id="xpack.ml.fieldDataCard.cardNumber.distinctCountDescription"
defaultMessage="{cardinality} distinct {cardinality, plural, zero {value} one {value} other {values}}"
values={{
cardinality,
}}
/>
</EuiText>
</div>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="xs" justifyContent="center">
<EuiFlexItem grow={1}>
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.ml.fieldDataCard.cardNumber.minLabel"
defaultMessage="min"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.ml.fieldDataCard.cardNumber.medianLabel"
defaultMessage="median"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.ml.fieldDataCard.cardNumber.maxLabel"
defaultMessage="max"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup gutterSize="xs" justifyContent="center">
<EuiFlexItem grow={1} className="eui-textTruncate" data-test-subj="mlFieldDataCardMin">
<DisplayValue value={kibanaFieldFormat(min, fieldFormat)} />
</EuiFlexItem>
<EuiFlexItem grow={1} className="eui-textTruncate" data-test-subj="mlFieldDataCardMedian">
<DisplayValue value={kibanaFieldFormat(median, fieldFormat)} />
</EuiFlexItem>
<EuiFlexItem grow={1} className="eui-textTruncate" data-test-subj="mlFieldDataCardMax">
<DisplayValue value={kibanaFieldFormat(max, fieldFormat)} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiButtonGroup
options={detailsOptions}
idSelected={detailsMode}
onChange={(optionId) => setDetailsMode(optionId as DetailsModeType)}
legend={i18n.translate(
'xpack.ml.fieldDataCard.cardNumber.selectMetricDetailsDisplayAriaLabel',
{
defaultMessage: 'Select display option for metric details',
}
)}
data-test-subj="mlFieldDataCardDetailsSelect"
isFullWidth={true}
buttonSize="compressed"
/>
<EuiSpacer size="m" />
{distribution && detailsMode === DETAILS_MODE.DISTRIBUTION && (
<Fragment>
<EuiFlexGroup justifyContent="spaceAround" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiText size="xs">
<FormattedMessage
id="xpack.ml.fieldDataCard.cardNumber.displayingPercentilesLabel"
defaultMessage="Displaying {minPercent} - {maxPercent} percentiles"
values={{
minPercent: numberAsOrdinal(distribution.minPercentile),
maxPercent: numberAsOrdinal(distribution.maxPercentile),
}}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup justifyContent="center" gutterSize="xs">
<EuiFlexItem grow={true}>
<MetricDistributionChart
width={METRIC_DISTRIBUTION_CHART_WIDTH}
height={METRIC_DISTRIBUTION_CHART_HEIGHT}
chartData={distributionChartData}
fieldFormat={fieldFormat}
/>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
)}
{detailsMode === DETAILS_MODE.TOP_VALUES && (
<EuiFlexGroup>
<EuiFlexItem>
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
</EuiFlexItem>
</EuiFlexGroup>
)}
</div>
);
};

View file

@ -1,83 +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, Fragment } from 'react';
import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { FieldDataCardProps } from '../field_data_card';
import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place';
import { ExamplesList } from '../examples_list';
export const OtherContent: FC<FieldDataCardProps> = ({ config }) => {
const { stats, type, aggregatable } = config;
if (stats === undefined) return null;
const { count, sampleCount, cardinality, examples } = stats;
if (
count === undefined ||
sampleCount === undefined ||
cardinality === undefined ||
examples === undefined
)
return null;
const docsPercent = roundToDecimalPlace((count / sampleCount) * 100);
return (
<div className="mlFieldDataCard__stats">
<div>
<EuiText>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardOther.cardTypeLabel"
defaultMessage="{cardType} type"
values={{
cardType: type,
}}
/>
</EuiText>
</div>
{aggregatable === true && (
<Fragment>
<EuiSpacer size="s" />
<div>
<EuiText size="xs" color="subdued">
<EuiIcon type="document" />
&nbsp;
<FormattedMessage
id="xpack.ml.fieldDataCard.cardOther.documentsCountDescription"
defaultMessage="{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)"
values={{
count,
docsPercent,
}}
/>
</EuiText>
</div>
<EuiSpacer size="xs" />
<div>
<EuiText size="xs" color="subdued">
<EuiIcon type="database" />
&nbsp;
<FormattedMessage
id="xpack.ml.fieldDataCard.cardOther.distinctCountDescription"
defaultMessage="{cardinality} distinct {cardinality, plural, zero {value} one {value} other {values}}"
values={{
cardinality,
}}
/>
</EuiText>
</div>
</Fragment>
)}
<EuiSpacer size="m" />
<ExamplesList examples={examples} />
</div>
);
};

View file

@ -1,62 +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, Fragment } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FieldDataCardProps } from '../field_data_card';
import { ExamplesList } from '../examples_list';
export const TextContent: FC<FieldDataCardProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
const { examples } = stats;
if (examples === undefined) return null;
const numExamples = examples.length;
return (
<div className="mlFieldDataCard__stats">
{numExamples > 0 && <ExamplesList examples={examples} />}
{numExamples === 0 && (
<Fragment>
<EuiSpacer size="xl" />
<EuiCallOut
title={i18n.translate('xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle', {
defaultMessage: 'No examples were obtained for this field',
})}
iconType="alert"
>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription"
defaultMessage="This field was not present in the {sourceParam} field of documents queried."
values={{
sourceParam: <span className="mlFieldDataCard__codeContent">_source</span>,
}}
/>
<EuiSpacer size="s" />
<FormattedMessage
id="xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription"
defaultMessage="It may be populated, for example, using a {copyToParam} parameter in the document mapping, or be pruned from the {sourceParam} field after indexing through the use of {includesParam} and {excludesParam} parameters."
values={{
copyToParam: <span className="mlFieldDataCard__codeContent">copy_to</span>,
sourceParam: <span className="mlFieldDataCard__codeContent">_source</span>,
includesParam: <span className="mlFieldDataCard__codeContent">includes</span>,
excludesParam: <span className="mlFieldDataCard__codeContent">excludes</span>,
}}
/>
</EuiCallOut>
</Fragment>
)}
</div>
);
};

View file

@ -6,11 +6,11 @@
import React, { FC } from 'react';
import type { FieldDataCardProps } from '../field_data_card';
import type { FieldDataRowProps } from '../../../../stats_table/types/field_data_row';
import { DocumentCountChart, DocumentCountChartPoint } from '../document_count_chart';
import { TotalCountHeader } from '../../total_count_header';
export interface Props extends FieldDataCardProps {
export interface Props extends FieldDataRowProps {
totalCount: number;
}

View file

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

View file

@ -25,7 +25,6 @@ export interface DocumentCountChartPoint {
interface Props {
width?: number;
height?: number;
chartPoints: DocumentCountChartPoint[];
timeRangeEarliest: number;
timeRangeLatest: number;
@ -35,7 +34,6 @@ const SPEC_ID = 'document_count';
export const DocumentCountChart: FC<Props> = ({
width,
height,
chartPoints,
timeRangeEarliest,
timeRangeLatest,

View file

@ -9,7 +9,7 @@ import React, { FC } from 'react';
import { EuiListGroup, EuiListGroupItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header';
import { ExpandedRowFieldHeader } from '../../../../stats_table/components/expanded_row_field_header';
interface Props {
examples: Array<string | object>;
}

View file

@ -157,8 +157,6 @@ export const SearchPanel: FC<Props> = ({
setVisibleFieldTypes={setVisibleFieldTypes}
visibleFieldTypes={visibleFieldTypes}
/>
<EuiFlexItem grow={false} />
</EuiFlexGroup>
);
};

View file

@ -45,17 +45,23 @@ import { getToastNotifications } from '../../util/dependency_cache';
import { usePageUrlState, useUrlState } from '../../util/url_state';
import { ActionsPanel } from './components/actions_panel';
import { SearchPanel } from './components/search_panel';
import { DocumentCountContent } from './components/field_data_card/content_types/document_count_content';
import { DataVisualizerDataGrid } from '../stats_datagrid';
import { DocumentCountContent } from './components/field_data_row/content_types/document_count_content';
import { DataVisualizerTable, ItemIdToExpandedRowMap } from '../stats_table';
import { FieldCountPanel } from './components/field_count_panel';
import { ML_PAGES } from '../../../../common/constants/ml_url_generator';
import { DataLoader } from './data_loader';
import type { FieldRequestConfig, FieldVisConfig } from './common';
import type { FieldRequestConfig } from './common';
import type { DataVisualizerIndexBasedAppState } from '../../../../common/types/ml_url_generator';
import type { OverallStats } from '../../../../common/types/datavisualizer';
import { MlJobFieldType } from '../../../../common/types/field_types';
import { HelpMenu } from '../../components/help_menu';
import { useMlKibana } from '../../contexts/kibana';
import { IndexBasedDataVisualizerExpandedRow } from './components/expanded_row';
import { FieldVisConfig } from '../stats_table/types';
import type {
MetricFieldsStats,
TotalFieldsStats,
} from '../stats_table/components/field_count_stats';
interface DataVisualizerPageState {
overallStats: OverallStats;
@ -106,6 +112,19 @@ 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();
@ -228,9 +247,7 @@ export const Page: FC = () => {
const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats);
const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs);
const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded);
const [metricsStats, setMetricsStats] = useState<
undefined | { visibleMetricFields: number; totalMetricFields: number }
>();
const [metricsStats, setMetricsStats] = useState<undefined | MetricFieldsStats>();
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
@ -537,8 +554,8 @@ export const Page: FC = () => {
});
setMetricsStats({
totalMetricFields: allMetricFields.length,
visibleMetricFields: metricFieldsToShow.length,
totalMetricFieldsCount: allMetricFields.length,
visibleMetricsCount: metricFieldsToShow.length,
});
setMetricConfigs(configs);
}
@ -642,7 +659,7 @@ export const Page: FC = () => {
return combinedConfigs;
}, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]);
const fieldsCountStats = useMemo(() => {
const fieldsCountStats: TotalFieldsStats | undefined = useMemo(() => {
let _visibleFieldsCount = 0;
let _totalFieldsCount = 0;
Object.keys(overallStats).forEach((key) => {
@ -736,10 +753,11 @@ export const Page: FC = () => {
metricsStats={metricsStats}
/>
<EuiSpacer size={'m'} />
<DataVisualizerDataGrid
<DataVisualizerTable<FieldVisConfig>
items={configs}
pageState={dataVisualizerListState}
updatePageState={setDataVisualizerListState}
getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
/>
</EuiPanel>
</EuiFlexItem>

View file

@ -1,4 +1,5 @@
@import 'components/field_data_expanded_row/number_content';
@import 'components/field_count_stats/index';
.mlDataVisualizerFieldExpandedRow {
padding-left: $euiSize * 4;
@ -35,6 +36,7 @@
}
}
.mlDataVisualizerSummaryTable {
max-width: 350px;
.euiTableRow > .euiTableRowCell {
border-bottom: 0;
}
@ -42,4 +44,7 @@
display: none;
}
}
.mlDataVisualizerSummaryTableWrapper {
max-width: 350px;
}
}

View file

@ -0,0 +1,3 @@
.mlDataVisualizerFieldCountContainer {
max-width: 300px;
}

View file

@ -0,0 +1,12 @@
/*
* 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 { TotalFieldsCount, TotalFieldsCountProps, TotalFieldsStats } from './total_fields_count';
export {
MetricFieldsCount,
MetricFieldsCountProps,
MetricFieldsStats,
} from './metric_fields_count';

View file

@ -0,0 +1,67 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiNotificationBadge, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC } from 'react';
export interface MetricFieldsStats {
visibleMetricsCount: number;
totalMetricFieldsCount: number;
}
export interface MetricFieldsCountProps {
metricsStats?: MetricFieldsStats;
}
export const MetricFieldsCount: FC<MetricFieldsCountProps> = ({ metricsStats }) => {
if (
!metricsStats ||
metricsStats.visibleMetricsCount === undefined ||
metricsStats.totalMetricFieldsCount === undefined
)
return null;
return (
<>
{metricsStats && (
<EuiFlexGroup
gutterSize="s"
alignItems="center"
className="mlDataVisualizerFieldCountContainer"
data-test-subj="mlDataVisualizerMetricFieldsSummary"
>
<EuiFlexItem grow={false}>
<EuiText>
<h5>
<FormattedMessage
id="xpack.ml.dataVisualizer.searchPanel.numberFieldsLabel"
defaultMessage="Number fields"
/>
</h5>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge
color="subdued"
size="m"
data-test-subj="mlDataVisualizerVisibleMetricFieldsCount"
>
<strong>{metricsStats.visibleMetricsCount}</strong>
</EuiNotificationBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s" data-test-subj="mlDataVisualizerMetricFieldsCount">
<FormattedMessage
id="xpack.ml.dataVisualizer.searchPanel.ofFieldsTotal"
defaultMessage="of {totalCount} total"
values={{ totalCount: metricsStats.totalMetricFieldsCount }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
);
};

View file

@ -0,0 +1,66 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiNotificationBadge, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC } from 'react';
export interface TotalFieldsStats {
visibleFieldsCount: number;
totalFieldsCount: number;
}
export interface TotalFieldsCountProps {
fieldsCountStats?: TotalFieldsStats;
}
export const TotalFieldsCount: FC<TotalFieldsCountProps> = ({ fieldsCountStats }) => {
if (
!fieldsCountStats ||
fieldsCountStats.visibleFieldsCount === undefined ||
fieldsCountStats.totalFieldsCount === undefined
)
return null;
return (
<EuiFlexGroup
gutterSize="s"
alignItems="center"
className="mlDataVisualizerFieldCountContainer"
data-test-subj="mlDataVisualizerFieldsSummary"
>
<EuiFlexItem grow={false}>
<EuiText>
<h5>
<FormattedMessage
id="xpack.ml.dataVisualizer.searchPanel.allFieldsLabel"
defaultMessage="All fields"
/>
</h5>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge
color="subdued"
size="m"
data-test-subj="mlDataVisualizerVisibleFieldsCount"
>
<strong>{fieldsCountStats.visibleFieldsCount}</strong>
</EuiNotificationBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s" data-test-subj="mlDataVisualizerTotalFieldsCount">
<FormattedMessage
id="xpack.ml.dataVisualizer.searchPanel.ofFieldsTotal"
defaultMessage="of {totalCount} total"
values={{ totalCount: fieldsCountStats.totalFieldsCount }}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,143 @@
/*
* 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, useMemo } from 'react';
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { Axis, BarSeries, Chart, Settings } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { getTFPercentage } from '../../utils';
import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place';
import { useDataVizChartTheme } from '../../hooks';
import { DocumentStatsTable } from './document_stats';
function getPercentLabel(value: number): string {
if (value === 0) {
return '0%';
}
if (value >= 0.1) {
return `${roundToDecimalPlace(value)}%`;
} else {
return '< 0.1%';
}
}
function getFormattedValue(value: number, totalCount: number): string {
const percentage = (value / totalCount) * 100;
return `${value} (${getPercentLabel(percentage)})`;
}
const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 100;
export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
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.ml.fieldDataCardExpandedRow.booleanContent.trueCountLabel"
defaultMessage="true"
/>
),
value: getFormattedValue(trueCount, count),
},
{
function: 'false',
display: (
<FormattedMessage
id="xpack.ml.fieldDataCardExpandedRow.booleanContent.falseCountLabel"
defaultMessage="false"
/>
),
value: getFormattedValue(falseCount, count),
},
];
const summaryTableColumns = [
{
name: '',
render: (summaryItem: { display: ReactNode }) => summaryItem.display,
width: '75px',
},
{
field: 'value',
name: '',
render: (v: string) => <strong>{v}</strong>,
},
];
const summaryTableTitle = i18n.translate(
'xpack.ml.fieldDataCardExpandedRow.booleanContent.summaryTableTitle',
{
defaultMessage: 'Summary',
}
);
return (
<EuiFlexGroup data-test-subj={'mlDVBooleanContent'} gutterSize={'xl'}>
<DocumentStatsTable config={config} />
<EuiFlexItem className={'mlDataVisualizerSummaryTableWrapper'}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable
className={'mlDataVisualizerSummaryTable'}
compressed
items={summaryTableItems}
columns={summaryTableColumns}
tableCaption={summaryTableTitle}
/>
</EuiFlexItem>
<EuiFlexItem>
<ExpandedRowFieldHeader>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardBoolean.valuesLabel"
defaultMessage="Values"
/>
</ExpandedRowFieldHeader>
<EuiSpacer size="xs" />
<Chart renderer="canvas" size={{ height: BOOLEAN_DISTRIBUTION_CHART_HEIGHT }}>
<Axis id="bottom" position="bottom" showOverlappingTicks />
<Axis
id="left2"
title="Left axis"
hide={true}
tickFormat={(d: any) => getFormattedValue(d, count)}
/>
<Settings showLegend={false} theme={theme} />
<BarSeries
id={config.fieldName || fieldFormat}
data={[
{
x: 'true',
count: formattedPercentages.trueCount,
},
{
x: 'false',
count: formattedPercentages.falseCount,
},
]}
splitSeriesAccessors={['x']}
stackAccessors={['x']}
xAccessor="x"
xScaleType="ordinal"
yAccessors={['count']}
yScaleType="linear"
/>
</Chart>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -5,14 +5,15 @@
*/
import React, { FC, ReactNode } from 'react';
import { EuiBasicTable } from '@elastic/eui';
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
// @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FieldDataCardProps } from '../field_data_card';
import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats';
const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS';
interface SummaryTableItem {
function: string;
@ -20,7 +21,7 @@ interface SummaryTableItem {
value: number | string | undefined | null;
}
export const DateContent: FC<FieldDataCardProps> = ({ config }) => {
export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
@ -38,7 +39,7 @@ export const DateContent: FC<FieldDataCardProps> = ({ config }) => {
defaultMessage="earliest"
/>
),
value: formatDate(earliest, TIME_FORMAT),
value: typeof earliest === 'string' ? earliest : formatDate(earliest, TIME_FORMAT),
},
{
function: 'latest',
@ -48,7 +49,7 @@ export const DateContent: FC<FieldDataCardProps> = ({ config }) => {
defaultMessage="latest"
/>
),
value: formatDate(latest, TIME_FORMAT),
value: typeof latest === 'string' ? latest : formatDate(latest, TIME_FORMAT),
},
];
const summaryTableColumns = [
@ -65,17 +66,20 @@ export const DateContent: FC<FieldDataCardProps> = ({ config }) => {
];
return (
<>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable<SummaryTableItem>
className={'mlDataVisualizerSummaryTable'}
data-test-subj={'mlDateSummaryTable'}
compressed
items={summaryTableItems}
columns={summaryTableColumns}
tableCaption={summaryTableTitle}
tableLayout="auto"
/>
</>
<EuiFlexGroup data-test-subj={'mlDVDateContent'} gutterSize={'xl'}>
<DocumentStatsTable config={config} />
<EuiFlexItem className={'mlDataVisualizerSummaryTableWrapper'}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable<SummaryTableItem>
className={'mlDataVisualizerSummaryTable'}
data-test-subj={'mlDateSummaryTable'}
compressed
items={summaryTableItems}
columns={summaryTableColumns}
tableCaption={summaryTableTitle}
tableLayout="auto"
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,91 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n/react';
import React, { FC, ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBasicTable, EuiFlexItem } from '@elastic/eui';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { FieldDataRowProps } from '../../types';
const metaTableColumns = [
{
name: '',
render: (metaItem: { display: ReactNode }) => metaItem.display,
width: '75px',
},
{
field: 'value',
name: '',
render: (v: string) => <strong>{v}</strong>,
},
];
const metaTableTitle = i18n.translate(
'xpack.ml.fieldDataCardExpandedRow.documentStatsTable.metaTableTitle',
{
defaultMessage: 'Documents stats',
}
);
export const DocumentStatsTable: FC<FieldDataRowProps> = ({ config }) => {
if (
config?.stats === undefined ||
config.stats.cardinality === undefined ||
config.stats.count === undefined ||
config.stats.sampleCount === undefined
)
return null;
const { cardinality, count, sampleCount } = config.stats;
const metaTableItems = [
{
function: 'count',
display: (
<FormattedMessage
id="xpack.ml.fieldDataCardExpandedRow.documentStatsTable.countLabel"
defaultMessage="count"
/>
),
value: count,
},
{
function: 'percentage',
display: (
<FormattedMessage
id="xpack.ml.fieldDataCardExpandedRow.documentStatsTable.percentageLabel"
defaultMessage="percentage"
/>
),
value: `${(count / sampleCount) * 100}%`,
},
{
function: 'distinctValues',
display: (
<FormattedMessage
id="xpack.ml.fieldDataCardExpandedRow.documentStatsTable.distinctValueLabel"
defaultMessage="distinct values"
/>
),
value: cardinality,
},
];
return (
<EuiFlexItem
data-test-subj={'mlDVDocumentStatsContent'}
className={'mlDataVisualizerSummaryTableWrapper'}
>
<ExpandedRowFieldHeader>{metaTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable
className={'mlDataVisualizerSummaryTable'}
compressed
items={metaTableItems}
columns={metaTableColumns}
tableCaption={metaTableTitle}
/>
</EuiFlexItem>
);
};

View file

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

View file

@ -6,11 +6,9 @@
export { BooleanContent } from './boolean_content';
export { DateContent } from './date_content';
export { DocumentCountContent } from './document_count_content';
export { GeoPointContent } from './geo_point_content';
export { KeywordContent } from './keyword_content';
export { IpContent } from './ip_content';
export { NotInDocsContent } from './not_in_docs_content';
export { NumberContent } from './number_content';
export { OtherContent } from './other_content';
export { TextContent } from './text_content';

View file

@ -0,0 +1,38 @@
/*
* 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 { 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';
export const IpContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
const { count, sampleCount, cardinality } = stats;
if (count === undefined || sampleCount === undefined || cardinality === undefined) return null;
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
return (
<EuiFlexGroup gutterSize={'xl'}>
<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>
);
};

View file

@ -0,0 +1,35 @@
/*
* 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, 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';
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'}>
<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>
);
};

View file

@ -9,17 +9,17 @@ import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { FieldDataCardProps } from '../../../index_based/components/field_data_card';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format';
import { numberAsOrdinal } from '../../../../formatters/number_as_ordinal';
import {
MetricDistributionChart,
MetricDistributionChartData,
buildChartDataFromStats,
} from '../../../index_based/components/field_data_card/metric_distribution_chart';
import { TopValues } from '../../../index_based/components/field_data_card/top_values';
} from '../metric_distribution_chart';
import { TopValues } from '../../../index_based/components/field_data_row/top_values';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats';
const METRIC_DISTRIBUTION_CHART_WIDTH = 325;
const METRIC_DISTRIBUTION_CHART_HEIGHT = 200;
@ -30,8 +30,8 @@ interface SummaryTableItem {
value: number | string | undefined | null;
}
export const NumberContent: FC<FieldDataCardProps> = ({ config }) => {
const { stats, fieldFormat } = config;
export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
useEffect(() => {
const chartData = buildChartDataFromStats(stats, METRIC_DISTRIBUTION_CHART_WIDTH);
@ -43,6 +43,7 @@ export const NumberContent: FC<FieldDataCardProps> = ({ config }) => {
if (stats === undefined) return null;
const { min, median, max, distribution } = stats;
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
const summaryTableItems = [
{
@ -96,8 +97,9 @@ export const NumberContent: FC<FieldDataCardProps> = ({ config }) => {
}
);
return (
<EuiFlexGroup direction={'row'} data-test-subj={'mlNumberSummaryTable'} gutterSize={'xl'}>
<EuiFlexItem>
<EuiFlexGroup data-test-subj={'mlDVNumberContent'} gutterSize={'xl'}>
<DocumentStatsTable config={config} />
<EuiFlexItem className={'mlDataVisualizerSummaryTableWrapper'}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable<SummaryTableItem>
className={'mlDataVisualizerSummaryTable'}
@ -105,8 +107,10 @@ export const NumberContent: FC<FieldDataCardProps> = ({ config }) => {
items={summaryTableItems}
columns={summaryTableColumns}
tableCaption={summaryTableTitle}
data-test-subj={'mlNumberSummaryTable'}
/>
</EuiFlexItem>
{stats && (
<EuiFlexItem data-test-subj={'mlTopValues'}>
<EuiFlexItem grow={false}>

View file

@ -0,0 +1,22 @@
/*
* 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 } 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';
export const OtherContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
return (
<EuiFlexGroup gutterSize={'xl'} data-test-subj={'mlDVOtherContent'}>
<DocumentStatsTable config={config} />
{Array.isArray(stats.examples) && <ExamplesList examples={stats.examples} />}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,64 @@
/*
* 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, Fragment } from 'react';
import { EuiCallOut, EuiFlexGroup, 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';
export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
const { examples } = stats;
if (examples === undefined) return null;
const numExamples = examples.length;
return (
<EuiFlexGroup gutterSize={'xl'} data-test-subj={'mlDVTextContent'}>
<EuiFlexItem>
{numExamples > 0 && <ExamplesList examples={examples} />}
{numExamples === 0 && (
<Fragment>
<EuiSpacer size="xl" />
<EuiCallOut
title={i18n.translate('xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle', {
defaultMessage: 'No examples were obtained for this field',
})}
iconType="alert"
>
<FormattedMessage
id="xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription"
defaultMessage="This field was not present in the {sourceParam} field of documents queried."
values={{
sourceParam: <span className="mlFieldDataCard__codeContent">_source</span>,
}}
/>
<EuiSpacer size="s" />
<FormattedMessage
id="xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription"
defaultMessage="It may be populated, for example, using a {copyToParam} parameter in the document mapping, or be pruned from the {sourceParam} field after indexing through the use of {includesParam} and {excludesParam} parameters."
values={{
copyToParam: <span className="mlFieldDataCard__codeContent">copy_to</span>,
sourceParam: <span className="mlFieldDataCard__codeContent">_source</span>,
includesParam: <span className="mlFieldDataCard__codeContent">includes</span>,
excludesParam: <span className="mlFieldDataCard__codeContent">excludes</span>,
}}
/>
</EuiCallOut>
</Fragment>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,40 @@
/*
* 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 { EuiDataGridColumn } from '@elastic/eui';
import { FieldDataRowProps } from '../../types';
import { getTFPercentage } from '../../utils';
import { ColumnChart } from '../../../../components/data_grid/column_chart';
import { OrdinalChartData } from '../../../../components/data_grid/use_column_chart';
export const BooleanContentPreview: FC<FieldDataRowProps> = ({ config }) => {
const chartData = useMemo(() => {
const results = getTFPercentage(config);
if (results) {
const data = [
{ key: 'true', key_as_string: 'true', doc_count: results.trueCount },
{ key: 'false', key_as_string: 'false', doc_count: results.falseCount },
];
return { id: config.fieldName, cardinality: 2, data, type: 'boolean' } as OrdinalChartData;
}
}, [config]);
if (!chartData || config.fieldName === undefined) return null;
const columnType: EuiDataGridColumn = {
id: config.fieldName,
schema: undefined,
};
const dataTestSubj = `mlDataGridChart-${config.fieldName}`;
return (
<ColumnChart
dataTestSubj={dataTestSubj}
chartData={chartData}
columnType={columnType}
hideLabel={true}
/>
);
};

View file

@ -7,10 +7,10 @@
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
import React from 'react';
import { FieldDataCardProps } from '../../../index_based/components/field_data_card';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place';
export const DocumentStat = ({ config }: FieldDataCardProps) => {
export const DocumentStat = ({ config }: FieldDataRowProps) => {
const { stats } = config;
if (stats === undefined) return null;

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { DataVisualizerDataGrid } from './stats_datagrid';
export { BooleanContentPreview } from './boolean_content_preview';

View file

@ -7,18 +7,22 @@
import React, { FC, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import classNames from 'classnames';
import { FieldDataCardProps } from '../../../index_based/components/field_data_card';
import {
MetricDistributionChart,
MetricDistributionChartData,
buildChartDataFromStats,
} from '../../../index_based/components/field_data_card/metric_distribution_chart';
} from '../metric_distribution_chart';
import { formatSingleValue } from '../../../../formatters/format_value';
import { FieldVisConfig } from '../../types';
const METRIC_DISTRIBUTION_CHART_WIDTH = 150;
const METRIC_DISTRIBUTION_CHART_HEIGHT = 80;
export const NumberContentPreview: FC<FieldDataCardProps> = ({ config }) => {
export interface NumberContentPreviewProps {
config: FieldVisConfig;
}
export const IndexBasedNumberContentPreview: FC<NumberContentPreviewProps> = ({ config }) => {
const { stats, fieldFormat, fieldName } = config;
const defaultChartData: MetricDistributionChartData[] = [];
const [distributionChartData, setDistributionChartData] = useState(defaultChartData);

View file

@ -6,12 +6,12 @@
import React, { FC } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
import { FieldDataCardProps } from '../../../index_based/components/field_data_card';
import type { FieldDataRowProps } from '../../types/field_data_row';
import { ColumnChart } from '../../../../components/data_grid/column_chart';
import { ChartData } from '../../../../components/data_grid';
import { OrdinalDataItem } from '../../../../components/data_grid/use_column_chart';
export const TopValuesPreview: FC<FieldDataCardProps> = ({ config }) => {
export const TopValuesPreview: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config;
if (stats === undefined) return null;
const { topValues, cardinality } = stats;

View file

@ -19,13 +19,10 @@ import {
TooltipValueFormatter,
} from '@elastic/charts';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { MetricDistributionChartTooltipHeader } from './metric_distribution_chart_tooltip_header';
import { useUiSettings } from '../../../../../contexts/kibana/use_ui_settings_context';
import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format';
import type { ChartTooltipValue } from '../../../../../components/chart_tooltip/chart_tooltip_service';
import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format';
import type { ChartTooltipValue } from '../../../../components/chart_tooltip/chart_tooltip_service';
import { useDataVizChartTheme } from '../../hooks';
export interface MetricDistributionChartData {
x: number;
@ -59,9 +56,7 @@ export const MetricDistributionChart: FC<Props> = ({
defaultMessage: 'distribution',
});
const IS_DARK_THEME = useUiSettings().get('theme:darkMode');
const themeName = IS_DARK_THEME ? darkTheme : lightTheme;
const AREA_SERIES_COLOR = themeName.euiColorVis0;
const theme = useDataVizChartTheme();
const headerFormatter: TooltipValueFormatter = (tooltipData: ChartTooltipValue) => {
const xValue = tooltipData.value;
@ -81,47 +76,7 @@ export const MetricDistributionChart: FC<Props> = ({
return (
<div data-test-subj="mlFieldDataMetricDistributionChart">
<Chart size={{ width, height }}>
<Settings
theme={{
axes: {
tickLabel: {
fontSize: parseInt(themeName.euiFontSizeXS, 10),
fontFamily: themeName.euiFontFamily,
fontStyle: 'italic',
},
},
background: { color: 'transparent' },
chartMargins: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
chartPaddings: {
left: 0,
right: 0,
top: 4,
bottom: 0,
},
scales: { barsPadding: 0.1 },
colors: {
vizColors: [AREA_SERIES_COLOR],
},
areaSeriesStyle: {
line: {
strokeWidth: 1,
visible: true,
},
point: {
visible: false,
radius: 0,
opacity: 0,
},
area: { visible: true, opacity: 1 },
},
}}
tooltip={{ headerFormatter }}
/>
<Settings theme={theme} tooltip={{ headerFormatter }} />
<Axis
id="bottom"
position={Position.Bottom}

View file

@ -9,7 +9,7 @@ import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { MetricDistributionChartData } from './metric_distribution_chart';
import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format';
import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format';
interface Props {
chartPoint: MetricDistributionChartData | undefined;

View file

@ -19,53 +19,56 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiTableComputedColumnType } from '@elastic/eui/src/components/basic_table/table_types';
import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types';
import { FieldTypeIcon } from '../../components/field_type_icon';
import { FieldVisConfig } from '../index_based/common';
import { DataVisualizerFieldExpandedRow } from './expanded_row';
import { DocumentStat } from './components/field_data_row/document_stats';
import { DistinctValues } from './components/field_data_row/distinct_values';
import { NumberContentPreview } from './components/field_data_row/number_content_preview';
import { DataVisualizerIndexBasedAppState } from '../../../../common/types/ml_url_generator';
import { IndexBasedNumberContentPreview } from './components/field_data_row/number_content_preview';
import {
DataVisualizerFileBasedAppState,
DataVisualizerIndexBasedAppState,
} from '../../../../common/types/ml_url_generator';
import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings';
import { TopValuesPreview } from './components/field_data_row/top_values_preview';
import type { MlJobFieldType } from '../../../../common/types/field_types';
const FIELD_NAME = 'fieldName';
import {
FieldVisConfig,
FileBasedFieldVisConfig,
isIndexBasedFieldVisConfig,
} from './types/field_vis_config';
import { FileBasedNumberContentPreview } from '../file_based/components/field_data_row';
import { BooleanContentPreview } from './components/field_data_row';
interface DataVisualizerDataGrid {
items: FieldVisConfig[];
pageState: DataVisualizerIndexBasedAppState;
updatePageState: (update: Partial<DataVisualizerIndexBasedAppState>) => void;
}
const FIELD_NAME = 'fieldName';
export type ItemIdToExpandedRowMap = Record<string, JSX.Element>;
function getItemIdToExpandedRowMap(
itemIds: string[],
items: FieldVisConfig[]
): ItemIdToExpandedRowMap {
return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => {
const item = items.find((fieldVisConfig) => fieldVisConfig[FIELD_NAME] === fieldName);
if (item !== undefined) {
m[fieldName] = <DataVisualizerFieldExpandedRow item={item} />;
}
return m;
}, {} as ItemIdToExpandedRowMap);
type DataVisualizerTableItem = FieldVisConfig | FileBasedFieldVisConfig;
interface DataVisualizerTableProps<T> {
items: T[];
pageState: DataVisualizerIndexBasedAppState | DataVisualizerFileBasedAppState;
updatePageState: (
update: Partial<DataVisualizerIndexBasedAppState | DataVisualizerFileBasedAppState>
) => void;
getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap;
}
export const DataVisualizerDataGrid = ({
export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
items,
pageState,
updatePageState,
}: DataVisualizerDataGrid) => {
getItemIdToExpandedRowMap,
}: DataVisualizerTableProps<T>) => {
const [expandedRowItemIds, setExpandedRowItemIds] = useState<string[]>([]);
const [expandAll, toggleExpandAll] = useState<boolean>(false);
const { onTableChange, pagination, sorting } = useTableSettings<FieldVisConfig>(
const { onTableChange, pagination, sorting } = useTableSettings<DataVisualizerTableItem>(
items,
pageState,
updatePageState
);
const showDistributions: boolean = pageState.showDistributions ?? true;
const showDistributions: boolean =
('showDistributions' in pageState && pageState.showDistributions) ?? true;
const toggleShowDistribution = () => {
updatePageState({
...pageState,
@ -73,7 +76,7 @@ export const DataVisualizerDataGrid = ({
});
};
function toggleDetails(item: FieldVisConfig) {
function toggleDetails(item: DataVisualizerTableItem) {
if (item.fieldName === undefined) return;
const index = expandedRowItemIds.indexOf(item.fieldName);
if (index !== -1) {
@ -87,7 +90,7 @@ export const DataVisualizerDataGrid = ({
}
const columns = useMemo(() => {
const expanderColumn: EuiTableComputedColumnType<FieldVisConfig> = {
const expanderColumn: EuiTableComputedColumnType<DataVisualizerTableItem> = {
name: (
<EuiButtonIcon
data-test-subj={`mlToggleDetailsForAllRowsButton ${expandAll ? 'expanded' : 'collapsed'}`}
@ -107,7 +110,7 @@ export const DataVisualizerDataGrid = ({
align: RIGHT_ALIGNMENT,
width: '40px',
isExpander: true,
render: (item: FieldVisConfig) => {
render: (item: DataVisualizerTableItem) => {
if (item.fieldName === undefined) return null;
const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown';
return (
@ -167,8 +170,10 @@ export const DataVisualizerDataGrid = ({
name: i18n.translate('xpack.ml.datavisualizer.dataGrid.documentsCountColumnName', {
defaultMessage: 'Documents (%)',
}),
render: (value: number | undefined, item: FieldVisConfig) => <DocumentStat config={item} />,
sortable: (item: FieldVisConfig) => item?.stats?.count,
render: (value: number | undefined, item: DataVisualizerTableItem) => (
<DocumentStat config={item} />
),
sortable: (item: DataVisualizerTableItem) => item?.stats?.count,
align: LEFT_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'mlDataVisualizerTableColumnDocumentsCount',
},
@ -203,15 +208,27 @@ export const DataVisualizerDataGrid = ({
/>
</div>
),
render: (item: FieldVisConfig) => {
render: (item: DataVisualizerTableItem) => {
if (item === undefined || showDistributions === false) return null;
if (item.type === 'keyword' && item.stats?.topValues !== undefined) {
if (
(item.type === ML_JOB_FIELD_TYPES.KEYWORD || item.type === ML_JOB_FIELD_TYPES.IP) &&
item.stats?.topValues !== undefined
) {
return <TopValuesPreview config={item} />;
}
if (item.type === 'number' && item.stats?.distribution !== undefined) {
return <NumberContentPreview config={item} />;
if (item.type === ML_JOB_FIELD_TYPES.NUMBER) {
if (isIndexBasedFieldVisConfig(item) && item.stats?.distribution !== undefined) {
return <IndexBasedNumberContentPreview config={item} />;
} else {
return <FileBasedNumberContentPreview config={item} />;
}
}
if (item.type === ML_JOB_FIELD_TYPES.BOOLEAN) {
return <BooleanContentPreview config={item} />;
}
return null;
},
align: LEFT_ALIGNMENT as HorizontalAlignment,
@ -230,7 +247,7 @@ export const DataVisualizerDataGrid = ({
return (
<EuiFlexItem data-test-subj="mlDataVisualizerTableContainer">
<EuiInMemoryTable<FieldVisConfig>
<EuiInMemoryTable<DataVisualizerTableItem>
className={'mlDataVisualizer'}
items={items}
itemId={FIELD_NAME}

View file

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

View file

@ -0,0 +1,54 @@
/*
* 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 type { PartialTheme } from '@elastic/charts';
import { useMemo } from 'react';
import { useCurrentEuiTheme } from '../../../components/color_range_legend';
export const useDataVizChartTheme = (): PartialTheme => {
const { euiTheme } = useCurrentEuiTheme();
const chartTheme = useMemo(() => {
const AREA_SERIES_COLOR = euiTheme.euiColorVis0;
return {
axes: {
tickLabel: {
fontSize: parseInt(euiTheme.euiFontSizeXS, 10),
fontFamily: euiTheme.euiFontFamily,
fontStyle: 'italic',
},
},
background: { color: 'transparent' },
chartMargins: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
chartPaddings: {
left: 0,
right: 0,
top: 4,
bottom: 0,
},
scales: { barsPadding: 0.1 },
colors: {
vizColors: [AREA_SERIES_COLOR],
},
areaSeriesStyle: {
line: {
strokeWidth: 1,
visible: true,
},
point: {
visible: false,
radius: 0,
opacity: 0,
},
area: { visible: true, opacity: 1 },
},
};
}, [euiTheme]);
return chartTheme;
};

View file

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

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config';
export interface FieldDataRowProps {
config: FieldVisConfig | FileBasedFieldVisConfig;
}

View file

@ -50,7 +50,7 @@ export interface FieldVisStats {
max?: number;
median?: number;
min?: number;
topValues?: Array<{ key: number; doc_count: number }>;
topValues?: Array<{ key: number | string; doc_count: number }>;
topValuesSampleSize?: number;
topValuesSamplerShardSize?: number;
examples?: Array<string | object>;
@ -70,3 +70,28 @@ export interface FieldVisConfig {
fieldFormat?: any;
isUnsupportedType?: boolean;
}
export interface FileBasedFieldVisConfig {
type: MlJobFieldType;
fieldName?: string;
stats?: FieldVisStats;
format?: string;
}
export interface FileBasedUnknownFieldVisConfig {
fieldName: string;
type: 'text' | 'unknown';
stats: { mean: number; count: number; sampleCount: number; cardinality: number };
}
export function isFileBasedFieldVisConfig(
field: FieldVisConfig | FileBasedFieldVisConfig
): field is FileBasedFieldVisConfig {
return !field.hasOwnProperty('existsInDocs');
}
export function isIndexBasedFieldVisConfig(
field: FieldVisConfig | FileBasedFieldVisConfig
): field is FieldVisConfig {
return field.hasOwnProperty('existsInDocs');
}

View file

@ -0,0 +1,15 @@
/*
* 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 { FieldDataRowProps } from './field_data_row';
export {
FieldVisConfig,
FileBasedFieldVisConfig,
FieldVisStats,
MetricFieldVisStats,
isFileBasedFieldVisConfig,
isIndexBasedFieldVisConfig,
} from './field_vis_config';

View file

@ -0,0 +1,37 @@
/*
* 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 { FileBasedFieldVisConfig } from './types';
export const getTFPercentage = (config: FileBasedFieldVisConfig) => {
const { stats } = config;
if (stats === undefined) return null;
const { count } = stats;
// use stats from index based config
let { trueCount, falseCount } = stats;
// use stats from file based find structure results
if (stats.trueCount === undefined || stats.falseCount === undefined) {
if (config?.stats?.topValues) {
config.stats.topValues.forEach((doc) => {
if (doc.doc_count !== undefined) {
if (doc.key.toString().toLowerCase() === 'false') {
falseCount = doc.doc_count;
}
if (doc.key.toString().toLowerCase() === 'true') {
trueCount = doc.doc_count;
}
}
});
}
}
if (count === undefined || trueCount === undefined || falseCount === undefined) return null;
return {
count,
trueCount,
falseCount,
};
};

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export function roundToDecimalPlace(num: number, dp: number = 2): number | string {
export function roundToDecimalPlace(num?: number, dp: number = 2): number | string {
if (num === undefined) return '';
if (num % 1 === 0) {
// no decimal place
return num;

View file

@ -13126,18 +13126,6 @@
"xpack.ml.fieldDataCard.cardDate.summaryTableTitle": "まとめ",
"xpack.ml.fieldDataCard.cardIp.topValuesLabel": "トップの値",
"xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "トップの値",
"xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel": "分布",
"xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel": "トップの値",
"xpack.ml.fieldDataCard.cardNumber.displayingPercentilesLabel": "{minPercent} - {maxPercent} パーセンタイルを表示中",
"xpack.ml.fieldDataCard.cardNumber.distinctCountDescription": "{cardinality} 個の特徴的な {cardinality, plural, other {値}}",
"xpack.ml.fieldDataCard.cardNumber.documentsCountDescription": "{count, plural, other {# 個のドキュメント}} ({docsPercent}%)",
"xpack.ml.fieldDataCard.cardNumber.maxLabel": "最高",
"xpack.ml.fieldDataCard.cardNumber.medianLabel": "中間",
"xpack.ml.fieldDataCard.cardNumber.minLabel": "分",
"xpack.ml.fieldDataCard.cardNumber.selectMetricDetailsDisplayAriaLabel": "メトリック詳細の表示オプションを選択してください",
"xpack.ml.fieldDataCard.cardOther.cardTypeLabel": "{cardType} タイプ",
"xpack.ml.fieldDataCard.cardOther.distinctCountDescription": "{cardinality} 個の特徴的な {cardinality, plural, other {値}}",
"xpack.ml.fieldDataCard.cardOther.documentsCountDescription": "{count, plural, other {# 個のドキュメント}} ({docsPercent}%)",
"xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "たとえば、ドキュメントマッピングで {copyToParam} パラメーターを使ったり、{includesParam} と {excludesParam} パラメーターを使用してインデックスした後に {sourceParam} フィールドから切り取ったりして入力される場合があります。",
"xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "このフィールドはクエリが実行されたドキュメントの {sourceParam} フィールドにありませんでした。",
"xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "このフィールドの例が取得されませんでした",
@ -13221,13 +13209,9 @@
"xpack.ml.fileDatavisualizer.explanationFlyout.closeButton": "閉じる",
"xpack.ml.fileDatavisualizer.explanationFlyout.content": "分析結果を生成した論理ステップ。",
"xpack.ml.fileDatavisualizer.explanationFlyout.title": "分析説明",
"xpack.ml.fileDatavisualizer.fieldStatsCard.distinctCountDescription": "{fieldCardinality} 個の特徴的な {fieldCardinality, plural, other {値}}",
"xpack.ml.fileDatavisualizer.fieldStatsCard.documentsCountDescription": "{fieldCount, plural, other {# 個のドキュメント}} ({fieldPercent}%)",
"xpack.ml.fileDatavisualizer.fieldStatsCard.maxTitle": "最高",
"xpack.ml.fileDatavisualizer.fieldStatsCard.medianTitle": "中間",
"xpack.ml.fileDatavisualizer.fieldStatsCard.minTitle": "分",
"xpack.ml.fileDatavisualizer.fieldStatsCard.noFieldInformationAvailableDescription": "フィールド情報がありません",
"xpack.ml.fileDatavisualizer.fieldStatsCard.topStatsValuesDescription": "トップの値",
"xpack.ml.fileDatavisualizer.fileBeatConfig.paths": "ファイルのパスをここに追加してください",
"xpack.ml.fileDatavisualizer.fileBeatConfigFlyout.closeButton": "閉じる",
"xpack.ml.fileDatavisualizer.fileBeatConfigFlyout.copyButton": "クリップボードにコピー",
@ -13314,7 +13298,6 @@
"xpack.ml.fileDatavisualizer.resultsLinks.openInDataVisualizerTitle": "データビジュアライザーを開く",
"xpack.ml.fileDatavisualizer.resultsLinks.viewIndexInDiscoverTitle": "インデックスをディスカバリで表示",
"xpack.ml.fileDatavisualizer.resultsView.analysisExplanationButtonLabel": "分析説明",
"xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName": "ファイル統計",
"xpack.ml.fileDatavisualizer.resultsView.overrideSettingsButtonLabel": "上書き設定",
"xpack.ml.fileDatavisualizer.simpleImportSettings.createIndexPatternLabel": "インデックスパターンを作成",
"xpack.ml.fileDatavisualizer.simpleImportSettings.indexNameAriaLabel": "インデックス名、必須フィールド",

View file

@ -13157,18 +13157,6 @@
"xpack.ml.fieldDataCard.cardDate.summaryTableTitle": "摘要",
"xpack.ml.fieldDataCard.cardIp.topValuesLabel": "排名最前值",
"xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "排名最前值",
"xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel": "分布",
"xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel": "排名最前值",
"xpack.ml.fieldDataCard.cardNumber.displayingPercentilesLabel": "显示 {minPercent} - {maxPercent} 百分位数",
"xpack.ml.fieldDataCard.cardNumber.distinctCountDescription": "{cardinality} 个不同的 {cardinality, plural, other {值}}",
"xpack.ml.fieldDataCard.cardNumber.documentsCountDescription": "{count, plural, other {# 个文档}} ({docsPercent}%)",
"xpack.ml.fieldDataCard.cardNumber.maxLabel": "最大值",
"xpack.ml.fieldDataCard.cardNumber.medianLabel": "中值",
"xpack.ml.fieldDataCard.cardNumber.minLabel": "最小值",
"xpack.ml.fieldDataCard.cardNumber.selectMetricDetailsDisplayAriaLabel": "选择指标详情的显示选项",
"xpack.ml.fieldDataCard.cardOther.cardTypeLabel": "{cardType} 类型",
"xpack.ml.fieldDataCard.cardOther.distinctCountDescription": "{cardinality} 个不同的 {cardinality, plural, other {值}}",
"xpack.ml.fieldDataCard.cardOther.documentsCountDescription": "{count, plural, other {# 个文档}} ({docsPercent}%)",
"xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "例如,可以使用文档映射中的 {copyToParam} 参数进行填充,也可以在索引后通过使用 {includesParam} 和 {excludesParam} 参数从 {sourceParam} 字段中修剪。",
"xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "查询的文档的 {sourceParam} 字段中不存在此字段。",
"xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "没有获取此字段的示例",
@ -13252,13 +13240,9 @@
"xpack.ml.fileDatavisualizer.explanationFlyout.closeButton": "关闭",
"xpack.ml.fileDatavisualizer.explanationFlyout.content": "产生分析结果的逻辑步骤。",
"xpack.ml.fileDatavisualizer.explanationFlyout.title": "分析说明",
"xpack.ml.fileDatavisualizer.fieldStatsCard.distinctCountDescription": "{fieldCardinality} 个不同的{fieldCardinality, plural, other {值}}",
"xpack.ml.fileDatavisualizer.fieldStatsCard.documentsCountDescription": "{fieldCount, plural, other {# 个文档}} ({fieldPercent}%)",
"xpack.ml.fileDatavisualizer.fieldStatsCard.maxTitle": "最大值",
"xpack.ml.fileDatavisualizer.fieldStatsCard.medianTitle": "中值",
"xpack.ml.fileDatavisualizer.fieldStatsCard.minTitle": "最小值",
"xpack.ml.fileDatavisualizer.fieldStatsCard.noFieldInformationAvailableDescription": "没有可用的字段信息",
"xpack.ml.fileDatavisualizer.fieldStatsCard.topStatsValuesDescription": "排在前面的值",
"xpack.ml.fileDatavisualizer.fileBeatConfig.paths": "在此处将路径添加您的文件中",
"xpack.ml.fileDatavisualizer.fileBeatConfigFlyout.closeButton": "关闭",
"xpack.ml.fileDatavisualizer.fileBeatConfigFlyout.copyButton": "复制到剪贴板",
@ -13346,7 +13330,6 @@
"xpack.ml.fileDatavisualizer.resultsLinks.openInDataVisualizerTitle": "在数据可视化工具中打开",
"xpack.ml.fileDatavisualizer.resultsLinks.viewIndexInDiscoverTitle": "在 Discover 中查看索引",
"xpack.ml.fileDatavisualizer.resultsView.analysisExplanationButtonLabel": "分析说明",
"xpack.ml.fileDatavisualizer.resultsView.fileStatsTabName": "文件统计",
"xpack.ml.fileDatavisualizer.resultsView.overrideSettingsButtonLabel": "替代设置",
"xpack.ml.fileDatavisualizer.simpleImportSettings.createIndexPatternLabel": "创建索引模式",
"xpack.ml.fileDatavisualizer.simpleImportSettings.indexNameAriaLabel": "索引名称,必填字段",

View file

@ -7,6 +7,7 @@
import path from 'path';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types';
export default function ({ getService }: FtrProviderContext) {
const ml = getService('ml');
@ -17,11 +18,98 @@ export default function ({ getService }: FtrProviderContext) {
filePath: path.join(__dirname, 'files_to_import', 'artificial_server_log'),
indexName: 'user-import_1',
createIndexPattern: false,
fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER, ML_JOB_FIELD_TYPES.DATE],
fieldNameFilters: ['clientip'],
expected: {
results: {
title: 'artificial_server_log',
numberOfFields: 4,
},
metricFields: [
{
fieldName: 'bytes',
type: ML_JOB_FIELD_TYPES.NUMBER,
docCountFormatted: '19 (100%)',
statsMaxDecimalPlaces: 3,
topValuesCount: 8,
},
{
fieldName: 'httpversion',
type: ML_JOB_FIELD_TYPES.NUMBER,
docCountFormatted: '19 (100%)',
statsMaxDecimalPlaces: 3,
topValuesCount: 1,
},
{
fieldName: 'response',
type: ML_JOB_FIELD_TYPES.NUMBER,
docCountFormatted: '19 (100%)',
statsMaxDecimalPlaces: 3,
topValuesCount: 3,
},
],
nonMetricFields: [
{
fieldName: 'timestamp',
type: ML_JOB_FIELD_TYPES.DATE,
docCountFormatted: '19 (100%)',
exampleCount: 10,
},
{
fieldName: 'agent',
type: ML_JOB_FIELD_TYPES.KEYWORD,
exampleCount: 8,
docCountFormatted: '19 (100%)',
},
{
fieldName: 'auth',
type: ML_JOB_FIELD_TYPES.KEYWORD,
exampleCount: 1,
docCountFormatted: '19 (100%)',
},
{
fieldName: 'ident',
type: ML_JOB_FIELD_TYPES.KEYWORD,
exampleCount: 1,
docCountFormatted: '19 (100%)',
},
{
fieldName: 'verb',
type: ML_JOB_FIELD_TYPES.KEYWORD,
exampleCount: 1,
docCountFormatted: '19 (100%)',
},
{
fieldName: 'request',
type: ML_JOB_FIELD_TYPES.KEYWORD,
exampleCount: 2,
docCountFormatted: '19 (100%)',
},
{
fieldName: 'referrer',
type: ML_JOB_FIELD_TYPES.KEYWORD,
exampleCount: 1,
docCountFormatted: '19 (100%)',
},
{
fieldName: 'clientip',
type: ML_JOB_FIELD_TYPES.IP,
exampleCount: 7,
docCountFormatted: '19 (100%)',
},
{
fieldName: 'message',
type: ML_JOB_FIELD_TYPES.TEXT,
exampleCount: 10,
docCountFormatted: '19 (100%)',
},
],
visibleMetricFieldsCount: 3,
totalMetricFieldsCount: 3,
populatedFieldsCount: 12,
totalFieldsCount: 12,
fieldTypeFiltersResultCount: 4,
fieldNameFiltersResultCount: 1,
},
},
];
@ -63,8 +151,65 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataVisualizerFileBased.assertFileContentPanelExists();
await ml.dataVisualizerFileBased.assertSummaryPanelExists();
await ml.dataVisualizerFileBased.assertFileStatsPanelExists();
await ml.dataVisualizerFileBased.assertNumberOfFieldCards(
testData.expected.results.numberOfFields
await ml.testExecution.logTestStep(
`displays elements in the data visualizer table correctly`
);
await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist();
await ml.dataVisualizerIndexBased.assertVisibleMetricFieldsCount(
testData.expected.visibleMetricFieldsCount
);
await ml.dataVisualizerIndexBased.assertTotalMetricFieldsCount(
testData.expected.totalMetricFieldsCount
);
await ml.dataVisualizerIndexBased.assertVisibleFieldsCount(
testData.expected.totalFieldsCount
);
await ml.dataVisualizerIndexBased.assertTotalFieldsCount(
testData.expected.totalFieldsCount
);
await ml.testExecution.logTestStep(
'displays details for metric fields and non-metric fields correctly'
);
await ml.dataVisualizerTable.ensureNumRowsPerPage(25);
for (const fieldRow of testData.expected.metricFields) {
await ml.dataVisualizerTable.assertNumberFieldContents(
fieldRow.fieldName,
fieldRow.docCountFormatted,
fieldRow.topValuesCount,
false
);
}
for (const fieldRow of testData.expected.nonMetricFields!) {
await ml.dataVisualizerTable.assertNonMetricFieldContents(
fieldRow.type,
fieldRow.fieldName!,
fieldRow.docCountFormatted,
fieldRow.exampleCount
);
}
await ml.testExecution.logTestStep('sets and resets field type filter correctly');
await ml.dataVisualizerTable.setFieldTypeFilter(
testData.fieldTypeFilters,
testData.expected.fieldTypeFiltersResultCount
);
await ml.dataVisualizerTable.removeFieldTypeFilter(
testData.fieldTypeFilters,
testData.expected.totalFieldsCount
);
await ml.testExecution.logTestStep('sets and resets field name filter correctly');
await ml.dataVisualizerTable.setFieldNameFilter(
testData.fieldNameFilters,
testData.expected.fieldNameFiltersResultCount
);
await ml.dataVisualizerTable.removeFieldNameFilter(
testData.fieldNameFilters,
testData.expected.totalFieldsCount
);
await ml.testExecution.logTestStep('loads the import settings page');

View file

@ -1,19 +1,20 @@
2018-01-06 16:56:14.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.0
2018-01-06 16:56:15.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.1
2018-01-06 16:56:16.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.2
2018-01-06 16:56:17.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.3
2018-01-06 16:56:18.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.0
2018-01-06 16:56:19.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.2
2018-01-06 16:56:20.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.3
2018-01-06 16:56:21.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.4
2018-01-06 16:56:22.295748 WARN host:'Server A' Disk watermark 80%
2018-01-06 17:16:23.295748 WARN host:'Server A' Disk watermark 90%
2018-01-06 17:36:10.295748 ERROR host:'Server A' Main process crashed
2018-01-06 17:36:14.295748 INFO host:'Server A' Connection from ip 123.456.789.0 closed
2018-01-06 17:36:15.295748 INFO host:'Server A' Connection from ip 123.456.789.1 closed
2018-01-06 17:36:16.295748 INFO host:'Server A' Connection from ip 123.456.789.2 closed
2018-01-06 17:36:17.295748 INFO host:'Server A' Connection from ip 123.456.789.3 closed
2018-01-06 17:46:11.295748 INFO host:'Server B' Some special characters °!"§$%&/()=?`'^²³{[]}\+*~#'-_.:,;µ|<>äöüß
2018-01-06 17:46:12.295748 INFO host:'Server B' Shutting down
93.180.71.3 - - [17/May/2015:08:05:32 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
93.180.71.3 - - [17/May/2015:08:05:23 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
80.91.33.133 - - [17/May/2015:08:05:24 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)"
217.168.17.5 - - [17/May/2015:08:05:34 +0000] "GET /downloads/product_1 HTTP/1.1" 200 490 "-" "Debian APT-HTTP/1.3 (0.8.10.3)"
217.168.17.5 - - [17/May/2015:08:05:09 +0000] "GET /downloads/product_2 HTTP/1.1" 200 490 "-" "Debian APT-HTTP/1.3 (0.8.10.3)"
93.180.71.3 - - [17/May/2015:08:05:57 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
217.168.17.5 - - [17/May/2015:08:05:02 +0000] "GET /downloads/product_2 HTTP/1.1" 404 337 "-" "Debian APT-HTTP/1.3 (0.8.10.3)"
217.168.17.5 - - [17/May/2015:08:05:42 +0000] "GET /downloads/product_1 HTTP/1.1" 404 332 "-" "Debian APT-HTTP/1.3 (0.8.10.3)"
80.91.33.133 - - [17/May/2015:08:05:01 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)"
93.180.71.3 - - [17/May/2015:08:05:27 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
217.168.17.5 - - [17/May/2015:08:05:12 +0000] "GET /downloads/product_2 HTTP/1.1" 200 3316 "-" "Some special characters °!"§$%&/()=?`'^²³{[]}\+*~#'-_.:,;µ|<>äöüß"
188.138.60.101 - - [17/May/2015:08:05:49 +0000] "GET /downloads/product_2 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.9.7.9)"
80.91.33.133 - - [17/May/2015:08:05:14 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.16)"
46.4.66.76 - - [17/May/2015:08:05:45 +0000] "GET /downloads/product_1 HTTP/1.1" 404 318 "-" "Debian APT-HTTP/1.3 (1.0.1ubuntu2)"
93.180.71.3 - - [17/May/2015:08:05:26 +0000] "GET /downloads/product_1 HTTP/1.1" 404 324 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
91.234.194.89 - - [17/May/2015:08:05:22 +0000] "GET /downloads/product_2 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.9.7.9)"
80.91.33.133 - - [17/May/2015:08:05:07 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)"
37.26.93.214 - - [17/May/2015:08:05:38 +0000] "GET /downloads/product_2 HTTP/1.1" 404 319 "-" "Go 1.1 package http"
188.138.60.101 - - [17/May/2015:08:05:25 +0000] "GET /downloads/product_2 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.9.7.9)"
93.180.71.3 - - [17/May/2015:08:05:11 +0000] "GET /downloads/product_1 HTTP/1.1" 404 340 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"

View file

@ -6,7 +6,7 @@
import { FtrProviderContext } from '../../../ftr_provider_context';
import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types';
import { FieldVisConfig } from '../../../../../plugins/ml/public/application/datavisualizer/index_based/common';
import { FieldVisConfig } from '../../../../../plugins/ml/public/application/datavisualizer/stats_table/types';
interface MetricFieldVisConfig extends FieldVisConfig {
statsMaxDecimalPlaces: number;

View file

@ -246,7 +246,8 @@ export function MachineLearningDataVisualizerTableProvider(
public async assertNumberFieldContents(
fieldName: string,
docCountFormatted: string,
topValuesCount: number
topValuesCount: number,
checkDistributionPreviewExist = true
) {
await this.assertRowExists(fieldName);
await this.assertFieldDocCount(fieldName, docCountFormatted);
@ -257,7 +258,9 @@ export function MachineLearningDataVisualizerTableProvider(
await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlTopValues'));
await this.assertTopValuesContents(fieldName, topValuesCount);
await this.assertDistributionPreviewExist(fieldName);
if (checkDistributionPreviewExist) {
await this.assertDistributionPreviewExist(fieldName);
}
await this.ensureDetailsClosed(fieldName);
}
@ -320,5 +323,19 @@ export function MachineLearningDataVisualizerTableProvider(
await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount);
}
}
public async ensureNumRowsPerPage(n: 10 | 25 | 100) {
const paginationButton = 'mlDataVisualizerTable > tablePaginationPopoverButton';
await retry.tryForTime(10000, async () => {
await testSubjects.existOrFail(paginationButton);
await testSubjects.click(paginationButton);
await testSubjects.click(`tablePagination-${n}-rows`);
const visibleTexts = await testSubjects.getVisibleText(paginationButton);
const [, pagination] = visibleTexts.split(': ');
expect(pagination).to.eql(n.toString());
});
}
})();
}