diff --git a/x-pack/plugins/ml/common/constants/field_types.ts b/x-pack/plugins/ml/common/constants/field_types.ts index 24b099d176c64..7833049b09f8b 100644 --- a/x-pack/plugins/ml/common/constants/field_types.ts +++ b/x-pack/plugins/ml/common/constants/field_types.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum ML_JOB_FIELD_TYPES { - BOOLEAN = 'boolean', - DATE = 'date', - GEO_POINT = 'geo_point', - IP = 'ip', - KEYWORD = 'keyword', - NUMBER = 'number', - TEXT = 'text', - UNKNOWN = 'unknown', -} +export const ML_JOB_FIELD_TYPES = { + BOOLEAN: 'boolean', + DATE: 'date', + GEO_POINT: 'geo_point', + IP: 'ip', + KEYWORD: 'keyword', + NUMBER: 'number', + TEXT: 'text', + UNKNOWN: 'unknown', +} as const; export const MLCATEGORY = 'mlcategory'; diff --git a/x-pack/plugins/ml/common/types/datavisualizer.ts b/x-pack/plugins/ml/common/types/datavisualizer.ts new file mode 100644 index 0000000000000..af8f891771739 --- /dev/null +++ b/x-pack/plugins/ml/common/types/datavisualizer.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface AggregatableField { + fieldName: string; + stats: { + cardinality?: number; + count?: number; + sampleCount?: number; + }; + existsInDocs: boolean; +} + +export type NonAggregatableField = Omit; +export interface OverallStats { + totalCount: number; + aggregatableExistsFields: AggregatableField[]; + aggregatableNotExistsFields: NonAggregatableField[]; + nonAggregatableExistsFields: AggregatableField[]; + nonAggregatableNotExistsFields: NonAggregatableField[]; +} diff --git a/x-pack/plugins/ml/common/types/field_types.ts b/x-pack/plugins/ml/common/types/field_types.ts new file mode 100644 index 0000000000000..ae7f5ac125288 --- /dev/null +++ b/x-pack/plugins/ml/common/types/field_types.ts @@ -0,0 +1,9 @@ +/* + * 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 '../constants/field_types'; + +export type MlJobFieldType = typeof ML_JOB_FIELD_TYPES[keyof typeof ML_JOB_FIELD_TYPES]; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index 29b57671e2990..8867a8179ba82 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; -import { JobId } from './anomaly_detection_jobs/job'; +import type { + Query, + RefreshInterval, + TimeRange, +} from '../../../../../src/plugins/data/common/query'; +import type { JobId } from './anomaly_detection_jobs/job'; import { ML_PAGES } from '../constants/ml_url_generator'; -import { DataFrameAnalysisConfigType } from './data_frame_analytics'; -import { SearchQueryLanguage } from '../constants/search'; -import { ListingPageUrlState } from './common'; +import type { DataFrameAnalysisConfigType } from './data_frame_analytics'; +import type { SearchQueryLanguage } from '../constants/search'; +import type { ListingPageUrlState } from './common'; type OptionalPageState = object | undefined; @@ -38,6 +42,21 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState { [key: string]: any; } +export interface DataVisualizerIndexBasedAppState { + pageIndex: number; + pageSize: number; + sortField: string; + sortDirection: string; + searchString?: Query['query']; + searchQuery?: Query['query']; + searchQueryLanguage?: SearchQueryLanguage; + visibleFieldTypes?: string[]; + visibleFieldNames?: string[]; + samplerShardSize?: number; + showDistributions?: boolean; + showAllFields?: boolean; + showEmptyFields?: boolean; +} export type MlGenericUrlState = MLPageState< | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx index 97fac052df1f6..b176519bffd6a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx @@ -18,34 +18,41 @@ interface Props { chartData: ChartData; columnType: EuiDataGridColumn; dataTestSubj: string; + hideLabel?: boolean; + maxChartColumns?: number; } -export const ColumnChart: FC = ({ chartData, columnType, dataTestSubj }) => { - const { data, legendText, xScaleType } = useColumnChart(chartData, columnType); +const columnChartTheme = { + background: { color: 'transparent' }, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 1, + }, + chartPaddings: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + scales: { barsPadding: 0.1 }, +}; +export const ColumnChart: FC = ({ + chartData, + columnType, + dataTestSubj, + hideLabel, + maxChartColumns, +}) => { + const { data, legendText, xScaleType } = useColumnChart(chartData, columnType, maxChartColumns); return (
{!isUnsupportedChartData(chartData) && data.length > 0 && (
- + = ({ chartData, columnType, dataTestSubj }) > {legendText}
-
{columnType.id}
+ {!hideLabel &&
{columnType.id}
}
); }; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx index a3169dc14a3a8..44f7eb50137e2 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx @@ -89,13 +89,13 @@ export const isNumericChartData = (arg: any): arg is NumericChartData => { ); }; -interface OrdinalDataItem { +export interface OrdinalDataItem { key: string; key_as_string?: string; doc_count: number; } -interface OrdinalChartData { +export interface OrdinalChartData { type: 'ordinal' | 'boolean'; cardinality: number; data: OrdinalDataItem[]; @@ -120,11 +120,14 @@ export const isUnsupportedChartData = (arg: any): arg is UnsupportedChartData => return arg.hasOwnProperty('type') && arg.type === 'unsupported'; }; -type ChartDataItem = NumericDataItem | OrdinalDataItem; +export type ChartDataItem = NumericDataItem | OrdinalDataItem; export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData; type LegendText = string | JSX.Element; -const getLegendText = (chartData: ChartData): LegendText => { +export const getLegendText = ( + chartData: ChartData, + maxChartColumns = MAX_CHART_COLUMNS +): LegendText => { if (chartData.type === 'unsupported') { return i18n.translate('xpack.ml.dataGridChart.histogramNotAvailable', { defaultMessage: 'Chart not supported.', @@ -150,17 +153,17 @@ const getLegendText = (chartData: ChartData): LegendText => { ); } - if (isOrdinalChartData(chartData) && chartData.cardinality <= MAX_CHART_COLUMNS) { + if (isOrdinalChartData(chartData) && chartData.cardinality <= maxChartColumns) { return i18n.translate('xpack.ml.dataGridChart.singleCategoryLegend', { defaultMessage: `{cardinality, plural, one {# category} other {# categories}}`, values: { cardinality: chartData.cardinality }, }); } - if (isOrdinalChartData(chartData) && chartData.cardinality > MAX_CHART_COLUMNS) { + if (isOrdinalChartData(chartData) && chartData.cardinality > maxChartColumns) { return i18n.translate('xpack.ml.dataGridChart.topCategoriesLegend', { - defaultMessage: `top {MAX_CHART_COLUMNS} of {cardinality} categories`, - values: { cardinality: chartData.cardinality, MAX_CHART_COLUMNS }, + defaultMessage: `top {maxChartColumns} of {cardinality} categories`, + values: { cardinality: chartData.cardinality, maxChartColumns }, }); } @@ -182,7 +185,8 @@ interface ColumnChart { export const useColumnChart = ( chartData: ChartData, - columnType: EuiDataGridColumn + columnType: EuiDataGridColumn, + maxChartColumns?: number ): ColumnChart => { const fieldType = getFieldType(columnType.schema); @@ -244,7 +248,7 @@ export const useColumnChart = ( return { data, - legendText: getLegendText(chartData), + legendText: getLegendText(chartData, maxChartColumns), xScaleType, }; }; diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.tsx index 7f736b65d494c..3e697713d5407 100644 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.tsx +++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.tsx @@ -12,10 +12,11 @@ import { i18n } from '@kbn/i18n'; import { getMLJobTypeAriaLabel } from '../../util/field_types_utils'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; +import type { MlJobFieldType } from '../../../../common/types/field_types'; interface FieldTypeIconProps { tooltipEnabled: boolean; - type: ML_JOB_FIELD_TYPES; + type: MlJobFieldType; fieldName?: string; needsAria: boolean; } diff --git a/x-pack/plugins/ml/public/application/components/multi_select_picker/index.ts b/x-pack/plugins/ml/public/application/components/multi_select_picker/index.ts new file mode 100644 index 0000000000000..6605aaf164b30 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/multi_select_picker/index.ts @@ -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 { MultiSelectPicker, Option } from './multi_select_picker'; diff --git a/x-pack/plugins/ml/public/application/components/multi_select_picker/multi_select_picker.tsx b/x-pack/plugins/ml/public/application/components/multi_select_picker/multi_select_picker.tsx new file mode 100644 index 0000000000000..30af6b80266c6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/multi_select_picker/multi_select_picker.tsx @@ -0,0 +1,134 @@ +/* + * 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 { + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFilterSelectItem, + EuiIcon, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, +} from '@elastic/eui'; +import React, { FC, ReactNode, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface Option { + name?: string | ReactNode; + value: string; + checked?: 'on' | 'off'; +} + +const NoFilterItems = () => { + return ( +
+
+ + +

+ +

+
+
+ ); +}; + +export const MultiSelectPicker: FC<{ + options: Option[]; + onChange?: (items: string[]) => void; + title?: string; + checkedOptions: string[]; + dataTestSubj: string; +}> = ({ options, onChange, title, checkedOptions, dataTestSubj }) => { + const [items, setItems] = useState(options); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + if (searchTerm === '') { + setItems(options); + } else { + const filteredOptions = options.filter((o) => o?.value?.includes(searchTerm)); + setItems(filteredOptions); + } + }, [options, searchTerm]); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const handleOnChange = (index: number) => { + if (!items[index] || !Array.isArray(checkedOptions) || onChange === undefined) { + return; + } + const item = items[index]; + const foundIndex = checkedOptions.findIndex((fieldValue) => fieldValue === item.value); + if (foundIndex > -1) { + onChange(checkedOptions.filter((_, idx) => idx !== foundIndex)); + } else { + onChange([...checkedOptions, item.value]); + } + }; + + const button = ( + 0} + numActiveFilters={checkedOptions && checkedOptions.length} + > + {title} + + ); + + return ( + + + + setSearchTerm(e.target.value)} /> + +
+ {Array.isArray(items) && items.length > 0 ? ( + items.map((item, index) => ( + fieldValue === item.value) > -1 + ? 'on' + : undefined + } + key={index} + onClick={() => handleOnChange(index)} + style={{ flexDirection: 'row' }} + > + {item.name ?? item.value} + + )) + ) : ( + + )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts index 68774fb86fe96..da8e272103f3d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -7,6 +7,7 @@ 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'; const PAGE_SIZE_OPTIONS = [10, 25, 50]; @@ -37,7 +38,7 @@ interface UseTableSettingsReturnValue { export function useTableSettings( items: TypeOfItem[], - pageState: ListingPageUrlState, + pageState: ListingPageUrlState | DataVisualizerIndexBasedAppState, updatePageState: (update: Partial) => void ): UseTableSettingsReturnValue { const { pageIndex, pageSize, sortField, sortDirection } = pageState; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/_index.scss index 35cf29928e53b..081f8b971432e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/_index.scss @@ -1,2 +1,3 @@ @import 'file_based/index'; @import 'index_based/index'; +@import 'stats_datagrid/index'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts index bf39cbb90e8f3..4783107742799 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts @@ -4,18 +4,69 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { MlJobFieldType } from '../../../../../common/types/field_types'; + +export interface Percentile { + percent: number; + minValue: number; + maxValue: number; +} + +export interface MetricFieldVisStats { + avg?: number; + distribution?: { + percentiles: Percentile[]; + maxPercentile: number; + minPercentile: 0; + }; + max?: number; + median?: number; + min?: number; +} + +interface DocumentCountBuckets { + [key: string]: number; +} + +export interface FieldVisStats { + cardinality?: number; + count?: number; + sampleCount?: number; + trueCount?: number; + falseCount?: number; + earliest?: number; + latest?: number; + documentCounts?: { + buckets?: DocumentCountBuckets; + }; + avg?: number; + distribution?: { + percentiles: Percentile[]; + maxPercentile: number; + minPercentile: 0; + }; + fieldName?: string; + isTopValuesSampled?: boolean; + max?: number; + median?: number; + min?: number; + topValues?: Array<{ key: number; doc_count: number }>; + topValuesSampleSize?: number; + topValuesSamplerShardSize?: number; + examples?: Array; + timeRangeEarliest?: number; + timeRangeLatest?: number; +} // The internal representation of the configuration used to build the visuals // which display the field information. -// TODO - type stats export interface FieldVisConfig { - type: ML_JOB_FIELD_TYPES; + type: MlJobFieldType; fieldName?: string; existsInDocs: boolean; aggregatable: boolean; loading: boolean; - stats?: any; + stats?: FieldVisStats; fieldFormat?: any; isUnsupportedType?: boolean; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/request.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/request.ts index fd4888b8729c1..21d7d10fbf1ff 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/request.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/request.ts @@ -5,12 +5,11 @@ */ import { KBN_FIELD_TYPES } from '../../../../../../../../src/plugins/data/public'; - -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { MlJobFieldType } from '../../../../../common/types/field_types'; export interface FieldRequestConfig { fieldName?: string; - type: ML_JOB_FIELD_TYPES; + type: MlJobFieldType; cardinality: number; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/field_count_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/field_count_panel.tsx new file mode 100644 index 0000000000000..61bf244fbbcdb --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/field_count_panel.tsx @@ -0,0 +1,128 @@ +/* + * 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, EuiSwitch, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC } from 'react'; + +interface Props { + metricsStats?: { + visibleMetricFields: number; + totalMetricFields: number; + }; + fieldsCountStats?: { + visibleFieldsCount: number; + totalFieldsCount: number; + }; + showEmptyFields: boolean; + toggleShowEmptyFields: () => void; +} +export const FieldCountPanel: FC = ({ + metricsStats, + fieldsCountStats, + showEmptyFields, + toggleShowEmptyFields, +}) => { + return ( + + {fieldsCountStats && ( + + + +
+ +
+
+
+ + + + {fieldsCountStats.visibleFieldsCount} + + + + + + + +
+ )} + + {metricsStats && ( + + + +
+ +
+
+
+ + + {metricsStats.visibleMetricFields} + + + + + + + +
+ )} + + + + } + checked={showEmptyFields} + onChange={toggleShowEmptyFields} + /> + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/index.ts similarity index 81% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/index.ts index 3b933af03d982..fed4f6a61cf1d 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_count_panel/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { FieldsPanel } from './fields_panel'; +export { FieldCountPanel } from './field_count_panel'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss index 4ca62ee1f03ec..45c0937b6723a 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss @@ -57,7 +57,7 @@ } .mlFieldDataCard__codeContent { - font-family: $euiCodeFontFamily; + @include euiCodeFont; } .mlFieldDataCard__stats { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss index 4f21c29123e84..e7c155d2554ba 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss @@ -1 +1,2 @@ @import 'field_data_card'; +@import 'top_values/top_values'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx index 02209cf0a1582..7a01e81de86fd 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx @@ -5,13 +5,14 @@ */ import React, { FC } from 'react'; -import { EuiIcon, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +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) { @@ -26,79 +27,58 @@ function getPercentLabel(value: number): string { export const BooleanContent: FC = ({ config }) => { const { stats } = config; - - const { count, sampleCount, trueCount, falseCount } = stats; - const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); + if (stats === undefined) return null; + const { count, trueCount, falseCount } = stats; + if (count === undefined || trueCount === undefined || falseCount === undefined) return null; return (
-
- - -   - - -
- - - -
- - - - - - - - - + + + + + + - - -
+ }, + }} + /> + +
); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx index 61addb4689f4e..f97a8d1c3a872 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx @@ -4,64 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { FC, ReactNode } from 'react'; +import { EuiBasicTable } 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 { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place'; - +import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header'; const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS'; +interface SummaryTableItem { + function: string; + display: ReactNode; + value: number | string | undefined | null; +} export const DateContent: FC = ({ config }) => { const { stats } = config; + if (stats === undefined) return null; - const { count, sampleCount, earliest, latest } = stats; - const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); - - return ( -
-
- - -   - - -
- - + const { earliest, latest } = stats; -
+ const summaryTableTitle = i18n.translate('xpack.ml.fieldDataCard.cardDate.summaryTableTitle', { + defaultMessage: 'Summary', + }); + const summaryTableItems = [ + { + function: 'earliest', + display: ( + + ), + value: formatDate(earliest, TIME_FORMAT), + }, + { + function: 'latest', + display: ( -
+ ), + value: formatDate(latest, TIME_FORMAT), + }, + ]; + const summaryTableColumns = [ + { + name: '', + render: (summaryItem: { display: ReactNode }) => summaryItem.display, + width: '75px', + }, + { + field: 'value', + name: '', + render: (v: string) => {v}, + }, + ]; - - -
- -
-
+ return ( + <> + {summaryTableTitle} + + className={'mlDataVisualizerSummaryTable'} + data-test-subj={'mlDateSummaryTable'} + compressed + items={summaryTableItems} + columns={summaryTableColumns} + tableCaption={summaryTableTitle} + tableLayout="auto" + /> + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx index e9297796c97c1..e7b9604c1c06e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx @@ -4,52 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { FC } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { FieldDataCardProps } from '../field_data_card'; +import type { FieldDataCardProps } from '../field_data_card'; import { DocumentCountChart, DocumentCountChartPoint } from '../document_count_chart'; +import { TotalCountHeader } from '../../total_count_header'; -const CHART_WIDTH = 325; -const CHART_HEIGHT = 350; +export interface Props extends FieldDataCardProps { + totalCount: number; +} -export const DocumentCountContent: FC = ({ config }) => { +export const DocumentCountContent: FC = ({ config, totalCount }) => { const { stats } = config; + if (stats === undefined) return null; const { documentCounts, timeRangeEarliest, timeRangeLatest } = stats; + if ( + documentCounts === undefined || + timeRangeEarliest === undefined || + timeRangeLatest === undefined + ) + return null; let chartPoints: DocumentCountChartPoint[] = []; - if (documentCounts !== undefined && documentCounts.buckets !== undefined) { - const buckets: Record = stats.documentCounts.buckets; + if (documentCounts.buckets !== undefined) { + const buckets: Record = documentCounts?.buckets; chartPoints = Object.entries(buckets).map(([time, value]) => ({ time: +time, value })); } return ( - - - - - - - - - - - - - - - + <> + + + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx index 57e0d2831035a..0c10cf1e6adcf 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx @@ -5,12 +5,8 @@ */ import React, { FC } 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 GeoPointContent: FC = ({ config }) => { @@ -35,46 +31,11 @@ export const GeoPointContent: FC = ({ config }) => { // } const { stats } = config; - - const { count, sampleCount, cardinality, examples } = stats; - const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); + if (stats?.examples === undefined) return null; return (
-
- - -   - - -
- - - -
- - -   - - -
- - - - +
); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx index eff20520c46e9..4b54e86cdc495 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx @@ -5,65 +5,30 @@ */ import React, { FC } from 'react'; -import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; -// @ts-ignore -import { formatDate } from '@elastic/eui/lib/services/format'; +import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FieldDataCardProps } from '../field_data_card'; -import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place'; import { TopValues } from '../top_values'; +import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header'; export const IpContent: FC = ({ config }) => { const { stats, fieldFormat } = config; - + if (stats === undefined) return null; const { count, sampleCount, cardinality } = stats; - const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); + if (count === undefined || sampleCount === undefined || cardinality === undefined) return null; return (
-
- - -   - - -
- - - -
- - -   - - -
- - - -
+ - - -
+ + +
); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx index 7aa1683ec4066..18c4fb190a125 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx @@ -5,69 +5,25 @@ */ import React, { FC } from 'react'; -import { EuiIcon, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -// @ts-ignore -import { formatDate } from '@elastic/eui/lib/services/format'; - +import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - import { FieldDataCardProps } from '../field_data_card'; -import { roundToDecimalPlace } from '../../../../../formatters/round_to_decimal_place'; import { TopValues } from '../top_values'; +import { ExpandedRowFieldHeader } from '../../../../stats_datagrid/components/expanded_row_field_header'; export const KeywordContent: FC = ({ config }) => { const { stats, fieldFormat } = config; - const { count, sampleCount, cardinality } = stats; - const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); - return (
-
- - -   - - -
- + + + - -
- - -   - - -
- - - -
- - - - - - - -
+
); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx index d7bf527d104cc..782880105da20 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx @@ -47,19 +47,20 @@ export const NumberContent: FC = ({ config }) => { const chartData = buildChartDataFromStats(stats, METRIC_DISTRIBUTION_CHART_WIDTH); setDistributionChartData(chartData); }, []); - - const { count, sampleCount, cardinality, min, median, max, distribution } = stats; - const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); - const [detailsMode, setDetailsMode] = useState( - cardinality <= DEFAULT_TOP_VALUES_THRESHOLD + 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, @@ -176,7 +177,7 @@ export const NumberContent: FC = ({ config }) => { - + = ({ config }) => { {detailsMode === DETAILS_MODE.TOP_VALUES && ( - + )} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx index a7b48325b9651..065d7d40c23e9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx @@ -15,8 +15,17 @@ import { ExamplesList } from '../examples_list'; export const OtherContent: FC = ({ 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 ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx index e0e3ea3ff4888..d54d2237c6603 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx @@ -15,8 +15,11 @@ import { ExamplesList } from '../examples_list'; export const TextContent: FC = ({ config }) => { const { stats } = config; + if (stats === undefined) return null; const { examples } = stats; + if (examples === undefined) return null; + const numExamples = examples.length; return ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx index 7e2671884b101..6023d32767039 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx @@ -18,19 +18,14 @@ import { Settings, } from '@elastic/charts'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; - -import { useUiSettings } from '../../../../../contexts/kibana/use_ui_settings_context'; - export interface DocumentCountChartPoint { time: number | string; value: number; } interface Props { - width: number; - height: number; + width?: number; + height?: number; chartPoints: DocumentCountChartPoint[]; timeRangeEarliest: number; timeRangeLatest: number; @@ -56,21 +51,15 @@ export const DocumentCountChart: FC = ({ const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); - const IS_DARK_THEME = useUiSettings().get('theme:darkMode'); - const themeName = IS_DARK_THEME ? darkTheme : lightTheme; - const EVENT_RATE_COLOR = themeName.euiColorVis2; - return ( -
- - +
+ + ; } export const ExamplesList: FC = ({ examples }) => { - if (examples === undefined || examples === null || examples.length === 0) { + if ( + examples === undefined || + examples === null || + !Array.isArray(examples) || + examples.length === 0 + ) { return null; } const examplesContent = examples.map((example, i) => { return ( @@ -32,19 +37,17 @@ export const ExamplesList: FC = ({ examples }) => { return (
- - - - - + + + - + {examplesContent}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx index 5db964fb91f47..a568356a06d26 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx @@ -14,7 +14,6 @@ import { FieldTitleBar } from '../../../../components/field_title_bar/index'; import { BooleanContent, DateContent, - DocumentCountContent, GeoPointContent, IpContent, KeywordContent, @@ -42,7 +41,7 @@ export const FieldDataCard: FC = ({ config }) => { if (fieldName !== undefined) { return ; } else { - return ; + return null; } case ML_JOB_FIELD_TYPES.BOOLEAN: diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx index 9ff6e99dbc5c2..012209867f18e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx @@ -25,7 +25,7 @@ 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 { ChartTooltipValue } from '../../../../../components/chart_tooltip/chart_tooltip_service'; +import type { ChartTooltipValue } from '../../../../../components/chart_tooltip/chart_tooltip_service'; export interface MetricDistributionChartData { x: number; @@ -40,11 +40,18 @@ interface Props { height: number; chartData: MetricDistributionChartData[]; fieldFormat?: any; // Kibana formatter for field being viewed + hideXAxis?: boolean; } const SPEC_ID = 'metric_distribution'; -export const MetricDistributionChart: FC = ({ width, height, chartData, fieldFormat }) => { +export const MetricDistributionChart: FC = ({ + width, + height, + chartData, + fieldFormat, + hideXAxis, +}) => { // This value is shown to label the y axis values in the tooltip. // Ideally we wouldn't show these values at all in the tooltip, // but this is not yet possible with Elastic charts. @@ -54,7 +61,7 @@ export const MetricDistributionChart: FC = ({ width, height, chartData, f const IS_DARK_THEME = useUiSettings().get('theme:darkMode'); const themeName = IS_DARK_THEME ? darkTheme : lightTheme; - const AREA_SERIES_COLOR = themeName.euiColorVis1; + const AREA_SERIES_COLOR = themeName.euiColorVis0; const headerFormatter: TooltipValueFormatter = (tooltipData: ChartTooltipValue) => { const xValue = tooltipData.value; @@ -72,10 +79,31 @@ export const MetricDistributionChart: FC = ({ width, height, chartData, f }; return ( -
- +
+ = ({ width, height, chartData, f id="bottom" position={Position.Bottom} tickFormat={(d) => kibanaFieldFormat(d, fieldFormat)} + hide={hideXAxis === true} /> d.toFixed(3)} hide={true} /> = ({ stats, fieldFormat, barColor }) => { +export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed }) => { const { topValues, topValuesSampleSize, @@ -43,32 +45,42 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor }) => { isTopValuesSampled, } = stats; const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count; - return (
- {topValues.map((value: any) => ( - - - - - {kibanaFieldFormat(value.key, fieldFormat)} + {Array.isArray(topValues) && + topValues.map((value: any) => ( + + + + + {kibanaFieldFormat(value.key, fieldFormat)} + + + + + + + + + {getPercentLabel(value.doc_count, progressBarMax)} - - - - - - - - {getPercentLabel(value.doc_count, progressBarMax)} - - - - ))} + + + ))} {isTopValuesSampled === true && ( - + = ({ - fieldTypes, - selectedFieldType, - setSelectedFieldType, -}) => { - const options = [ - { - value: '*', - text: i18n.translate('xpack.ml.datavisualizer.fieldTypesSelect.allFieldsTypeOptionLabel', { - defaultMessage: 'All field types', - }), - }, - ]; - fieldTypes.forEach((fieldType) => { - options.push({ - value: fieldType, - text: i18n.translate('xpack.ml.datavisualizer.fieldTypesSelect.typeOptionLabel', { - defaultMessage: '{fieldType} types', - values: { - fieldType, - }, - }), - }); - }); - - return ( - setSelectedFieldType(e.target.value as ML_JOB_FIELD_TYPES | '*')} - aria-label={i18n.translate('xpack.ml.datavisualizer.fieldTypesSelect.selectAriaLabel', { - defaultMessage: 'Select field types to display', - })} - data-test-subj="mlDataVisualizerFieldTypesSelect" - /> - ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx deleted file mode 100644 index 28ba1139f7460..0000000000000 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx +++ /dev/null @@ -1,188 +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 { - EuiBadge, - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - // @ts-ignore - EuiSearchBar, - EuiSpacer, - EuiSwitch, - EuiText, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { useMlKibana } from '../../../../contexts/kibana'; -import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; -import { FieldDataCard } from '../field_data_card'; -import { FieldTypesSelect } from '../field_types_select'; -import { FieldVisConfig } from '../../common'; - -interface Props { - title: string; - totalFieldCount: number; - populatedFieldCount: number; - showAllFields: boolean; - setShowAllFields(b: boolean): void; - fieldTypes: ML_JOB_FIELD_TYPES[]; - showFieldType: ML_JOB_FIELD_TYPES | '*'; - setShowFieldType?(t: ML_JOB_FIELD_TYPES | '*'): void; - fieldSearchBarQuery?: string; - setFieldSearchBarQuery(s: string): void; - fieldVisConfigs: FieldVisConfig[]; -} - -interface SearchBarQuery { - queryText: string; - error?: { message: string } | null; -} - -export const FieldsPanel: FC = ({ - title, - totalFieldCount, - populatedFieldCount, - showAllFields, - setShowAllFields, - fieldTypes, - showFieldType, - setShowFieldType, - fieldSearchBarQuery, - setFieldSearchBarQuery, - fieldVisConfigs, -}) => { - const { - services: { notifications }, - } = useMlKibana(); - function onShowAllFieldsChange() { - setShowAllFields(!showAllFields); - } - - function onSearchBarChange(query: SearchBarQuery) { - if (query.error) { - const { toasts } = notifications; - toasts.addWarning( - i18n.translate('xpack.ml.datavisualizer.fieldsPanel.searchBarError', { - defaultMessage: `An error occurred running the search. {message}.`, - values: { message: query.error.message }, - }) - ); - } else { - setFieldSearchBarQuery(query.queryText); - } - } - - return ( -
- - - - - -

{title}

-
-
- - - } - > - - {populatedFieldCount} - - - - - - {totalFieldCount}, - }} - /> - - - {populatedFieldCount < totalFieldCount && ( - - - - )} -
-
- - - - - - - {typeof setShowFieldType === 'function' && ( - - - - )} - - -
- - - {fieldVisConfigs - .filter(({ stats }) => stats !== undefined) - .map((visConfig, i) => ( - - - - ))} - -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/field_name_filter.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/field_name_filter.tsx new file mode 100644 index 0000000000000..77514c20adecd --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/field_name_filter.tsx @@ -0,0 +1,61 @@ +/* + * 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 { Option, MultiSelectPicker } from '../../../../components/multi_select_picker'; +import type { OverallStats } from '../../../../../../common/types/datavisualizer'; + +interface Props { + overallStats: OverallStats; + setVisibleFieldNames(q: string[]): void; + visibleFieldNames: string[]; + showEmptyFields: boolean; +} + +export const DataVisualizerFieldNamesFilter: FC = ({ + overallStats, + setVisibleFieldNames, + visibleFieldNames, + showEmptyFields, +}) => { + const items: Option[] = useMemo(() => { + const options: Option[] = []; + if (overallStats) { + Object.keys(overallStats).forEach((key) => { + const fieldsGroup = overallStats[key as keyof OverallStats]; + if (Array.isArray(fieldsGroup) && fieldsGroup.length > 0) { + fieldsGroup.forEach((field) => { + if ( + (field.existsInDocs === true || showEmptyFields === true) && + field.fieldName !== undefined + ) { + options.push({ value: field.fieldName }); + } + }); + } + }); + } + return options; + }, [overallStats]); + + const fieldNameTitle = useMemo( + () => + i18n.translate('xpack.ml.dataVisualizer.indexBased.fieldNameSelect', { + defaultMessage: 'Field name', + }), + [] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/field_type_filter.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/field_type_filter.tsx new file mode 100644 index 0000000000000..984a9116c1b63 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/field_type_filter.tsx @@ -0,0 +1,71 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Option, MultiSelectPicker } from '../../../../components/multi_select_picker'; +import { FieldTypeIcon } from '../../../../components/field_type_icon'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import type { MlJobFieldType } from '../../../../../../common/types/field_types'; + +export const ML_JOB_FIELD_TYPES_OPTIONS = { + [ML_JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' }, + [ML_JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' }, + [ML_JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' }, + [ML_JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' }, + [ML_JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' }, + [ML_JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' }, + [ML_JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' }, + [ML_JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' }, +}; + +export const DatavisualizerFieldTypeFilter: FC<{ + indexedFieldTypes: MlJobFieldType[]; + setVisibleFieldTypes(q: string[]): void; + visibleFieldTypes: string[]; +}> = ({ indexedFieldTypes, setVisibleFieldTypes, visibleFieldTypes }) => { + const options: Option[] = useMemo(() => { + return indexedFieldTypes.map((indexedFieldName) => { + const item = ML_JOB_FIELD_TYPES_OPTIONS[indexedFieldName]; + + return { + value: indexedFieldName, + name: ( + + {item.name} + {indexedFieldName && ( + + + + )} + + ), + }; + }); + }, [indexedFieldTypes]); + const fieldTypeTitle = useMemo( + () => + i18n.translate('xpack.ml.dataVisualizer.indexBased.fieldTypeSelect', { + defaultMessage: 'Field type', + }), + [] + ); + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx index 073945ee2e766..af3c1a0e7c16c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx @@ -6,17 +6,8 @@ import React, { FC, useState } from 'react'; -import { - EuiCode, - EuiFlexItem, - EuiFlexGroup, - EuiIconTip, - EuiInputPopover, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; +import { EuiCode, EuiFlexItem, EuiFlexGroup, EuiInputPopover } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; @@ -33,54 +24,50 @@ import { Query, QueryStringInput, } from '../../../../../../../../../src/plugins/data/public'; +import { ShardSizeFilter } from './shard_size_select'; +import { DataVisualizerFieldNamesFilter } from './field_name_filter'; +import { DatavisualizerFieldTypeFilter } from './field_type_filter'; +import { MlJobFieldType } from '../../../../../../common/types/field_types'; interface Props { indexPattern: IndexPattern; searchString: Query['query']; - setSearchString(s: Query['query']): void; searchQuery: Query['query']; - setSearchQuery(q: Query['query']): void; searchQueryLanguage: SearchQueryLanguage; - setSearchQueryLanguage(q: any): void; samplerShardSize: number; setSamplerShardSize(s: number): void; - totalCount: number; + overallStats: any; + indexedFieldTypes: MlJobFieldType[]; + setVisibleFieldTypes(q: string[]): void; + visibleFieldTypes: string[]; + setVisibleFieldNames(q: string[]): void; + visibleFieldNames: string[]; + setSearchParams({ + searchQuery, + searchString, + queryLanguage, + }: { + searchQuery: Query['query']; + searchString: Query['query']; + queryLanguage: SearchQueryLanguage; + }): void; + showEmptyFields: boolean; } -const searchSizeOptions = [1000, 5000, 10000, 100000, -1].map((v) => { - return { - value: String(v), - inputDisplay: - v > 0 ? ( - - {v} }} - /> - - ) : ( - - - - ), - }; -}); - export const SearchPanel: FC = ({ indexPattern, searchString, - setSearchString, - searchQuery, - setSearchQuery, searchQueryLanguage, - setSearchQueryLanguage, samplerShardSize, setSamplerShardSize, - totalCount, + overallStats, + indexedFieldTypes, + setVisibleFieldTypes, + visibleFieldTypes, + setVisibleFieldNames, + visibleFieldNames, + setSearchParams, + showEmptyFields, }) => { // The internal state of the input query bar updated on every key stroke. const [searchInput, setSearchInput] = useState({ @@ -102,10 +89,11 @@ export const SearchPanel: FC = ({ } else { filterQuery = {}; } - - setSearchQuery(filterQuery); - setSearchString(query.query); - setSearchQueryLanguage(query.language); + setSearchParams({ + searchQuery: filterQuery, + searchString: query.query, + queryLanguage: query.language as SearchQueryLanguage, + }); } catch (e) { console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console setErrorMessage({ query: query.query as string, message: e.message }); @@ -133,7 +121,7 @@ export const SearchPanel: FC = ({ } )} disableAutoFocus={true} - dataTestSubj="transformQueryInput" + dataTestSubj="mlDataVisualizerQueryInput" languageSwitcherPopoverAnchorPosition="rightDown" /> } @@ -153,51 +141,23 @@ export const SearchPanel: FC = ({ - - - setSamplerShardSize(+value)} - aria-label={i18n.translate( - 'xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel', - { - defaultMessage: 'Select number of documents to sample', - } - )} - data-test-subj="mlDataVisualizerShardSizeSelect" - /> - - - - - - - - - - - - ), - }} - /> - + + + + ); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/shard_size_select.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/shard_size_select.tsx new file mode 100644 index 0000000000000..c8aa737f71d6e --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/shard_size_select.tsx @@ -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 { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + samplerShardSize: number; + setSamplerShardSize(s: number): void; +} + +const searchSizeOptions = [1000, 5000, 10000, 100000, -1].map((v) => { + return { + value: String(v), + inputDisplay: + v > 0 ? ( + + {v} }} + /> + + ) : ( + + + + ), + }; +}); + +export const ShardSizeFilter: FC = ({ samplerShardSize, setSamplerShardSize }) => { + return ( + + + setSamplerShardSize(+value)} + aria-label={i18n.translate('xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel', { + defaultMessage: 'Select number of documents to sample', + })} + data-test-subj="mlDataVisualizerShardSizeSelect" + /> + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/total_count_header/index.ts similarity index 81% rename from x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/index.ts rename to x-pack/plugins/ml/public/application/datavisualizer/index_based/components/total_count_header/index.ts index 212299caf14ab..dee3920b48e01 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/total_count_header/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { FieldTypesSelect } from './field_types_select'; +export { TotalCountHeader } from './total_count_header'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/total_count_header/total_count_header.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/total_count_header/total_count_header.tsx new file mode 100644 index 0000000000000..64861ff71fef8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/total_count_header/total_count_header.tsx @@ -0,0 +1,33 @@ +/* + * 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 { EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + +export const TotalCountHeader = ({ totalCount }: { totalCount: number }) => { + return ( + + + + + + ), + }} + /> + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 2248bf01ebf8a..cec84e8fb455e 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -6,12 +6,9 @@ import React, { FC, Fragment, useEffect, useMemo, useState } from 'react'; import { merge } from 'rxjs'; -import { i18n } from '@kbn/i18n'; - import { EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiPage, EuiPageBody, EuiPageContentBody, @@ -24,10 +21,10 @@ import { import { IFieldType, KBN_FIELD_TYPES, - Query, esQuery, esKuery, UI_SETTINGS, + Query, } from '../../../../../../../src/plugins/data/public'; import { SavedSearchSavedObject } from '../../../../common/types/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; @@ -45,30 +42,28 @@ import { useTimefilter } from '../../contexts/kibana'; import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; import { getTimeBucketsFromCache } from '../../util/time_buckets'; import { getToastNotifications } from '../../util/dependency_cache'; -import { useUrlState } from '../../util/url_state'; -import { FieldRequestConfig, FieldVisConfig } from './common'; +import { usePageUrlState, useUrlState } from '../../util/url_state'; import { ActionsPanel } from './components/actions_panel'; -import { FieldsPanel } from './components/fields_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 { 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 { DataVisualizerIndexBasedAppState } from '../../../../common/types/ml_url_generator'; +import type { OverallStats } from '../../../../common/types/datavisualizer'; +import { MlJobFieldType } from '../../../../common/types/field_types'; interface DataVisualizerPageState { - searchQuery: Query['query']; - searchString: Query['query']; - searchQueryLanguage: SearchQueryLanguage; - samplerShardSize: number; - overallStats: any; + overallStats: OverallStats; metricConfigs: FieldVisConfig[]; totalMetricFieldCount: number; populatedMetricFieldCount: number; - showAllMetrics: boolean; - metricFieldQuery?: string; + metricsLoaded: boolean; nonMetricConfigs: FieldVisConfig[]; - totalNonMetricFieldCount: number; - populatedNonMetricFieldCount: number; - showAllNonMetrics: boolean; - nonMetricShowFieldType: ML_JOB_FIELD_TYPES | '*'; - nonMetricFieldQuery?: string; + nonMetricsLoaded: boolean; + documentCountStats?: FieldVisConfig; } const defaultSearchQuery = { @@ -77,10 +72,6 @@ const defaultSearchQuery = { function getDefaultPageState(): DataVisualizerPageState { return { - searchString: '', - searchQuery: defaultSearchQuery, - searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, - samplerShardSize: 5000, overallStats: { totalCount: 0, aggregatableExistsFields: [], @@ -91,17 +82,35 @@ function getDefaultPageState(): DataVisualizerPageState { metricConfigs: [], totalMetricFieldCount: 0, populatedMetricFieldCount: 0, - showAllMetrics: false, + metricsLoaded: false, nonMetricConfigs: [], - totalNonMetricFieldCount: 0, - populatedNonMetricFieldCount: 0, - showAllNonMetrics: false, - nonMetricShowFieldType: '*', + nonMetricsLoaded: false, + documentCountStats: undefined, }; } +export const getDefaultDataVisualizerListState = (): Required => ({ + pageIndex: 0, + pageSize: 10, + sortField: 'fieldName', + sortDirection: 'asc', + visibleFieldTypes: [], + visibleFieldNames: [], + samplerShardSize: 5000, + searchString: '', + searchQuery: defaultSearchQuery, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + showDistributions: true, + showAllFields: false, + showEmptyFields: false, +}); export const Page: FC = () => { const mlContext = useMlContext(); + const restorableDefaults = getDefaultDataVisualizerListState(); + const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState( + ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + restorableDefaults + ); const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = mlContext; const timefilter = useTimefilter({ @@ -135,16 +144,12 @@ export const Page: FC = () => { }, []); // Obtain the list of non metric field types which appear in the index pattern. - let indexedFieldTypes: ML_JOB_FIELD_TYPES[] = []; + let indexedFieldTypes: MlJobFieldType[] = []; const indexPatternFields: IFieldType[] = currentIndexPattern.fields; indexPatternFields.forEach((field) => { if (field.scripted !== true) { - const dataVisualizerType: ML_JOB_FIELD_TYPES | undefined = kbnTypeToMLJobType(field); - if ( - dataVisualizerType !== undefined && - !indexedFieldTypes.includes(dataVisualizerType) && - dataVisualizerType !== ML_JOB_FIELD_TYPES.NUMBER - ) { + const dataVisualizerType: MlJobFieldType | undefined = kbnTypeToMLJobType(field); + if (dataVisualizerType !== undefined && !indexedFieldTypes.includes(dataVisualizerType)) { indexedFieldTypes.push(dataVisualizerType); } } @@ -159,46 +164,74 @@ export const Page: FC = () => { mlNodesAvailable() && currentIndexPattern.timeFieldName !== undefined; - const { - searchQuery: initSearchQuery, - searchString: initSearchString, - queryLanguage: initQueryLanguage, - } = extractSearchData(currentSavedSearch); + const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { + const searchData = extractSearchData(currentSavedSearch); + if (searchData === undefined || dataVisualizerListState.searchString !== '') { + return { + searchQuery: dataVisualizerListState.searchQuery, + searchString: dataVisualizerListState.searchString, + searchQueryLanguage: dataVisualizerListState.searchQueryLanguage, + }; + } else { + return { + searchQuery: searchData.searchQuery, + searchString: searchData.searchString, + searchQueryLanguage: searchData.queryLanguage, + }; + } + }, [currentSavedSearch, dataVisualizerListState]); + + const setSearchParams = (searchParams: { + searchQuery: Query['query']; + searchString: Query['query']; + queryLanguage: SearchQueryLanguage; + }) => { + setDataVisualizerListState({ + ...dataVisualizerListState, + searchQuery: searchParams.searchQuery, + searchString: searchParams.searchString, + searchQueryLanguage: searchParams.queryLanguage, + }); + }; + + const samplerShardSize = + dataVisualizerListState.samplerShardSize ?? restorableDefaults.samplerShardSize; + const setSamplerShardSize = (value: number) => { + setDataVisualizerListState({ ...dataVisualizerListState, samplerShardSize: value }); + }; - const [searchString, setSearchString] = useState(initSearchString); - const [searchQuery, setSearchQuery] = useState(initSearchQuery); - const [searchQueryLanguage, setSearchQueryLanguage] = useState( - initQueryLanguage - ); - const [samplerShardSize, setSamplerShardSize] = useState(defaults.samplerShardSize); + 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 showEmptyFields = + dataVisualizerListState.showEmptyFields ?? restorableDefaults.showEmptyFields; + const toggleShowEmptyFields = () => { + setDataVisualizerListState({ + ...dataVisualizerListState, + showEmptyFields: !dataVisualizerListState.showEmptyFields, + }); + }; - // TODO - type overallStats and stats const [overallStats, setOverallStats] = useState(defaults.overallStats); + const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats); const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); - const [totalMetricFieldCount, setTotalMetricFieldCount] = useState( - defaults.totalMetricFieldCount - ); - const [populatedMetricFieldCount, setPopulatedMetricFieldCount] = useState( - defaults.populatedMetricFieldCount - ); - const [showAllMetrics, setShowAllMetrics] = useState(defaults.showAllMetrics); - const [metricFieldQuery, setMetricFieldQuery] = useState(defaults.metricFieldQuery); + const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded); + const [metricsStats, setMetricsStats] = useState< + undefined | { visibleMetricFields: number; totalMetricFields: number } + >(); const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); - const [totalNonMetricFieldCount, setTotalNonMetricFieldCount] = useState( - defaults.totalNonMetricFieldCount - ); - const [populatedNonMetricFieldCount, setPopulatedNonMetricFieldCount] = useState( - defaults.populatedNonMetricFieldCount - ); - const [showAllNonMetrics, setShowAllNonMetrics] = useState(defaults.showAllNonMetrics); - - const [nonMetricShowFieldType, setNonMetricShowFieldType] = useState( - defaults.nonMetricShowFieldType - ); - - const [nonMetricFieldQuery, setNonMetricFieldQuery] = useState(defaults.nonMetricFieldQuery); + const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); useEffect(() => { const timeUpdateSubscription = merge( @@ -223,7 +256,7 @@ export const Page: FC = () => { useEffect(() => { createMetricCards(); createNonMetricCards(); - }, [overallStats]); + }, [overallStats, showEmptyFields]); useEffect(() => { loadMetricFieldStats(); @@ -235,22 +268,18 @@ export const Page: FC = () => { useEffect(() => { createMetricCards(); - }, [showAllMetrics, metricFieldQuery]); + }, [metricsLoaded]); useEffect(() => { createNonMetricCards(); - }, [showAllNonMetrics, nonMetricShowFieldType, nonMetricFieldQuery]); + }, [nonMetricsLoaded]); /** * Extract query data from the saved search object. */ function extractSearchData(savedSearch: SavedSearchSavedObject | null) { if (!savedSearch) { - return { - searchQuery: defaults.searchQuery, - searchString: defaults.searchString, - queryLanguage: defaults.searchQueryLanguage, - }; + return undefined; } const { query } = getQueryFromSavedSearch(savedSearch); @@ -364,18 +393,21 @@ export const Page: FC = () => { (fieldStats: any) => fieldStats.fieldName === config.fieldName ), }; + configWithStats.loading = false; + configs.push(configWithStats); } else { // Document count card. configWithStats.stats = metricFieldStats.find( (fieldStats: any) => fieldStats.fieldName === undefined ); - // Add earliest / latest of timefilter for setting x axis domain. - configWithStats.stats.timeRangeEarliest = earliest; - configWithStats.stats.timeRangeLatest = latest; + if (configWithStats.stats !== undefined) { + // Add earliest / latest of timefilter for setting x axis domain. + configWithStats.stats.timeRangeEarliest = earliest; + configWithStats.stats.timeRangeLatest = latest; + } + setDocumentCountStats(configWithStats); } - configWithStats.loading = false; - configs.push(configWithStats); }); setMetricConfigs(configs); @@ -450,21 +482,13 @@ export const Page: FC = () => { const configs: FieldVisConfig[] = []; const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - let allMetricFields = indexPatternFields.filter((f) => { + const allMetricFields = indexPatternFields.filter((f) => { return ( f.type === KBN_FIELD_TYPES.NUMBER && f.displayName !== undefined && dataLoader.isDisplayField(f.displayName) === true ); }); - if (metricFieldQuery !== undefined) { - const metricFieldRegexp = new RegExp(`(${metricFieldQuery})`, 'gi'); - allMetricFields = allMetricFields.filter((f) => { - const addField = f.displayName !== undefined && !!f.displayName.match(metricFieldRegexp); - return addField; - }); - } - const metricExistsFields = allMetricFields.filter((f) => { return aggregatableExistsFields.find((existsF) => { return existsF.fieldName === f.displayName; @@ -472,8 +496,6 @@ export const Page: FC = () => { }); // Add a config for 'document count', identified by no field name if indexpattern is time based. - let allFieldCount = allMetricFields.length; - let popFieldCount = metricExistsFields.length; if (currentIndexPattern.timeFieldName !== undefined) { configs.push({ type: ML_JOB_FIELD_TYPES.NUMBER, @@ -481,91 +503,52 @@ export const Page: FC = () => { loading: true, aggregatable: true, }); - allFieldCount++; - popFieldCount++; } - // Add on 1 for the document count card. - setTotalMetricFieldCount(allFieldCount); - setPopulatedMetricFieldCount(popFieldCount); - - if (allMetricFields.length === metricExistsFields.length && showAllMetrics === false) { - setShowAllMetrics(true); + if (metricsLoaded === false) { + setMetricsLoaded(true); return; } let aggregatableFields: any[] = overallStats.aggregatableExistsFields; - if (allMetricFields.length !== metricExistsFields.length && showAllMetrics === true) { + if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) { aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); } - const metricFieldsToShow = showAllMetrics === true ? allMetricFields : metricExistsFields; + const metricFieldsToShow = + metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields; metricFieldsToShow.forEach((field) => { const fieldData = aggregatableFields.find((f) => { return f.fieldName === field.displayName; }); - if (fieldData !== undefined) { - const metricConfig: FieldVisConfig = { - ...fieldData, - fieldFormat: currentIndexPattern.getFormatterForField(field), - type: ML_JOB_FIELD_TYPES.NUMBER, - loading: true, - aggregatable: true, - }; + const metricConfig: FieldVisConfig = { + ...(fieldData ? fieldData : {}), + fieldFormat: currentIndexPattern.getFormatterForField(field), + type: ML_JOB_FIELD_TYPES.NUMBER, + loading: true, + aggregatable: true, + }; - configs.push(metricConfig); - } + configs.push(metricConfig); }); + setMetricsStats({ + totalMetricFields: allMetricFields.length, + visibleMetricFields: metricFieldsToShow.length, + }); setMetricConfigs(configs); } function createNonMetricCards() { - let allNonMetricFields = []; - if (nonMetricShowFieldType === '*') { - allNonMetricFields = indexPatternFields.filter((f) => { - return ( - f.type !== KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - } else { - if ( - nonMetricShowFieldType === ML_JOB_FIELD_TYPES.TEXT || - nonMetricShowFieldType === ML_JOB_FIELD_TYPES.KEYWORD - ) { - const aggregatableCheck = - nonMetricShowFieldType === ML_JOB_FIELD_TYPES.KEYWORD ? true : false; - allNonMetricFields = indexPatternFields.filter((f) => { - return ( - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true && - f.type === KBN_FIELD_TYPES.STRING && - f.aggregatable === aggregatableCheck - ); - }); - } else { - allNonMetricFields = indexPatternFields.filter((f) => { - return ( - f.type === nonMetricShowFieldType && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - } - } - - // If a field filter has been entered, perform another filter on the entered regexp. - if (nonMetricFieldQuery !== undefined) { - const nonMetricFieldRegexp = new RegExp(`(${nonMetricFieldQuery})`, 'gi'); - allNonMetricFields = allNonMetricFields.filter( - (f) => f.displayName !== undefined && f.displayName.match(nonMetricFieldRegexp) + const allNonMetricFields = indexPatternFields.filter((f) => { + return ( + f.type !== KBN_FIELD_TYPES.NUMBER && + f.displayName !== undefined && + dataLoader.isDisplayField(f.displayName) === true ); - } - + }); // Obtain the list of all non-metric fields which appear in documents // (aggregatable or not aggregatable). const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields. @@ -593,15 +576,12 @@ export const Page: FC = () => { } }); - setTotalNonMetricFieldCount(allNonMetricFields.length); - setPopulatedNonMetricFieldCount(nonMetricFieldData.length); - - if (allNonMetricFields.length === nonMetricFieldData.length && showAllNonMetrics === false) { - setShowAllNonMetrics(true); + if (nonMetricsLoaded === false) { + setNonMetricsLoaded(true); return; } - if (allNonMetricFields.length !== nonMetricFieldData.length && showAllNonMetrics === true) { + if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) { // Combine the field data obtained from Elasticsearch into a single array. nonMetricFieldData = nonMetricFieldData.concat( overallStats.aggregatableNotExistsFields, @@ -609,8 +589,7 @@ export const Page: FC = () => { ); } - const nonMetricFieldsToShow = - showAllNonMetrics === true ? allNonMetricFields : populatedNonMetricFields; + const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields; const configs: FieldVisConfig[] = []; @@ -645,6 +624,42 @@ export const Page: FC = () => { const wizardPanelWidth = '280px'; + const configs = useMemo(() => { + let combinedConfigs = [...nonMetricConfigs, ...metricConfigs]; + if (visibleFieldTypes && visibleFieldTypes.length > 0) { + combinedConfigs = combinedConfigs.filter( + (config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1 + ); + } + if (visibleFieldNames && visibleFieldNames.length > 0) { + combinedConfigs = combinedConfigs.filter( + (config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1 + ); + } + + return combinedConfigs; + }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]); + + const fieldsCountStats = useMemo(() => { + let _visibleFieldsCount = 0; + let _totalFieldsCount = 0; + Object.keys(overallStats).forEach((key) => { + const fieldsGroup = overallStats[key as keyof OverallStats]; + if (Array.isArray(fieldsGroup) && fieldsGroup.length > 0) { + _totalFieldsCount += fieldsGroup.length; + } + }); + + if (showEmptyFields === true) { + _visibleFieldsCount = _totalFieldsCount; + } else { + _visibleFieldsCount = + overallStats.aggregatableExistsFields.length + + overallStats.nonAggregatableExistsFields.length; + } + return { visibleFieldsCount: _visibleFieldsCount, totalFieldsCount: _totalFieldsCount }; + }, [overallStats, showEmptyFields]); + return ( @@ -682,60 +697,45 @@ export const Page: FC = () => { + {documentCountStats && overallStats?.totalCount !== undefined && ( + + + + )} + + + + + + - - - - {totalMetricFieldCount > 0 && ( - - - - - )} - - - {showActionsPanel === true && ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/_index.scss b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/_index.scss new file mode 100644 index 0000000000000..1e8b19f79fa9a --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/_index.scss @@ -0,0 +1,37 @@ +.mlDataVisualizerFieldExpandedRow { + padding-left: $euiSize * 4; + width: 100%; + + .mlFieldDataCard__valuesTitle { + text-transform: uppercase; + text-align: left; + color: $euiColorDarkShade; + font-weight: bold; + } + .mlFieldDataCard__codeContent { + @include euiCodeFont; + } +} + +.mlDataVisualizer { + .euiTableRow > .euiTableRowCell { + border-bottom: 0px; + border-top: 1px solid $euiColorLightShade; + + } + .euiTableRow-isExpandedRow { + + .euiTableRowCell{ + background-color: transparent !important; + border-top: 0px; + border-bottom: 1px solid $euiColorLightShade; + + } + } + .mlDataVisualizerSummaryTable { + .euiTableRow > .euiTableRowCell{ + border-bottom: 0px; + } + } +} + diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/expanded_row_field_header.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/expanded_row_field_header.tsx new file mode 100644 index 0000000000000..46667ae66cd7d --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/expanded_row_field_header.tsx @@ -0,0 +1,14 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import React from 'react'; + +export const ExpandedRowFieldHeader = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/index.ts new file mode 100644 index 0000000000000..060a0a93554e9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/expanded_row_field_header/index.ts @@ -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 { ExpandedRowFieldHeader } from './expanded_row_field_header'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/number_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/number_content.tsx new file mode 100644 index 0000000000000..f3ea3ba1b7ac8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_expanded_row/number_content.tsx @@ -0,0 +1,165 @@ +/* + * 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, useEffect, useState } from 'react'; +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 { 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'; +import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; + +const METRIC_DISTRIBUTION_CHART_WIDTH = 325; +const METRIC_DISTRIBUTION_CHART_HEIGHT = 200; + +interface SummaryTableItem { + function: string; + display: ReactNode; + value: number | string | undefined | null; +} + +export const NumberContent: FC = ({ config }) => { + const { stats, fieldFormat } = config; + + useEffect(() => { + const chartData = buildChartDataFromStats(stats, METRIC_DISTRIBUTION_CHART_WIDTH); + setDistributionChartData(chartData); + }, []); + + const defaultChartData: MetricDistributionChartData[] = []; + const [distributionChartData, setDistributionChartData] = useState(defaultChartData); + + if (stats === undefined) return null; + const { min, median, max, distribution } = stats; + + const summaryTableItems = [ + { + function: 'min', + display: ( + + ), + value: kibanaFieldFormat(min, fieldFormat), + }, + { + function: 'median', + display: ( + + ), + value: kibanaFieldFormat(median, fieldFormat), + }, + { + function: 'max', + display: ( + + ), + value: kibanaFieldFormat(max, fieldFormat), + }, + ]; + const summaryTableColumns = [ + { + name: '', + render: (summaryItem: { display: ReactNode }) => summaryItem.display, + width: '75px', + }, + { + field: 'value', + name: '', + render: (v: string) => {v}, + }, + ]; + + const summaryTableTitle = i18n.translate( + 'xpack.ml.fieldDataCardExpandedRow.numberContent.summaryTableTitle', + { + defaultMessage: 'Summary', + } + ); + return ( + + + {summaryTableTitle} + + className={'mlDataVisualizerSummaryTable'} + compressed + items={summaryTableItems} + columns={summaryTableColumns} + tableCaption={summaryTableTitle} + /> + + {stats && ( + + + + + + + + + + + )} + {distribution && ( + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/distinct_values.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/distinct_values.tsx new file mode 100644 index 0000000000000..819d5278c0d78 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/distinct_values.tsx @@ -0,0 +1,23 @@ +/* + * 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, EuiIcon, EuiText } from '@elastic/eui'; + +import React from 'react'; + +export const DistinctValues = ({ cardinality }: { cardinality?: number }) => { + if (cardinality === undefined) return null; + return ( + + + + + + {cardinality} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/document_stats.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/document_stats.tsx new file mode 100644 index 0000000000000..9421b7d9f51e7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/document_stats.tsx @@ -0,0 +1,32 @@ +/* + * 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, EuiIcon, EuiText } from '@elastic/eui'; + +import React from 'react'; +import { FieldDataCardProps } from '../../../index_based/components/field_data_card'; +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; + +export const DocumentStat = ({ config }: FieldDataCardProps) => { + const { stats } = config; + if (stats === undefined) return null; + + const { count, sampleCount } = stats; + if (count === undefined || sampleCount === undefined) return null; + + const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); + + return ( + + + + + + {count} ({docsPercent}%) + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/number_content_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/number_content_preview.tsx new file mode 100644 index 0000000000000..7759aaefb03d0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/number_content_preview.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import { 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'; +import { formatSingleValue } from '../../../../formatters/format_value'; + +const METRIC_DISTRIBUTION_CHART_WIDTH = 150; +const METRIC_DISTRIBUTION_CHART_HEIGHT = 80; + +export const NumberContentPreview: FC = ({ config }) => { + const { stats, fieldFormat, fieldName } = config; + const defaultChartData: MetricDistributionChartData[] = []; + const [distributionChartData, setDistributionChartData] = useState(defaultChartData); + const [legendText, setLegendText] = useState<{ min: number; max: number } | undefined>(); + const dataTestSubj = fieldName; + useEffect(() => { + const chartData = buildChartDataFromStats(stats, METRIC_DISTRIBUTION_CHART_WIDTH); + if ( + Array.isArray(chartData) && + chartData[0].x !== undefined && + chartData[chartData.length - 1].x !== undefined + ) { + setDistributionChartData(chartData); + setLegendText({ + min: formatSingleValue(chartData[0].x), + max: formatSingleValue(chartData[chartData.length - 1].x), + }); + } + }, []); + + return ( +
+
+ +
+
+ {legendText && ( + <> + + + {legendText.min} + + {legendText.max} + + + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/top_values_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/top_values_preview.tsx new file mode 100644 index 0000000000000..52607ee71f25b --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/components/field_data_row/top_values_preview.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiDataGridColumn } from '@elastic/eui'; +import { FieldDataCardProps } from '../../../index_based/components/field_data_card'; +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 = ({ config }) => { + const { stats } = config; + if (stats === undefined) return null; + const { topValues, cardinality } = stats; + if (cardinality === undefined || topValues === undefined || config.fieldName === undefined) + return null; + + const data: OrdinalDataItem[] = topValues.map((d) => ({ + ...d, + key: d.key.toString(), + })); + const chartData: ChartData = { + cardinality, + data, + id: config.fieldName, + type: 'ordinal', + }; + const columnType: EuiDataGridColumn = { + id: config.fieldName, + schema: undefined, + }; + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx new file mode 100644 index 0000000000000..fe6e0b6ec9a8a --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx @@ -0,0 +1,68 @@ +/* + * 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 { FieldVisConfig } from '../index_based/common'; +import { + BooleanContent, + DateContent, + GeoPointContent, + IpContent, + KeywordContent, + NotInDocsContent, + 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'; + +export const DataVisualizerFieldExpandedRow = ({ item }: { item: FieldVisConfig }) => { + const config = item; + const { loading, type, existsInDocs, fieldName } = config; + + function getCardContent() { + if (existsInDocs === false) { + return ; + } + + switch (type) { + case ML_JOB_FIELD_TYPES.NUMBER: + return ; + + case ML_JOB_FIELD_TYPES.BOOLEAN: + return ; + + case ML_JOB_FIELD_TYPES.DATE: + return ; + + case ML_JOB_FIELD_TYPES.GEO_POINT: + return ; + + case ML_JOB_FIELD_TYPES.IP: + return ; + + case ML_JOB_FIELD_TYPES.KEYWORD: + return ; + + case ML_JOB_FIELD_TYPES.TEXT: + return ; + + default: + return ; + } + } + + return ( +
+ {loading === true ? : getCardContent()} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/index.ts new file mode 100644 index 0000000000000..35a785e3cba67 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/index.ts @@ -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 { DataVisualizerDataGrid } from './stats_datagrid'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/stats_datagrid.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/stats_datagrid.tsx new file mode 100644 index 0000000000000..be85d7ad01596 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/stats_datagrid.tsx @@ -0,0 +1,251 @@ +/* + * 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, useState } from 'react'; + +import { + CENTER_ALIGNMENT, + EuiButtonIcon, + EuiFlexItem, + EuiIcon, + EuiInMemoryTable, + EuiText, + HorizontalAlignment, + LEFT_ALIGNMENT, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiTableComputedColumnType } from '@elastic/eui/src/components/basic_table/table_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 { 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'; + +interface DataVisualizerDataGrid { + items: FieldVisConfig[]; + pageState: DataVisualizerIndexBasedAppState; + updatePageState: (update: Partial) => void; +} + +export type ItemIdToExpandedRowMap = Record; + +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] = ; + } + return m; + }, {} as ItemIdToExpandedRowMap); +} + +export const DataVisualizerDataGrid = ({ + items, + pageState, + updatePageState, +}: DataVisualizerDataGrid) => { + const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); + const [expandAll, toggleExpandAll] = useState(false); + + const { onTableChange, pagination, sorting } = useTableSettings( + items, + pageState, + updatePageState + ); + const showDistributions: boolean = pageState.showDistributions ?? true; + const toggleShowDistribution = () => { + updatePageState({ + ...pageState, + showDistributions: !showDistributions, + }); + }; + + function toggleDetails(item: FieldVisConfig) { + if (item.fieldName === undefined) return; + const index = expandedRowItemIds.indexOf(item.fieldName); + if (index !== -1) { + expandedRowItemIds.splice(index, 1); + } else { + expandedRowItemIds.push(item.fieldName); + } + + // spread to a new array otherwise the component wouldn't re-render + setExpandedRowItemIds([...expandedRowItemIds]); + } + + const columns = useMemo(() => { + const expanderColumn: EuiTableComputedColumnType = { + name: ( + toggleExpandAll(!expandAll)} + aria-label={ + !expandAll + ? i18n.translate('xpack.ml.datavisualizer.dataGrid.expandDetailsForAllAriaLabel', { + defaultMessage: 'Expand details for all fields', + }) + : i18n.translate('xpack.ml.datavisualizer.dataGrid.collapseDetailsForAllAriaLabel', { + defaultMessage: 'Collapse details for all fields', + }) + } + iconType={expandAll ? 'arrowUp' : 'arrowDown'} + /> + ), + align: RIGHT_ALIGNMENT, + width: '40px', + isExpander: true, + render: (item: FieldVisConfig) => { + if (item.fieldName === undefined) return null; + const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown'; + return ( + toggleDetails(item)} + aria-label={ + expandedRowItemIds.includes(item.fieldName) + ? i18n.translate('xpack.ml.datavisualizer.dataGrid.rowCollapse', { + defaultMessage: 'Hide details for {fieldName}', + values: { fieldName: item.fieldName }, + }) + : i18n.translate('xpack.ml.datavisualizer.dataGrid.rowExpand', { + defaultMessage: 'Show details for {fieldName}', + values: { fieldName: item.fieldName }, + }) + } + iconType={direction} + /> + ); + }, + 'data-test-subj': 'mlDataVisualizerTableColumnDetailsToggle', + }; + + return [ + expanderColumn, + { + field: 'type', + name: i18n.translate('xpack.ml.datavisualizer.dataGrid.typeColumnName', { + defaultMessage: 'Type', + }), + render: (fieldType: MlJobFieldType) => { + return ; + }, + width: '75px', + sortable: true, + align: CENTER_ALIGNMENT as HorizontalAlignment, + 'data-test-subj': 'mlDataVisualizerTableColumnType', + }, + { + field: 'fieldName', + name: i18n.translate('xpack.ml.datavisualizer.dataGrid.nameColumnName', { + defaultMessage: 'Name', + }), + sortable: true, + truncateText: true, + render: (fieldName: string) => ( + + {fieldName} + + ), + align: LEFT_ALIGNMENT as HorizontalAlignment, + 'data-test-subj': 'mlDataVisualizerTableColumnName', + }, + { + field: 'docCount', + name: i18n.translate('xpack.ml.datavisualizer.dataGrid.documentsCountColumnName', { + defaultMessage: 'Documents (%)', + }), + render: (value: number | undefined, item: FieldVisConfig) => , + sortable: (item: FieldVisConfig) => item?.stats?.count, + align: LEFT_ALIGNMENT as HorizontalAlignment, + 'data-test-subj': 'mlDataVisualizerTableColumnDocumentsCount', + }, + { + field: 'stats.cardinality', + name: i18n.translate('xpack.ml.datavisualizer.dataGrid.distinctValuesColumnName', { + defaultMessage: 'Distinct values', + }), + render: (cardinality?: number) => , + sortable: true, + align: LEFT_ALIGNMENT as HorizontalAlignment, + 'data-test-subj': 'mlDataVisualizerTableColumnDistinctValues', + }, + { + name: ( +
+ + {i18n.translate('xpack.ml.datavisualizer.dataGrid.distributionsColumnName', { + defaultMessage: 'Distributions', + })} + toggleShowDistribution()} + aria-label={i18n.translate( + 'xpack.ml.datavisualizer.dataGrid.showDistributionsAriaLabel', + { + defaultMessage: 'Show distributions', + } + )} + /> +
+ ), + render: (item: FieldVisConfig) => { + if (item === undefined || showDistributions === false) return null; + if (item.type === 'keyword' && item.stats?.topValues !== undefined) { + return ; + } + + if (item.type === 'number' && item.stats?.distribution !== undefined) { + return ; + } + return null; + }, + align: LEFT_ALIGNMENT as HorizontalAlignment, + 'data-test-subj': 'mlDataVisualizerTableColumnDistribution', + }, + ]; + }, [expandAll, showDistributions, updatePageState]); + + const itemIdToExpandedRowMap = useMemo(() => { + let itemIds = expandedRowItemIds; + if (expandAll) { + itemIds = items.map((i) => i[FIELD_NAME]).filter((f) => f !== undefined) as string[]; + } + return getItemIdToExpandedRowMap(itemIds, items); + }, [expandAll, items, expandedRowItemIds]); + + return ( + + + className={'mlDataVisualizer'} + items={items} + itemId={FIELD_NAME} + columns={columns} + pagination={pagination} + sorting={sorting} + isExpandable={true} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + isSelectable={false} + onTableChange={onTableChange} + data-test-subj={'mlDataVisualizerTable'} + rowProps={(item) => ({ + 'data-test-subj': `mlDataVisualizerRow row-${item.fieldName}`, + })} + /> + + ); +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7059c614185ef..1a72a8acc338e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12106,25 +12106,14 @@ "xpack.ml.dataGridChart.histogramNotAvailable": "グラフはサポートされていません。", "xpack.ml.dataGridChart.notEnoughData": "0個のドキュメントにフィールドが含まれます。", "xpack.ml.dataGridChart.singleCategoryLegend": "{cardinality, plural, one {カテゴリ} other {カテゴリ}}", - "xpack.ml.dataGridChart.topCategoriesLegend": "上位{MAX_CHART_COLUMNS}/{cardinality}カテゴリ", "xpack.ml.datavisualizer.actionsPanel.advancedDescription": "より高度なユースケースでは、ジョブの作成にすべてのオプションを使用します", "xpack.ml.datavisualizer.actionsPanel.advancedTitle": "高度な設定", "xpack.ml.datavisualizer.actionsPanel.createJobDescription": "高度なジョブウィザードでジョブを作成し、このデータの異常を検出します:", "xpack.ml.datavisualizer.actionsPanel.createJobTitle": "ジョブの作成", "xpack.ml.datavisualizer.actionsPanel.selectKnownConfigurationDescription": "認識されたデータの既知の構成を選択します:", "xpack.ml.datavisualizer.dataLoader.internalServerErrorMessage": "インデックス {index} のデータの読み込み中にエラーが発生。{message}。リクエストがタイムアウトした可能性があります。小さなサンプルサイズを使うか、時間範囲を狭めてみてください。", - "xpack.ml.datavisualizer.fieldsPanel.countDescription": "サンプリングされたドキュメントに{cardsCount} {cardsCount, plural, one {フィールドが存在します} other {フィールドが存在します}}", - "xpack.ml.datavisualizer.fieldsPanel.filterFieldsPlaceholder": "{type}をフィルター", - "xpack.ml.datavisualizer.fieldsPanel.searchBarError": "検索の実行中にエラーが発生。{message}。", - "xpack.ml.datavisualizer.fieldsPanel.showEmptyFieldsLabel": "空のフィールドを表示", - "xpack.ml.datavisualizer.fieldsPanel.totalFieldLabel": "合計フィールド数: {wrappedTotalFields}", - "xpack.ml.datavisualizer.fieldTypesSelect.allFieldsTypeOptionLabel": "すべてのフィールドタイプ", - "xpack.ml.datavisualizer.fieldTypesSelect.selectAriaLabel": "表示するフィールドタイプを選択してください", - "xpack.ml.datavisualizer.fieldTypesSelect.typeOptionLabel": "{fieldType} タイプ", "xpack.ml.dataVisualizer.fileBasedLabel": "ファイル", "xpack.ml.datavisualizer.page.errorLoadingDataMessage": "インデックス {index} のデータの読み込み中にエラーが発生。{message}。", - "xpack.ml.datavisualizer.page.fieldsPanelTitle": "フィールド", - "xpack.ml.datavisualizer.page.metricsPanelTitle": "メトリック", "xpack.ml.datavisualizer.searchPanel.allOptionLabel": "すべて検索", "xpack.ml.datavisualizer.searchPanel.invalidKuerySyntaxErrorMessageQueryBar": "無効なクエリ", "xpack.ml.datavisualizer.searchPanel.queryBarPlaceholder": "小さいサンプルサイズを選択することで、クエリの実行時間を短縮しクラスターへの負荷を軽減できます。", @@ -12233,19 +12222,8 @@ "xpack.ml.explorer.viewByLabel": "表示方式", "xpack.ml.feature.reserved.description": "ユーザーアクセスを許可するには、machine_learning_user か machine_learning_admin ロールのどちらかを割り当てる必要があります。", "xpack.ml.featureRegistry.mlFeatureName": "機械学習", - "xpack.ml.fieldDataCard.cardBoolean.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardBoolean.valuesLabel": "値", - "xpack.ml.fieldDataCard.cardDate.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", - "xpack.ml.fieldDataCard.cardDate.earliestDescription": "最も古い {earliestFormatted}", - "xpack.ml.fieldDataCard.cardDate.latestDescription": "最近の {latestFormatted}", - "xpack.ml.fieldDataCard.cardDocumentCount.calculatedOverAllDocumentsLabel": "全ドキュメントで計算", - "xpack.ml.fieldDataCard.cardGeoPoint.distinctCountDescription": "{cardinality} 個の特徴的な {cardinality, plural, zero {value} one {value} other {values}}", - "xpack.ml.fieldDataCard.cardGeoPoint.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", - "xpack.ml.fieldDataCard.cardIp.distinctCountDescription": "{cardinality} 個の特徴的な {cardinality, plural, zero {value} one {value} other {values}}", - "xpack.ml.fieldDataCard.cardIp.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardIp.topValuesLabel": "トップの値", - "xpack.ml.fieldDataCard.cardKeyword.distinctCountDescription": "{cardinality} 個の特徴的な {cardinality, plural, zero {value} one {value} other {values}}", - "xpack.ml.fieldDataCard.cardKeyword.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "トップの値", "xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel": "分布", "xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel": "トップの値", @@ -12259,7 +12237,6 @@ "xpack.ml.fieldDataCard.cardOther.cardTypeLabel": "{cardType} タイプ", "xpack.ml.fieldDataCard.cardOther.distinctCountDescription": "{cardinality} 個の特徴的な {cardinality, plural, zero {value} one {value} other {values}}", "xpack.ml.fieldDataCard.cardOther.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", - "xpack.ml.fieldDataCard.cardText.examplesTitle": "{numExamples, plural, one {値} other {例}}", "xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "たとえば、ドキュメントマッピングで {copyToParam} パラメーターを使ったり、{includesParam} と {excludesParam} パラメーターを使用してインデックスした後に {sourceParam} フィールドから切り取ったりして入力される場合があります。", "xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "このフィールドはクエリが実行されたドキュメントの {sourceParam} フィールドにありませんでした。", "xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "このフィールドの例が取得されませんでした", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 45ee8fbcabbd5..e67293f68c097 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12120,25 +12120,14 @@ "xpack.ml.dataGridChart.histogramNotAvailable": "不支持图表。", "xpack.ml.dataGridChart.notEnoughData": "0 个文档包含字段。", "xpack.ml.dataGridChart.singleCategoryLegend": "{cardinality, plural, one {# 个类别} other {# 个类别}}", - "xpack.ml.dataGridChart.topCategoriesLegend": "{cardinality} 个类别中的排名前 {MAX_CHART_COLUMNS}", "xpack.ml.datavisualizer.actionsPanel.advancedDescription": "使用全部选项为更高级的用例创建作业", "xpack.ml.datavisualizer.actionsPanel.advancedTitle": "高级", "xpack.ml.datavisualizer.actionsPanel.createJobDescription": "使用“高级作业”向导创建作业,以查找此数据中的异常:", "xpack.ml.datavisualizer.actionsPanel.createJobTitle": "创建作业", "xpack.ml.datavisualizer.actionsPanel.selectKnownConfigurationDescription": "选择已识别数据的已知配置:", "xpack.ml.datavisualizer.dataLoader.internalServerErrorMessage": "加载索引 {index} 中的数据时出错。{message}。请求可能已超时。请尝试使用较小的样例大小或缩小时间范围。", - "xpack.ml.datavisualizer.fieldsPanel.countDescription": "采样的文档中存在 {cardsCount} 个{cardsCount, plural, one {字段} other {字段}}", - "xpack.ml.datavisualizer.fieldsPanel.filterFieldsPlaceholder": "筛选 {type}", - "xpack.ml.datavisualizer.fieldsPanel.searchBarError": "运行搜索时发生错误。{message}。", - "xpack.ml.datavisualizer.fieldsPanel.showEmptyFieldsLabel": "显示空字段", - "xpack.ml.datavisualizer.fieldsPanel.totalFieldLabel": "字段总数:{wrappedTotalFields}", - "xpack.ml.datavisualizer.fieldTypesSelect.allFieldsTypeOptionLabel": "所有字段类型", - "xpack.ml.datavisualizer.fieldTypesSelect.selectAriaLabel": "选择要显示的字段类型", - "xpack.ml.datavisualizer.fieldTypesSelect.typeOptionLabel": "{fieldType} 类型", "xpack.ml.dataVisualizer.fileBasedLabel": "文件", "xpack.ml.datavisualizer.page.errorLoadingDataMessage": "加载索引 {index} 中的数据时出错。{message}", - "xpack.ml.datavisualizer.page.fieldsPanelTitle": "字段", - "xpack.ml.datavisualizer.page.metricsPanelTitle": "指标", "xpack.ml.datavisualizer.searchPanel.allOptionLabel": "搜索全部", "xpack.ml.datavisualizer.searchPanel.invalidKuerySyntaxErrorMessageQueryBar": "无效查询", "xpack.ml.datavisualizer.searchPanel.queryBarPlaceholder": "选择较小的样例大小将减少查询运行时间和集群上的负载。", @@ -12247,19 +12236,8 @@ "xpack.ml.explorer.viewByLabel": "查看者", "xpack.ml.feature.reserved.description": "要向用户授予访问权限,还应分配 machine_learning_user 或 machine_learning_admin 角色。", "xpack.ml.featureRegistry.mlFeatureName": "机器学习", - "xpack.ml.fieldDataCard.cardBoolean.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardBoolean.valuesLabel": "值", - "xpack.ml.fieldDataCard.cardDate.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", - "xpack.ml.fieldDataCard.cardDate.earliestDescription": "最早的 {earliestFormatted}", - "xpack.ml.fieldDataCard.cardDate.latestDescription": "最新的 {latestFormatted}", - "xpack.ml.fieldDataCard.cardDocumentCount.calculatedOverAllDocumentsLabel": "计算所有文档", - "xpack.ml.fieldDataCard.cardGeoPoint.distinctCountDescription": "{cardinality} 不同的 {cardinality, plural, zero {值} one {value} 其他 {values}}", - "xpack.ml.fieldDataCard.cardGeoPoint.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", - "xpack.ml.fieldDataCard.cardIp.distinctCountDescription": "{cardinality} 不同的 {cardinality, plural, zero {值} one {value} 其他 {values}}", - "xpack.ml.fieldDataCard.cardIp.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardIp.topValuesLabel": "排在前面的值", - "xpack.ml.fieldDataCard.cardKeyword.distinctCountDescription": "{cardinality} 不同的 {cardinality, plural, zero {值} one {value} 其他 {values}}", - "xpack.ml.fieldDataCard.cardKeyword.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardKeyword.topValuesLabel": "排名最前值", "xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel": "分布", "xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel": "排名最前值", @@ -12273,7 +12251,6 @@ "xpack.ml.fieldDataCard.cardOther.cardTypeLabel": "{cardType} 类型", "xpack.ml.fieldDataCard.cardOther.distinctCountDescription": "{cardinality} 不同的 {cardinality, plural, zero {值} one {value} 其他 {values}}", "xpack.ml.fieldDataCard.cardOther.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", - "xpack.ml.fieldDataCard.cardText.examplesTitle": "{numExamples, plural, one { 个值} other { 个示例}}", "xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription": "例如,可以使用文档映射中的 {copyToParam} 参数进行填充,也可以在索引后通过使用 {includesParam} 和 {excludesParam} 参数从 {sourceParam} 字段中进行剪裁。", "xpack.ml.fieldDataCard.cardText.fieldNotPresentDescription": "查询的文档的 {sourceParam} 字段中不存在此字段。", "xpack.ml.fieldDataCard.cardText.noExamplesForFieldsTitle": "没有获取此字段的示例", diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index a7660e68e93e1..4b793c4c3adba 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -11,11 +11,11 @@ import { FieldVisConfig } from '../../../../../plugins/ml/public/application/dat interface MetricFieldVisConfig extends FieldVisConfig { statsMaxDecimalPlaces: number; docCountFormatted: string; - selectedDetailsMode: 'distribution' | 'top_values'; topValuesCount: number; } interface NonMetricFieldVisConfig extends FieldVisConfig { + docCountFormatted: string; exampleCount?: number; } @@ -34,21 +34,13 @@ interface TestData { nonMetricFieldsTypeFilterCardCount: number; metricFieldsFilterCardCount: number; nonMetricFieldsFilterCardCount: number; + visibleMetricFieldsCount: number; + totalMetricFieldsCount: number; + populatedFieldsCount: number; + totalFieldsCount: number; }; } -function getFieldTypes(cards: FieldVisConfig[]) { - const fieldTypes: ML_JOB_FIELD_TYPES[] = []; - cards.forEach((card) => { - const fieldType = card.type; - if (fieldTypes.includes(fieldType) === false) { - fieldTypes.push(fieldType); - } - }); - - return fieldTypes.sort(); -} - export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); @@ -75,9 +67,8 @@ export default function ({ getService }: FtrProviderContext) { existsInDocs: true, aggregatable: true, loading: false, - docCountFormatted: '5,000', + docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, - selectedDetailsMode: 'distribution', topValuesCount: 10, }, ], @@ -88,6 +79,7 @@ export default function ({ getService }: FtrProviderContext) { existsInDocs: true, aggregatable: true, loading: false, + docCountFormatted: '5000 (100%)', }, { fieldName: '@version', @@ -96,6 +88,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: false, loading: false, exampleCount: 1, + docCountFormatted: '', }, { fieldName: '@version.keyword', @@ -104,6 +97,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 1, + docCountFormatted: '5000 (100%)', }, { fieldName: 'airline', @@ -112,6 +106,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 10, + docCountFormatted: '5000 (100%)', }, { fieldName: 'type', @@ -120,6 +115,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: false, loading: false, exampleCount: 1, + docCountFormatted: '', }, { fieldName: 'type.keyword', @@ -128,11 +124,16 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 1, + docCountFormatted: '5000 (100%)', }, ], nonMetricFieldsTypeFilterCardCount: 3, metricFieldsFilterCardCount: 1, nonMetricFieldsFilterCardCount: 1, + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, }, }; @@ -158,9 +159,8 @@ export default function ({ getService }: FtrProviderContext) { existsInDocs: true, aggregatable: true, loading: false, - docCountFormatted: '5,000', + docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, - selectedDetailsMode: 'distribution', topValuesCount: 10, }, ], @@ -171,6 +171,7 @@ export default function ({ getService }: FtrProviderContext) { existsInDocs: true, aggregatable: true, loading: false, + docCountFormatted: '5000 (100%)', }, { fieldName: '@version', @@ -179,6 +180,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: false, loading: false, exampleCount: 1, + docCountFormatted: '', }, { fieldName: '@version.keyword', @@ -187,6 +189,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 1, + docCountFormatted: '5000 (100%)', }, { fieldName: 'airline', @@ -195,6 +198,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 5, + docCountFormatted: '5000 (100%)', }, { fieldName: 'type', @@ -203,6 +207,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: false, loading: false, exampleCount: 1, + docCountFormatted: '', }, { fieldName: 'type.keyword', @@ -211,11 +216,16 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 1, + docCountFormatted: '5000 (100%)', }, ], nonMetricFieldsTypeFilterCardCount: 3, metricFieldsFilterCardCount: 2, nonMetricFieldsFilterCardCount: 1, + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, }, }; @@ -241,9 +251,8 @@ export default function ({ getService }: FtrProviderContext) { existsInDocs: true, aggregatable: true, loading: false, - docCountFormatted: '5,000', + docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, - selectedDetailsMode: 'distribution', topValuesCount: 10, }, ], @@ -254,6 +263,7 @@ export default function ({ getService }: FtrProviderContext) { existsInDocs: true, aggregatable: true, loading: false, + docCountFormatted: '5000 (100%)', }, { fieldName: '@version', @@ -262,6 +272,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: false, loading: false, exampleCount: 1, + docCountFormatted: '', }, { fieldName: '@version.keyword', @@ -270,6 +281,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 1, + docCountFormatted: '5000 (100%)', }, { fieldName: 'airline', @@ -278,6 +290,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 5, + docCountFormatted: '5000 (100%)', }, { fieldName: 'type', @@ -286,6 +299,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: false, loading: false, exampleCount: 1, + docCountFormatted: '', }, { fieldName: 'type.keyword', @@ -294,11 +308,16 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, exampleCount: 1, + docCountFormatted: '5000 (100%)', }, ], nonMetricFieldsTypeFilterCardCount: 3, metricFieldsFilterCardCount: 2, nonMetricFieldsFilterCardCount: 1, + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, }, }; @@ -326,105 +345,60 @@ export default function ({ getService }: FtrProviderContext) { testData.expected.totalDocCountFormatted ); - await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the panels of fields`); - await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(testData.expected.fieldsPanelCount); - - await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the Metrics panel`); - await ml.dataVisualizerIndexBased.assertFieldsPanelForTypesExist([ML_JOB_FIELD_TYPES.NUMBER]); - - await ml.testExecution.logTestStep( - `${testData.suiteTitle} displays the expected document count card` + await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the doc count`); + await ml.dataVisualizerIndexBased.assertTotalDocCountHeaderExist(); + await ml.dataVisualizerIndexBased.assertTotalDocCountChartExist(); + + await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the search panel count`); + await ml.dataVisualizerIndexBased.assertSearchPanelExist(); + await ml.dataVisualizerIndexBased.assertSampleSizeInputExists(); + await ml.dataVisualizerIndexBased.assertFieldTypeInputExists(); + await ml.dataVisualizerIndexBased.assertFieldNameInputExists(); + + await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the field count`); + await ml.dataVisualizerIndexBased.assertFieldCountPanelExist(); + await ml.dataVisualizerIndexBased.assertMetricFieldsSummaryExist(); + await ml.dataVisualizerIndexBased.assertFieldsSummaryExist(); + await ml.dataVisualizerIndexBased.assertShowEmptyFieldsSwitchExists(); + await ml.dataVisualizerIndexBased.assertVisibleMetricFieldsCount( + testData.expected.visibleMetricFieldsCount + ); + await ml.dataVisualizerIndexBased.assertTotalMetricFieldsCount( + testData.expected.totalMetricFieldsCount ); - await ml.dataVisualizerIndexBased.assertCardExists( - testData.expected.documentCountCard.type, - testData.expected.documentCountCard.fieldName + await ml.dataVisualizerIndexBased.assertVisibleFieldsCount( + testData.expected.populatedFieldsCount ); - await ml.dataVisualizerIndexBased.assertDocumentCountCardContents(); + await ml.dataVisualizerIndexBased.assertTotalFieldsCount(testData.expected.totalFieldsCount); await ml.testExecution.logTestStep( - `${testData.suiteTitle} displays the expected metric field cards and contents` + `${testData.suiteTitle} displays the data visualizer table` ); + await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - if (testData.expected.metricCards !== undefined && testData.expected.metricCards.length > 0) { - await ml.testExecution.logTestStep( - `${testData.suiteTitle} displays the expected metric field cards and contents` - ); - for (const fieldCard of testData.expected.metricCards as MetricFieldVisConfig[]) { - await ml.dataVisualizerIndexBased.assertCardExists(fieldCard.type, fieldCard.fieldName); - await ml.dataVisualizerIndexBased.assertNumberCardContents( - fieldCard.fieldName!, - fieldCard.docCountFormatted, - fieldCard.statsMaxDecimalPlaces, - fieldCard.selectedDetailsMode, - fieldCard.topValuesCount - ); - } - - await ml.testExecution.logTestStep( - `${testData.suiteTitle} filters metric fields cards with search` - ); - await ml.dataVisualizerIndexBased.filterFieldsPanelWithSearchString( - ['number'], - testData.metricFieldsFilter, - testData.expected.metricFieldsFilterCardCount + await ml.testExecution.logTestStep( + 'displays details for the created job in the analytics table' + ); + for (const fieldCard of testData.expected.metricCards as Array< + Required + >) { + await ml.dataVisualizerTable.assertNumberRowContents( + fieldCard.fieldName, + fieldCard.docCountFormatted ); } - if ( - testData.expected.nonMetricCards !== undefined && - testData.expected.nonMetricCards.length > 0 - ) { - await ml.testExecution.logTestStep( - `${testData.suiteTitle} displays the non-metric Fields panel` - ); - await ml.dataVisualizerIndexBased.assertFieldsPanelForTypesExist( - getFieldTypes(testData.expected.nonMetricCards as FieldVisConfig[]) - ); - - await ml.testExecution.logTestStep( - `${testData.suiteTitle} displays the expected non-metric field cards and contents` - ); - for (const fieldCard of testData.expected.nonMetricCards!) { - await ml.dataVisualizerIndexBased.assertCardExists(fieldCard.type, fieldCard.fieldName); - await ml.dataVisualizerIndexBased.assertNonMetricCardContents( - fieldCard.type, - fieldCard.fieldName!, - fieldCard.exampleCount - ); - } - - await ml.testExecution.logTestStep( - `${testData.suiteTitle} sets the non metric field types input` - ); - const fieldTypes: ML_JOB_FIELD_TYPES[] = getFieldTypes( - testData.expected.nonMetricCards as FieldVisConfig[] - ); - await ml.dataVisualizerIndexBased.assertFieldsPanelTypeInputExists(fieldTypes); - await ml.dataVisualizerIndexBased.setFieldsPanelTypeInputValue( - fieldTypes, - testData.nonMetricFieldsTypeFilter, - testData.expected.nonMetricFieldsTypeFilterCardCount - ); - - await ml.testExecution.logTestStep( - `${testData.suiteTitle} filters non-metric fields cards with search` - ); - await ml.dataVisualizerIndexBased.filterFieldsPanelWithSearchString( - fieldTypes, - testData.nonMetricFieldsFilter, - testData.expected.nonMetricFieldsFilterCardCount - ); + for (const fieldCard of testData.expected.nonMetricCards as Array< + Required + >) { + await ml.dataVisualizerTable.assertRowExists(fieldCard.fieldName); + } - await ml.testExecution.logTestStep( - `${testData.suiteTitle} sample size control changes non-metric field cards doc count` - ); - await ml.dataVisualizerIndexBased.clearFieldsPanelSearchInput(fieldTypes); - await ml.dataVisualizerIndexBased.assertSampleSizeInputExists(); - await ml.dataVisualizerIndexBased.setSampleSizeInputValue( - 1000, - ML_JOB_FIELD_TYPES.KEYWORD, - 'airline', - '1,000' + for (const fieldCard of testData.expected.nonMetricCards!) { + await ml.dataVisualizerTable.assertNonMetricCardContents( + fieldCard.type, + fieldCard.fieldName!, + fieldCard.docCountFormatted ); } }); diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 285ea49419bbd..ebbc077c4675c 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -97,7 +97,6 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const ecExpectedFieldPanelCount = 2; const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( @@ -354,8 +353,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should load data for full time range'); await ml.dataVisualizerIndexBased.clickUseFullDataButton(ecExpectedTotalCount); - await ml.testExecution.logTestStep('should display the panels of fields'); - await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(ecExpectedFieldPanelCount); + await ml.testExecution.logTestStep('should display the data visualizer table'); + await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); await ml.testExecution.logTestStep('should display the actions panel with cards'); await ml.dataVisualizerIndexBased.assertActionsPanelExists(); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index c759f22d0396c..e3bd0384475e2 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -98,7 +98,6 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const ecExpectedFieldPanelCount = 2; const uploadFilePath = path.join( __dirname, @@ -346,8 +345,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should load data for full time range'); await ml.dataVisualizerIndexBased.clickUseFullDataButton(ecExpectedTotalCount); - await ml.testExecution.logTestStep('should display the panels of fields'); - await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(ecExpectedFieldPanelCount); + await ml.testExecution.logTestStep('should display the data visualizer table'); + await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); await ml.testExecution.logTestStep('should not display the actions panel'); await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 60677423a2aa1..413f4565ef30d 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { ML_JOB_FIELD_TYPES } from '../../../../plugins/ml/common/constants/field_types'; import { MlCommonUI } from './common_ui'; +import { MlJobFieldType } from '../../../../plugins/ml/common/types/field_types'; export function MachineLearningDataVisualizerIndexBasedProvider( { getService }: FtrProviderContext, @@ -37,15 +38,91 @@ export function MachineLearningDataVisualizerIndexBasedProvider( await this.assertTotalDocumentCount(expectedFormattedTotalDocCount); }, - async assertFieldsPanelsExist(expectedPanelCount: number) { - const allPanels = await testSubjects.findAll('~mlDataVisualizerFieldsPanel'); - expect(allPanels).to.have.length( - expectedPanelCount, - `Expected field panels count to be '${expectedPanelCount}' (got '${allPanels.length}')` - ); + async assertTotalDocCountHeaderExist() { + await testSubjects.existOrFail(`mlDataVisualizerTotalDocCountHeader`); + }, + + async assertTotalDocCountChartExist() { + await testSubjects.existOrFail(`mlFieldDataCardDocumentCountChart`); + }, + + async assertSearchPanelExist() { + await testSubjects.existOrFail(`mlDataVisualizerSearchPanel`); + }, + + async assertSearchQueryInputExist() { + await testSubjects.existOrFail(`mlDataVisualizerQueryInput`); + }, + + async assertFieldCountPanelExist() { + await testSubjects.existOrFail(`mlDataVisualizerFieldCountPanel`); + }, + + async assertMetricFieldsSummaryExist() { + await testSubjects.existOrFail(`mlDataVisualizerMetricFieldsSummary`); }, - async assertFieldsPanelForTypesExist(fieldTypes: ML_JOB_FIELD_TYPES[]) { + async assertVisibleMetricFieldsCount(count: number) { + const expectedCount = count.toString(); + await testSubjects.existOrFail('mlDataVisualizerVisibleMetricFieldsCount'); + await retry.tryForTime(5000, async () => { + const actualCount = await testSubjects.getVisibleText( + 'mlDataVisualizerVisibleMetricFieldsCount' + ); + expect(expectedCount).to.eql( + expectedCount, + `Expected visible metric fields count to be '${expectedCount}' (got '${actualCount}')` + ); + }); + }, + + async assertTotalMetricFieldsCount(count: number) { + const expectedCount = count.toString(); + await testSubjects.existOrFail('mlDataVisualizerMetricFieldsCount'); + await retry.tryForTime(5000, async () => { + const actualCount = await testSubjects.getVisibleText( + 'mlDataVisualizerVisibleMetricFieldsCount' + ); + expect(expectedCount).to.contain( + expectedCount, + `Expected total metric fields count to be '${expectedCount}' (got '${actualCount}')` + ); + }); + }, + + async assertVisibleFieldsCount(count: number) { + const expectedCount = count.toString(); + await testSubjects.existOrFail('mlDataVisualizerVisibleFieldsCount'); + await retry.tryForTime(5000, async () => { + const actualCount = await testSubjects.getVisibleText('mlDataVisualizerVisibleFieldsCount'); + expect(expectedCount).to.eql( + expectedCount, + `Expected fields count to be '${expectedCount}' (got '${actualCount}')` + ); + }); + }, + + async assertTotalFieldsCount(count: number) { + const expectedCount = count.toString(); + await testSubjects.existOrFail('mlDataVisualizerTotalFieldsCount'); + await retry.tryForTime(5000, async () => { + const actualCount = await testSubjects.getVisibleText('mlDataVisualizerTotalFieldsCount'); + expect(expectedCount).to.contain( + expectedCount, + `Expected total fields count to be '${expectedCount}' (got '${actualCount}')` + ); + }); + }, + + async assertFieldsSummaryExist() { + await testSubjects.existOrFail(`mlDataVisualizerFieldsSummary`); + }, + + async assertDataVisualizerTableExist() { + await testSubjects.existOrFail(`mlDataVisualizerTable`); + }, + + async assertFieldsPanelForTypesExist(fieldTypes: MlJobFieldType[]) { await testSubjects.existOrFail(`mlDataVisualizerFieldsPanel ${fieldTypes}`); }, @@ -290,6 +367,18 @@ export function MachineLearningDataVisualizerIndexBasedProvider( await this.assertFieldsPanelCardCount(panelFieldTypes, expectedCardCount); }, + async assertShowEmptyFieldsSwitchExists() { + await testSubjects.existOrFail('mlDataVisualizerShowEmptyFieldsSwitch'); + }, + + async assertFieldNameInputExists() { + await testSubjects.existOrFail('mlDataVisualizerFieldNameSelect'); + }, + + async assertFieldTypeInputExists() { + await testSubjects.existOrFail('mlDataVisualizerFieldTypeSelect'); + }, + async assertSampleSizeInputExists() { await testSubjects.existOrFail('mlDataVisualizerShardSizeSelect'); }, diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts new file mode 100644 index 0000000000000..94f7b618af998 --- /dev/null +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -0,0 +1,164 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { ML_JOB_FIELD_TYPES } from '../../../../plugins/ml/common/constants/field_types'; + +export function MachineLearningDataVisualizerTableProvider({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + return new (class DataVisualizerTable { + public async parseDataVisualizerTable() { + const table = await testSubjects.find('~mlDataVisualizerTable'); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects('~mlDataVisualizerRow').toArray()) { + const $tr = $(tr); + + rows.push({ + type: $tr + .findTestSubject('mlDataVisualizerTableColumnType') + .find('.euiTableCellContent') + .text() + .trim(), + fieldName: $tr + .findTestSubject('mlDataVisualizerTableColumnName') + .find('.euiTableCellContent') + .text() + .trim(), + documentsCount: $tr + .findTestSubject('mlDataVisualizerTableColumnDocumentsCount') + .find('.euiTableCellContent') + .text() + .trim(), + distinctValues: $tr + .findTestSubject('mlDataVisualizerTableColumnDistinctValues') + .find('.euiTableCellContent') + .text() + .trim(), + distribution: $tr + .findTestSubject('mlDataVisualizerTableColumnDistribution') + .find('.euiTableCellContent') + .text() + .trim(), + }); + } + + return rows; + } + + public rowSelector(fieldName: string, subSelector?: string) { + const row = `~mlDataVisualizerTable > ~row-${fieldName}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async assertRowExists(fieldName: string) { + await testSubjects.existOrFail(this.rowSelector(fieldName)); + } + + public async expandRowDetails(fieldName: string, fieldType: string) { + const selector = this.rowSelector( + fieldName, + `mlDataVisualizerToggleDetails ${fieldName} arrowDown` + ); + await testSubjects.existOrFail(selector); + await testSubjects.click(selector); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail( + this.rowSelector(fieldName, `mlDataVisualizerToggleDetails ${fieldName} arrowUp`) + ); + await testSubjects.existOrFail( + `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType}` + ); + }); + } + + public async assertFieldDocCount(fieldName: string, docCountFormatted: string) { + const docCountFormattedSelector = this.rowSelector( + fieldName, + 'mlDataVisualizerTableColumnDocumentsCount' + ); + await testSubjects.existOrFail(docCountFormattedSelector); + const docCount = await testSubjects.getVisibleText(docCountFormattedSelector); + expect(docCount).to.eql( + docCountFormatted, + `Expected total document count to be '${docCountFormatted}' (got '${docCount}')` + ); + } + + public async assertNumberRowContents(fieldName: string, docCountFormatted: string) { + const fieldType = ML_JOB_FIELD_TYPES.NUMBER; + await this.assertRowExists(fieldName); + await this.assertFieldDocCount(fieldName, docCountFormatted); + + await this.expandRowDetails(fieldName, fieldType); + + await testSubjects.existOrFail( + `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlNumberSummaryTable` + ); + + await testSubjects.existOrFail( + `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlTopValues` + ); + await testSubjects.existOrFail( + `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlMetricDistribution` + ); + } + + public async assertDateRowContents(fieldName: string, docCountFormatted: string) { + const fieldType = ML_JOB_FIELD_TYPES.DATE; + await this.assertRowExists(fieldName); + await this.assertFieldDocCount(fieldName, docCountFormatted); + + await this.expandRowDetails(fieldName, fieldType); + + await testSubjects.existOrFail( + `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlDateSummaryTable` + ); + } + + public async assertKeywordRowContents(fieldName: string, docCountFormatted: string) { + const fieldType = ML_JOB_FIELD_TYPES.KEYWORD; + await this.assertRowExists(fieldName); + await this.assertFieldDocCount(fieldName, docCountFormatted); + + await this.expandRowDetails(fieldName, fieldType); + + await testSubjects.existOrFail( + `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlFieldDataCardTopValues` + ); + } + + public async assertTextRowContents(fieldName: string, docCountFormatted: string) { + const fieldType = ML_JOB_FIELD_TYPES.TEXT; + await this.assertRowExists(fieldName); + await this.assertFieldDocCount(fieldName, docCountFormatted); + + await this.expandRowDetails(fieldName, fieldType); + + await testSubjects.existOrFail( + `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlFieldDataCardExamplesList` + ); + } + + async assertNonMetricCardContents( + cardType: string, + fieldName: string, + docCountFormatted: string + ) { + // Currently the data used in the data visualizer tests only contains these field types. + if (cardType === ML_JOB_FIELD_TYPES.DATE) { + await this.assertDateRowContents(fieldName, docCountFormatted); + } else if (cardType === ML_JOB_FIELD_TYPES.KEYWORD) { + await this.assertKeywordRowContents(fieldName, docCountFormatted!); + } else if (cardType === ML_JOB_FIELD_TYPES.TEXT) { + await this.assertTextRowContents(fieldName, docCountFormatted!); + } + } + })(); +} diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 3744e3cad6471..72903b2c409d9 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -41,6 +41,7 @@ import { MachineLearningSettingsFilterListProvider } from './settings_filter_lis import { MachineLearningSingleMetricViewerProvider } from './single_metric_viewer'; import { MachineLearningTestExecutionProvider } from './test_execution'; import { MachineLearningTestResourcesProvider } from './test_resources'; +import { MachineLearningDataVisualizerTableProvider } from './data_visualizer_table'; export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); @@ -66,6 +67,8 @@ export function MachineLearningProvider(context: FtrProviderContext) { context, commonUI ); + const dataVisualizerTable = MachineLearningDataVisualizerTableProvider(context); + const jobManagement = MachineLearningJobManagementProvider(context, api); const jobSelection = MachineLearningJobSelectionProvider(context); const jobSourceSelection = MachineLearningJobSourceSelectionProvider(context); @@ -103,6 +106,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { dataVisualizer, dataVisualizerFileBased, dataVisualizerIndexBased, + dataVisualizerTable, jobManagement, jobSelection, jobSourceSelection, diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index 8140b471fc6c8..f72f52399c20b 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -21,7 +21,6 @@ export default function ({ getService }: FtrProviderContext) { describe(`(${user})`, function () { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const ecExpectedFieldPanelCount = 2; const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( @@ -124,8 +123,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should load data for full time range'); await ml.dataVisualizerIndexBased.clickUseFullDataButton(ecExpectedTotalCount); - await ml.testExecution.logTestStep('should display the panels of fields'); - await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(ecExpectedFieldPanelCount); + await ml.testExecution.logTestStep('should display the data visualizer table'); + await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); await ml.testExecution.logTestStep('should not display the actions panel with cards'); await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index 522ca15934e22..4ae56bfaa08de 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -21,7 +21,6 @@ export default function ({ getService }: FtrProviderContext) { describe(`(${user})`, function () { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const ecExpectedFieldPanelCount = 2; const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( @@ -124,8 +123,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should load data for full time range'); await ml.dataVisualizerIndexBased.clickUseFullDataButton(ecExpectedTotalCount); - await ml.testExecution.logTestStep('should display the panels of fields'); - await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(ecExpectedFieldPanelCount); + await ml.testExecution.logTestStep('should display the data visualizer table'); + await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); await ml.testExecution.logTestStep('should not display the actions panel with cards'); await ml.dataVisualizerIndexBased.assertActionsPanelNotExists();