From 900ca2a5f93e453526266cb466c67366da240c9d Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 8 Aug 2019 12:48:07 +0100 Subject: [PATCH] [ML] Converts index based data visualizer to React (#42685) * [ML] Converts index based data visualizer to React * [ML] Remove unused imports in React data visualizer * [ML] Address review feedback * [ML] Address comments from code review * [ML] Remove redundant ts-ignore --- .../plugins/ml/common/constants/search.ts | 5 + x-pack/legacy/plugins/ml/public/app.js | 1 + .../create_job_link_card.tsx | 28 + .../components/create_job_link_card/index.ts | 7 + .../{display_value.js => display_value.tsx} | 24 +- .../display_value/{index.js => index.ts} | 0 .../content_types/card_text.html | 2 +- .../components/field_title_bar/index.js | 2 + .../ml/public/data_visualizer/_index.scss | 1 + .../ml/public/data_visualizer/breadcrumbs.ts | 14 + .../common/field_vis_config.ts | 21 + .../ml/public/data_visualizer/common/index.ts | 8 + .../public/data_visualizer/common/request.ts | 13 + .../actions_panel/actions_panel.tsx | 65 ++ .../components/actions_panel/index.ts | 7 + .../field_data_card/_field_data_card.scss | 62 ++ .../components/field_data_card/_index.scss | 1 + .../content_types/boolean_content.tsx | 99 +++ .../content_types/date_content.tsx | 69 ++ .../content_types/document_count_content.tsx | 58 ++ .../content_types/geo_point_content.tsx | 80 +++ .../field_data_card/content_types/index.ts | 16 + .../content_types/ip_content.tsx | 69 ++ .../content_types/keyword_content.tsx | 69 ++ .../content_types/not_in_docs_content.tsx | 33 + .../content_types/number_content.tsx | 194 +++++ .../content_types/other_content.tsx | 74 ++ .../content_types/text_content.tsx | 62 ++ .../document_count_chart.tsx | 99 +++ .../document_count_chart/index.ts | 7 + .../field_data_card/examples_list/example.tsx | 31 + .../examples_list/examples_list.tsx | 43 ++ .../field_data_card/examples_list/index.ts | 7 + .../field_data_card/field_data_card.tsx | 81 +++ .../components/field_data_card/index.ts | 7 + .../loading_indicator/index.ts | 7 + .../loading_indicator/loading_indicator.tsx | 29 + .../metric_distribution_chart/index.ts | 8 + .../metric_distribution_chart.tsx | 145 ++++ ...metric_distribution_chart_data_builder.tsx | 155 ++++ ...tric_distribution_chart_tooltip_header.tsx | 53 ++ .../field_data_card/top_values/index.ts | 7 + .../field_data_card/top_values/top_values.tsx | 85 +++ .../field_types_select/field_types_select.tsx | 57 ++ .../components/field_types_select/index.ts | 7 + .../components/fields_panel/fields_panel.tsx | 169 +++++ .../components/fields_panel/index.ts | 7 + .../components/search_panel/index.ts | 7 + .../components/search_panel/search_panel.tsx | 143 ++++ .../data_loader/data_loader.ts | 135 ++++ .../data_visualizer/data_loader/index.ts | 7 + .../ml/public/data_visualizer/directive.tsx | 61 ++ .../ml/public/data_visualizer/index.ts | 8 + .../ml/public/data_visualizer/page.tsx | 672 ++++++++++++++++++ .../ml/public/data_visualizer/route.ts | 32 + .../selector/datavisualizer_selector.js | 21 + .../public/formatters/kibana_field_format.ts | 18 + x-pack/legacy/plugins/ml/public/index.scss | 1 + .../index_or_search_controller.js | 13 + .../public/services/ml_api_service/index.d.ts | 3 + .../ml/public/util/ml_time_buckets.d.ts | 1 + 61 files changed, 3192 insertions(+), 18 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx create mode 100644 x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts rename x-pack/legacy/plugins/ml/public/components/display_value/{display_value.js => display_value.tsx} (62%) rename x-pack/legacy/plugins/ml/public/components/display_value/{index.js => index.ts} (100%) create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/_index.scss create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/breadcrumbs.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/common/field_vis_config.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/common/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/common/request.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/actions_panel/actions_panel.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/actions_panel/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/_field_data_card.scss create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/_index.scss create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/boolean_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/date_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/document_count_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/geo_point_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/ip_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/keyword_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/not_in_docs_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/number_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/other_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/text_content.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/document_count_chart/document_count_chart.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/document_count_chart/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/example.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/examples_list.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/field_data_card.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/loading_indicator/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/loading_indicator/loading_indicator.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/top_values/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/top_values/top_values.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_types_select/field_types_select.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/field_types_select/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/fields_panel/fields_panel.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/fields_panel/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/search_panel/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/components/search_panel/search_panel.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/data_loader/data_loader.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/data_loader/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/page.tsx create mode 100644 x-pack/legacy/plugins/ml/public/data_visualizer/route.ts create mode 100644 x-pack/legacy/plugins/ml/public/formatters/kibana_field_format.ts diff --git a/x-pack/legacy/plugins/ml/common/constants/search.ts b/x-pack/legacy/plugins/ml/common/constants/search.ts index 2ea27c5b5322b..e17f6b3098421 100644 --- a/x-pack/legacy/plugins/ml/common/constants/search.ts +++ b/x-pack/legacy/plugins/ml/common/constants/search.ts @@ -6,3 +6,8 @@ export const ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE = 500; export const ANOMALIES_TABLE_DEFAULT_QUERY_SIZE = 500; + +export enum SEARCH_QUERY_LANGUAGE { + KUERY = 'kuery', + LUCENE = 'lucene', +} diff --git a/x-pack/legacy/plugins/ml/public/app.js b/x-pack/legacy/plugins/ml/public/app.js index 1ac74e9bb6321..191ca25c8eaf9 100644 --- a/x-pack/legacy/plugins/ml/public/app.js +++ b/x-pack/legacy/plugins/ml/public/app.js @@ -22,6 +22,7 @@ import 'plugins/ml/jobs'; import 'plugins/ml/services/calendar_service'; import 'plugins/ml/components/messagebar'; import 'plugins/ml/data_frame'; +import 'plugins/ml/data_visualizer'; import 'plugins/ml/datavisualizer'; import 'plugins/ml/explorer'; import 'plugins/ml/timeseriesexplorer'; diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx new file mode 100644 index 0000000000000..6549df35ba381 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx @@ -0,0 +1,28 @@ +/* + * 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 { EuiCard, EuiIcon, IconType } from '@elastic/eui'; + +interface Props { + iconType: IconType; + title: string; + description: string; + onClick(): void; +} + +// Component for rendering a card which links to the Create Job page, displaying an +// icon, card title, description and link. +export const CreateJobLinkCard: FC = ({ iconType, title, description, onClick }) => ( + } + title={title} + description={description} + onClick={onClick} + /> +); diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts new file mode 100644 index 0000000000000..b0fa3762a4ef3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/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 { CreateJobLinkCard } from './create_job_link_card'; diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/display_value.js b/x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx similarity index 62% rename from x-pack/legacy/plugins/ml/public/components/display_value/display_value.js rename to x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx index b9db4510ca533..cfe3d09a16320 100644 --- a/x-pack/legacy/plugins/ml/public/components/display_value/display_value.js +++ b/x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx @@ -4,31 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import React from 'react'; -import { - EuiToolTip -} from '@elastic/eui'; - +import React, { FC } from 'react'; +import { EuiToolTip } from '@elastic/eui'; const MAX_CHARS = 12; -export function DisplayValue({ value }) { +export const DisplayValue: FC<{ value: any }> = ({ value }) => { const length = String(value).length; - let formattedValue; if (length <= MAX_CHARS) { - formattedValue = value; + return value; } else { - formattedValue = ( + return ( - - {value} - + {value} ); } - - return formattedValue; -} +}; diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/index.js b/x-pack/legacy/plugins/ml/public/components/display_value/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/display_value/index.js rename to x-pack/legacy/plugins/ml/public/components/display_value/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_text.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_text.html index 5d28f05a9abe2..4ac8569510876 100644 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_text.html +++ b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_text.html @@ -30,7 +30,7 @@ >

+ +

+ +

+ + + +

+ +

+
+ + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/actions_panel/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/actions_panel/index.ts new file mode 100644 index 0000000000000..4e5ac41b2e4f6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/actions_panel/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 { ActionsPanel } from './actions_panel'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/_field_data_card.scss b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/_field_data_card.scss new file mode 100644 index 0000000000000..ca7d8e3f31c58 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/_field_data_card.scss @@ -0,0 +1,62 @@ +.mlFieldDataCard { + height: 420px; + width: 360px; + + + // Note the names of these styles need to match the type of the field they are displaying. + .boolean { + background-color: $euiColorVis5; + } + + .date { + background-color: $euiColorVis7; + } + + .document_count { + background-color: $euiColorVis2; + } + + .geo_point { + background-color: $euiColorVis8; + } + + .ip { + background-color: $euiColorVis3; + } + + .keyword { + background-color: $euiColorVis0; + } + + .number { + background-color: $euiColorVis1; + } + + .text { + background-color: $euiColorVis9; + } + + .type-other, .unknown { + background-color: $euiColorVis6; + } + + + // Use euiPanel styling + @include euiPanel($selector: 'mlFieldDataCard__content'); + + .mlFieldDataCard__content { + @include euiFontSizeS; + height: 385px; + border-radius: 0px 0px $euiBorderRadius $euiBorderRadius; + overflow: hidden; + } + + .mlFieldDataCard__codeContent { + font-family: $euiCodeFontFamily; + } + + .mlFieldDataCard__stats { + padding: $euiSizeS $euiSizeS 0px $euiSizeS; + text-align: center; + } +} diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/_index.scss b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/_index.scss new file mode 100644 index 0000000000000..4f21c29123e84 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/_index.scss @@ -0,0 +1 @@ +@import 'field_data_card'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/boolean_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/boolean_content.tsx new file mode 100644 index 0000000000000..bd24a52eb91e9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/boolean_content.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldDataCardProps } from '../field_data_card'; +// @ts-ignore +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; + +function getPercentLabel(valueCount: number, totalCount: number): string { + if (valueCount === 0) { + return '0%'; + } + + const percent = (100 * valueCount) / totalCount; + if (percent >= 0.1) { + return `${roundToDecimalPlace(percent, 1)}%`; + } else { + return '< 0.1%'; + } +} + +export const BooleanContent: FC = ({ config }) => { + const { stats } = config; + if (stats === undefined) { + return null; + } + + const { count, sampleCount, trueCount, falseCount } = stats; + const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); + + // TODO - display counts of true / false in an Elastic Charts bar chart (or Pie chart if available). + + return ( +
+
+ +   + +
+ + + +
+ + + + + + true + + + + + + + + {getPercentLabel(trueCount, count)} + + + + + + + + + + false + + + + + + + + {getPercentLabel(falseCount, count)} + + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/date_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/date_content.tsx new file mode 100644 index 0000000000000..75c3822c98629 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/date_content.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiIcon, EuiSpacer } from '@elastic/eui'; +// @ts-ignore +import { formatDate } from '@elastic/eui/lib/services/format'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldDataCardProps } from '../field_data_card'; +// @ts-ignore +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; + +const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS'; + +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 ( +
+
+ +   + +
+ + + +
+ +
+ + + +
+ +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/document_count_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/document_count_content.tsx new file mode 100644 index 0000000000000..a50f49df2fcd6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/document_count_content.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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 { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldDataCardProps } from '../field_data_card'; +import { DocumentCountChart, DocumentCountChartPoint } from '../document_count_chart'; + +const CHART_WIDTH = 325; +const CHART_HEIGHT = 350; + +export const DocumentCountContent: FC = ({ config }) => { + const { stats } = config; + if (stats === undefined) { + return null; + } + + const { documentCounts, timeRangeEarliest, timeRangeLatest } = stats; + + let chartPoints: DocumentCountChartPoint[] = []; + if (documentCounts !== undefined && documentCounts.buckets !== undefined) { + const buckets: Record = stats.documentCounts.buckets; + chartPoints = Object.entries(buckets).map(([time, value]) => ({ time: +time, value })); + } + + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/geo_point_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/geo_point_content.tsx new file mode 100644 index 0000000000000..f173cba03d0cd --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/geo_point_content.tsx @@ -0,0 +1,80 @@ +/* + * 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 { EuiIcon, EuiSpacer } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldDataCardProps } from '../field_data_card'; +// @ts-ignore +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; +import { ExamplesList } from '../examples_list'; + +export const GeoPointContent: FC = ({ config }) => { + // TODO - adjust server-side query to get examples using: + + // GET /filebeat-apache-2019.01.30/_search + // { + // "size":10, + // "_source": false, + // "docvalue_fields": ["source.geo.location"], + // "query": { + // "bool":{ + // "must":[ + // { + // "exists":{ + // "field":"source.geo.location" + // } + // } + // ] + // } + // } + // } + + const { stats } = config; + if (stats === undefined) { + return null; + } + + const { count, sampleCount, cardinality, examples } = stats; + const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); + + return ( +
+
+ +   + +
+ + + +
+ +   + +
+ + + + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/index.ts new file mode 100644 index 0000000000000..230be246eb4eb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { BooleanContent } from './boolean_content'; +export { DateContent } from './date_content'; +export { DocumentCountContent } from './document_count_content'; +export { GeoPointContent } from './geo_point_content'; +export { KeywordContent } from './keyword_content'; +export { IpContent } from './ip_content'; +export { NotInDocsContent } from './not_in_docs_content'; +export { NumberContent } from './number_content'; +export { OtherContent } from './other_content'; +export { TextContent } from './text_content'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/ip_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/ip_content.tsx new file mode 100644 index 0000000000000..59272e8df693a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/ip_content.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiIcon, EuiSpacer } from '@elastic/eui'; +// @ts-ignore +import { formatDate } from '@elastic/eui/lib/services/format'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldDataCardProps } from '../field_data_card'; +// @ts-ignore +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; +import { TopValues } from '../top_values'; + +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); + + return ( +
+
+ +   + +
+ + + +
+ +   + +
+ + + +
+ + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/keyword_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/keyword_content.tsx new file mode 100644 index 0000000000000..54749c8ccb318 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/keyword_content.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiIcon, EuiSpacer } from '@elastic/eui'; +// @ts-ignore +import { formatDate } from '@elastic/eui/lib/services/format'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldDataCardProps } from '../field_data_card'; +// @ts-ignore +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; +import { TopValues } from '../top_values'; + +export const KeywordContent: FC = ({ config }) => { + const { stats, fieldFormat } = config; + if (stats === undefined) { + return null; + } + + const { count, sampleCount, cardinality } = stats; + const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); + + return ( +
+
+ +   + +
+ + + +
+ +   + +
+ + + +
+ + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/not_in_docs_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/not_in_docs_content.tsx new file mode 100644 index 0000000000000..34acf3b6c388f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/not_in_docs_content.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 React, { FC, Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +export const NotInDocsContent: FC = () => ( + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/number_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/number_content.tsx new file mode 100644 index 0000000000000..7134e43e4bc28 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/number_content.tsx @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSelect, EuiSpacer, EuiText } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +// @ts-ignore +import { ordinalSuffix } from 'ui/utils/ordinal_suffix'; + +import { FieldDataCardProps } from '../field_data_card'; +import { DisplayValue } from '../../../../components/display_value'; +import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format'; +// @ts-ignore +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; +import { + MetricDistributionChart, + MetricDistributionChartData, + buildChartDataFromStats, +} from '../metric_distribution_chart'; +import { TopValues } from '../top_values'; + +enum DETAILS_MODE { + DISTRIBUTION = 'distribution', + TOP_VALUES = 'top_values', +} + +const METRIC_DISTRIBUTION_CHART_WIDTH = 325; +const METRIC_DISTRIBUTION_CHART_HEIGHT = 210; +const DEFAULT_TOP_VALUES_THRESHOLD = 100; + +export const NumberContent: FC = ({ config }) => { + const { stats, fieldFormat } = config; + if (stats === undefined) { + return null; + } + + useEffect(() => { + 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 + ? DETAILS_MODE.TOP_VALUES + : DETAILS_MODE.DISTRIBUTION + ); + + const defaultChartData: MetricDistributionChartData[] = []; + const [distributionChartData, setDistributionChartData] = useState(defaultChartData); + + const detailsOptions = [ + { + value: DETAILS_MODE.DISTRIBUTION, + text: i18n.translate('xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel', { + defaultMessage: 'distribution of values', + }), + }, + { + value: DETAILS_MODE.TOP_VALUES, + text: i18n.translate('xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel', { + defaultMessage: 'top values', + }), + }, + ]; + + return ( +
+
+ +   + +
+ + + +
+ +   + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setDetailsMode(e.target.value as DETAILS_MODE)} + style={{ width: '200px' }} + aria-label={i18n.translate( + 'xpack.ml.fieldDataCard.cardNumber.selectMetricDetailsDisplayAriaLabel', + { + defaultMessage: 'Select display option for metric details', + } + )} + data-test-subj="mlFieldDataCardNumberDetailsSelect" + /> + + + + + + {detailsMode === DETAILS_MODE.DISTRIBUTION && ( + + + + + + + + + + + + + + + )} + + {detailsMode === DETAILS_MODE.TOP_VALUES && ( + + + + + + )} +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/other_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/other_content.tsx new file mode 100644 index 0000000000000..2279205f21768 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/other_content.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldDataCardProps } from '../field_data_card'; +// @ts-ignore +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; +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; + const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); + + return ( +
+
+ + + +
+ {aggregatable === true && ( + + +
+ +   + +
+ + + +
+ +   + +
+
+ )} + + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/text_content.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/text_content.tsx new file mode 100644 index 0000000000000..81fff60960a8d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/content_types/text_content.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { FieldDataCardProps } from '../field_data_card'; +import { ExamplesList } from '../examples_list'; + +export const TextContent: FC = ({ config }) => { + const { stats } = config; + if (stats === undefined) { + return null; + } + + const { examples } = stats; + const numExamples = examples.length; + + return ( +
+ {numExamples > 0 && } + {numExamples === 0 && ( + + + + _source, + }} + /> + + + + copy_to, + sourceParam: _source, + includesParam: includes, + excludesParam: excludes, + }} + /> + + + )} +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/document_count_chart/document_count_chart.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/document_count_chart/document_count_chart.tsx new file mode 100644 index 0000000000000..c760985d7ee41 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/document_count_chart/document_count_chart.tsx @@ -0,0 +1,99 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { + Axis, + BarSeries, + Chart, + DataSeriesColorsValues, + getAxisId, + getSpecId, + niceTimeFormatter, + Position, + ScaleType, + 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 { useUiChromeContext } from '../../../../contexts/ui/use_ui_chrome_context'; + +export interface DocumentCountChartPoint { + time: number | string; + value: number; +} + +interface Props { + width: number; + height: number; + chartPoints: DocumentCountChartPoint[]; + timeRangeEarliest: number; + timeRangeLatest: number; +} + +const SPEC_ID = 'document_count'; + +export const DocumentCountChart: FC = ({ + width, + height, + chartPoints, + timeRangeEarliest, + timeRangeLatest, +}) => { + const seriesName = i18n.translate('xpack.ml.fieldDataCard.documentCountChart.seriesLabel', { + defaultMessage: 'document count', + }); + + const xDomain = { + min: timeRangeEarliest, + max: timeRangeLatest, + }; + + const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); + + // TODO - switch to use inline areaSeriesStyle to set series fill once charts version is 8.0.0+ + const IS_DARK_THEME = useUiChromeContext() + .getUiSettingsClient() + .get('theme:darkMode'); + const themeName = IS_DARK_THEME ? darkTheme : lightTheme; + const EVENT_RATE_COLOR = themeName.euiColorVis2; + const barSeriesColorValues: DataSeriesColorsValues = { + colorValues: [], + specId: getSpecId(SPEC_ID), + }; + const seriesColors = new Map([[barSeriesColorValues, EVENT_RATE_COLOR]]); + + return ( +
+ + + + + 0 ? chartPoints : [{ time: timeRangeEarliest, value: 0 }]} + customSeriesColors={seriesColors} + /> + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/document_count_chart/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/document_count_chart/index.ts new file mode 100644 index 0000000000000..26d004af38f67 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/document_count_chart/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 { DocumentCountChart, DocumentCountChartPoint } from './document_count_chart'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/example.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/example.tsx new file mode 100644 index 0000000000000..29fe690f4a43b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/example.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; + +interface Props { + example: string | object; +} + +export const Example: FC = ({ example }) => { + const exampleStr = typeof example === 'string' ? example : JSON.stringify(example); + + // Use 95% width for each example so that the truncation ellipses show up when + // wrapped inside a tooltip. + return ( + + + + + {exampleStr} + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/examples_list.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/examples_list.tsx new file mode 100644 index 0000000000000..d4a662a3a6dab --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/examples_list.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiSpacer, EuiText } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { Example } from './example'; + +interface Props { + examples: Array; +} + +export const ExamplesList: FC = ({ examples }) => { + if (examples === undefined || examples === null || examples.length === 0) { + return null; + } + + const examplesContent = examples.map((example, i) => { + return ; + }); + + return ( +
+ + + + + {examplesContent} +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/index.ts new file mode 100644 index 0000000000000..5682694a1cba3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/examples_list/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 { ExamplesList } from './examples_list'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/field_data_card.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/field_data_card.tsx new file mode 100644 index 0000000000000..c81ccf02929fe --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/field_data_card.tsx @@ -0,0 +1,81 @@ +/* + * 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 { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; + +import { FieldVisConfig } from '../../common'; +// @ts-ignore +import { FieldTitleBar } from '../../../components/field_title_bar'; +import { + BooleanContent, + DateContent, + DocumentCountContent, + GeoPointContent, + IpContent, + KeywordContent, + NotInDocsContent, + NumberContent, + OtherContent, + TextContent, +} from './content_types'; +import { LoadingIndicator } from './loading_indicator'; + +export interface FieldDataCardProps { + config: FieldVisConfig; +} + +export const FieldDataCard: FC = ({ config }) => { + const { fieldName, loading, type, existsInDocs } = config; + + function getCardContent() { + if (existsInDocs === false) { + return ; + } + + switch (type) { + case ML_JOB_FIELD_TYPES.NUMBER: + if (fieldName !== undefined) { + return ; + } else { + 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/legacy/plugins/ml/public/data_visualizer/components/field_data_card/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/index.ts new file mode 100644 index 0000000000000..9b7939c90c71d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/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 { FieldDataCard, FieldDataCardProps } from './field_data_card'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/loading_indicator/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/loading_indicator/index.ts new file mode 100644 index 0000000000000..0e4613bcdccca --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/loading_indicator/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 { LoadingIndicator } from './loading_indicator'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/loading_indicator/loading_indicator.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/loading_indicator/loading_indicator.tsx new file mode 100644 index 0000000000000..51a1093b4c409 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/loading_indicator/loading_indicator.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +export const LoadingIndicator: FC = () => ( + + + + + + + + + + + + + + + +); diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/index.ts new file mode 100644 index 0000000000000..2e319dc810e24 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MetricDistributionChart, MetricDistributionChartData } from './metric_distribution_chart'; +export { buildChartDataFromStats } from './metric_distribution_chart_data_builder'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx new file mode 100644 index 0000000000000..617103eafc523 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx @@ -0,0 +1,145 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { + AreaSeries, + Axis, + Chart, + CurveType, + DataSeriesColorsValues, + getAxisId, + getSpecId, + Position, + ScaleType, + Settings, + TooltipValueFormatter, + TooltipValue, +} from '@elastic/charts'; + +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; + +import { MetricDistributionChartTooltipHeader } from './metric_distribution_chart_tooltip_header'; +import { useUiChromeContext } from '../../../../contexts/ui/use_ui_chrome_context'; +import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format'; + +export interface MetricDistributionChartData { + x: number; + y: number; + dataMin: number; + dataMax: number; + percent: number; +} + +interface Props { + width: number; + height: number; + chartData: MetricDistributionChartData[]; + fieldFormat?: any; // Kibana formatter for field being viewed +} + +const SPEC_ID = 'metric_distribution'; + +export const MetricDistributionChart: FC = ({ width, height, chartData, fieldFormat }) => { + // 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. + const seriesName = i18n.translate('xpack.ml.fieldDataCard.metricDistributionChart.seriesName', { + defaultMessage: 'distribution', + }); + + // TODO - switch to use inline areaSeriesStyle to set series fill once charts version is 8.0.0+ + const IS_DARK_THEME = useUiChromeContext() + .getUiSettingsClient() + .get('theme:darkMode'); + const themeName = IS_DARK_THEME ? darkTheme : lightTheme; + const EVENT_RATE_COLOR = themeName.euiColorVis1; + const seriesColorValues: DataSeriesColorsValues = { + colorValues: [], + specId: getSpecId(SPEC_ID), + }; + const seriesColors = new Map([[seriesColorValues, EVENT_RATE_COLOR]]); + + const headerFormatter: TooltipValueFormatter = (tooltipData: TooltipValue) => { + const xValue = tooltipData.value; + const chartPoint: MetricDistributionChartData | undefined = chartData.find( + data => data.x === xValue + ); + + return ( + + ); + }; + + return ( +
+ + + kibanaFieldFormat(d, fieldFormat)} + /> + d.toFixed(3)} + hide={true} + /> + 0 ? chartData : [{ x: 0, y: 0 }]} + curve={CurveType.CURVE_STEP_AFTER} + // TODO - switch to use inline areaSeriesStyle to set series fill once charts version is 8.0.0+ + // areaSeriesStyle={{ + // line: { + // stroke: 'red', + // strokeWidth: 0, + // visible: false, + // }, + // point: { + // visible: false, + // radius: 0, + // opacity: 0, + // }, + // area: { fill: 'red', visible: true, opacity: 1 }, + // }} + customSeriesColors={seriesColors} + /> + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx new file mode 100644 index 0000000000000..ede9f1edc1223 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx @@ -0,0 +1,155 @@ +/* + * 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. + */ + +const METRIC_DISTRIBUTION_CHART_MIN_BAR_WIDTH = 3; // Minimum bar width, in pixels. +const METRIC_DISTRIBUTION_CHART_MAX_BAR_HEIGHT_FACTOR = 20; // Max bar height relative to median bar height. + +import { MetricDistributionChartData } from './metric_distribution_chart'; + +interface DistributionPercentile { + minValue: number; + maxValue: number; + percent: number; +} + +interface DistributionChartBar { + x0: number; + x1: number; + y: number; + dataMin: number; + dataMax: number; + percent: number; + isMinWidth: boolean; +} + +export function buildChartDataFromStats( + stats: any, + chartWidth: number +): MetricDistributionChartData[] { + // Process the raw percentiles data so it is in a suitable format for plotting in the metric distribution chart. + let chartData: MetricDistributionChartData[] = []; + + const distribution = stats.distribution; + if (distribution === undefined) { + return chartData; + } + + const percentiles: DistributionPercentile[] = distribution.percentiles; + if (percentiles.length === 0) { + return chartData; + } + + // Adjust x axis min and max if there is a single bar. + const minX = percentiles[0].minValue; + const maxX = percentiles[percentiles.length - 1].maxValue; + + let xAxisMin: number = minX; + let xAxisMax: number = maxX; + if (maxX === minX) { + if (minX !== 0) { + xAxisMin = 0; + xAxisMax = 2 * minX; + } else { + xAxisMax = 1; + } + } + + // Adjust the right hand x coordinates so that each bar is at least METRIC_DISTRIBUTION_CHART_MIN_BAR_WIDTH. + const minBarWidth = + (METRIC_DISTRIBUTION_CHART_MIN_BAR_WIDTH / chartWidth) * (xAxisMax - xAxisMin); + const processedData: DistributionChartBar[] = []; + let lastBar: DistributionChartBar; + percentiles.forEach((data, index) => { + if (index === 0) { + const bar: DistributionChartBar = { + x0: data.minValue, + x1: Math.max(data.minValue + minBarWidth, data.maxValue), + y: 0, // Set below + dataMin: data.minValue, + dataMax: data.maxValue, + percent: data.percent, + isMinWidth: false, + }; + + // Scale the height of the bar according to the range of data values in the bar. + bar.y = + (data.percent / (bar.x1 - bar.x0)) * + Math.max(1, minBarWidth / Math.max(data.maxValue - data.minValue, 0.5 * minBarWidth)); + bar.isMinWidth = data.maxValue <= data.minValue + minBarWidth; + processedData.push(bar); + lastBar = bar; + } else { + if (lastBar.isMinWidth === false || data.maxValue > lastBar.x1) { + const bar = { + x0: lastBar.x1, + x1: Math.max(lastBar.x1 + minBarWidth, data.maxValue), + y: 0, // Set below + dataMin: data.minValue, + dataMax: data.maxValue, + percent: data.percent, + isMinWidth: false, + }; + + // Scale the height of the bar according to the range of data values in the bar. + bar.y = + (data.percent / (bar.x1 - bar.x0)) * + Math.max(1, minBarWidth / Math.max(data.maxValue - data.minValue, 0.5 * minBarWidth)); + bar.isMinWidth = data.maxValue <= lastBar.x1 + minBarWidth; + processedData.push(bar); + lastBar = bar; + } else { + // Combine bars which are less than minBarWidth apart. + lastBar.percent = lastBar.percent + data.percent; + lastBar.y = lastBar.percent / (lastBar.x1 - lastBar.x0); + lastBar.dataMax = data.maxValue; + } + } + }); + + if (maxX !== minX) { + xAxisMax = processedData[processedData.length - 1].x1; + } + + // Adjust the maximum bar height to be (METRIC_DISTRIBUTION_CHART_MAX_BAR_HEIGHT_FACTOR * median bar height). + let barHeights = processedData.map(data => data.y); + barHeights = barHeights.sort((a, b) => a - b); + + let maxBarHeight = 0; + const processedDataLength = processedData.length; + if (Math.abs(processedDataLength % 2) === 1) { + maxBarHeight = + METRIC_DISTRIBUTION_CHART_MAX_BAR_HEIGHT_FACTOR * + barHeights[Math.floor(processedDataLength / 2)]; + } else { + maxBarHeight = + (METRIC_DISTRIBUTION_CHART_MAX_BAR_HEIGHT_FACTOR * + (barHeights[Math.floor(processedDataLength / 2) - 1] + + barHeights[Math.floor(processedDataLength / 2)])) / + 2; + } + + processedData.forEach(data => { + data.y = Math.min(data.y, maxBarHeight); + }); + + // Convert the data to the format used by the chart. + chartData = processedData.map(data => { + const { x0, y, dataMin, dataMax, percent } = data; + return { x: x0, y, dataMin, dataMax, percent }; + }); + + // Add a final point to drop the curve back to the y axis. + const last = processedData[processedData.length - 1]; + chartData.push({ + x: last.x1, + y: 0, + dataMin: last.dataMin, + dataMax: last.dataMax, + percent: last.percent, + }); + + return chartData; +} diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx new file mode 100644 index 0000000000000..449964f932858 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx @@ -0,0 +1,53 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; + +import { MetricDistributionChartData } from './metric_distribution_chart'; +import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format'; + +interface Props { + chartPoint: MetricDistributionChartData | undefined; + maxWidth: number; + fieldFormat?: any; // Kibana formatter for field being viewed +} + +export const MetricDistributionChartTooltipHeader: FC = ({ + chartPoint, + maxWidth, + fieldFormat, +}) => { + if (chartPoint === undefined) { + return null; + } + + return ( +
+ {chartPoint.dataMax > chartPoint.dataMin ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/top_values/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/top_values/index.ts new file mode 100644 index 0000000000000..9e1cf53b5d463 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/top_values/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 { TopValues } from './top_values'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/top_values/top_values.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/top_values/top_values.tsx new file mode 100644 index 0000000000000..38d6f2f8485e3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_data_card/top_values/top_values.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { kibanaFieldFormat } from '../../../../formatters/kibana_field_format'; +// @ts-ignore +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; + +interface Props { + stats: any; + fieldFormat?: any; + barColor?: 'primary' | 'secondary' | 'danger' | 'subdued' | 'accent'; +} + +function getPercentLabel(docCount: number, topValuesSampleSize: number): string { + const percent = (100 * docCount) / topValuesSampleSize; + if (percent >= 0.1) { + return `${roundToDecimalPlace(percent, 1)}%`; + } else { + return '< 0.1%'; + } +} + +export const TopValues: FC = ({ stats, fieldFormat, barColor }) => { + const { + topValues, + topValuesSampleSize, + topValuesSamplerShardSize, + count, + isTopValuesSampled, + } = stats; + const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count; + + return ( + + {topValues.map((value: any) => ( + + + + + {kibanaFieldFormat(value.key, fieldFormat)} + + + + + + + + + {getPercentLabel(value.doc_count, progressBarMax)} + + + + ))} + {isTopValuesSampled === true && ( + + + + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_types_select/field_types_select.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_types_select/field_types_select.tsx new file mode 100644 index 0000000000000..a068f83fa2c1b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_types_select/field_types_select.tsx @@ -0,0 +1,57 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { EuiSelect } from '@elastic/eui'; + +import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; + +interface Props { + fieldTypes: ML_JOB_FIELD_TYPES[]; + selectedFieldType: ML_JOB_FIELD_TYPES | '*'; + setSelectedFieldType(t: ML_JOB_FIELD_TYPES | '*'): void; +} + +export const FieldTypesSelect: FC = ({ + 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/legacy/plugins/ml/public/data_visualizer/components/field_types_select/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_types_select/index.ts new file mode 100644 index 0000000000000..212299caf14ab --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/field_types_select/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 { FieldTypesSelect } from './field_types_select'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/fields_panel/fields_panel.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/fields_panel/fields_panel.tsx new file mode 100644 index 0000000000000..069572c685ebf --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/fields_panel/fields_panel.tsx @@ -0,0 +1,169 @@ +/* + * 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 { + EuiCheckbox, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + // @ts-ignore + EuiSearchBar, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { toastNotifications } from 'ui/notify'; + +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 }; +} + +export const FieldsPanel: FC = ({ + title, + totalFieldCount, + populatedFieldCount, + showAllFields, + setShowAllFields, + fieldTypes, + showFieldType, + setShowFieldType, + fieldSearchBarQuery, + setFieldSearchBarQuery, + fieldVisConfigs, +}) => { + function onShowAllFieldsChange() { + setShowAllFields(!showAllFields); + } + + function onSearchBarChange(query: SearchBarQuery) { + if (query.error) { + toastNotifications.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}

+
+ + + + + {showAllFields === true ? ( + {fieldVisConfigs.length}, + populatedFieldCount, + wrappedPopulatedFieldCount: {populatedFieldCount}, + }} + /> + ) : ( + {fieldVisConfigs.length}, + wrappedTotalFieldCount: {totalFieldCount}, + }} + /> + )} + + + {populatedFieldCount < totalFieldCount && ( + + + + )} + + + + + + {typeof setShowFieldType === 'function' && ( + + + + )} + + + + + + {fieldVisConfigs.map((visConfig, i) => ( + + + + ))} + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/fields_panel/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/fields_panel/index.ts new file mode 100644 index 0000000000000..3b933af03d982 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/fields_panel/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 { FieldsPanel } from './fields_panel'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/search_panel/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/components/search_panel/index.ts new file mode 100644 index 0000000000000..fda1159934d24 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/search_panel/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 { SearchPanel } from './search_panel'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/components/search_panel/search_panel.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/components/search_panel/search_panel.tsx new file mode 100644 index 0000000000000..342888bd18fba --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/components/search_panel/search_panel.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { + EuiFieldSearch, + EuiFlexItem, + EuiFlexGroup, + EuiForm, + EuiFormRow, + EuiIconTip, + EuiPanel, + EuiSelect, + EuiSpacer, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { IndexPattern } from 'ui/index_patterns'; + +import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; +import { SavedSearchQuery } from '../../../contexts/kibana'; + +// @ts-ignore +import { KqlFilterBar } from '../../../components/kql_filter_bar'; + +interface Props { + indexPattern: IndexPattern; + searchString: string | SavedSearchQuery; + setSearchString(s: string): void; + searchQuery: string | SavedSearchQuery; + setSearchQuery(q: string | SavedSearchQuery): void; + searchQueryLanguage: SEARCH_QUERY_LANGUAGE; + samplerShardSize: number; + setSamplerShardSize(s: number): void; + totalCount: number; +} + +export const SearchPanel: FC = ({ + indexPattern, + searchString, + setSearchString, + searchQuery, + setSearchQuery, + searchQueryLanguage, + samplerShardSize, + setSamplerShardSize, + totalCount, +}) => { + const searchAllOptionText = i18n.translate('xpack.ml.datavisualizer.searchPanel.allOptionLabel', { + defaultMessage: 'all', + }); + + const searchSizeOptions = [ + { value: '1000', text: '1000' }, + { value: '5000', text: '5000' }, + { value: '10000', text: '10000' }, + { value: '100000', text: '100000' }, + { value: '-1', text: searchAllOptionText }, + ]; + + const searchHandler = (d: Record) => { + setSearchQuery(d.filterQuery); + }; + + return ( + + {searchQueryLanguage === SEARCH_QUERY_LANGUAGE.KUERY ? ( + + ) : ( + + + + + + )} + + + + + + + setSamplerShardSize(+e.target.value)} + aria-label={i18n.translate('xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel', { + defaultMessage: 'Select number of documents to sample', + })} + data-test-subj="mlDataVisualizerShardSizeSelect" + /> + + + + {totalCount}, + totalCount, + }} + /> + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/data_loader/data_loader.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/data_loader/data_loader.ts new file mode 100644 index 0000000000000..b289c472528bc --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/data_loader/data_loader.ts @@ -0,0 +1,135 @@ +/* + * 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. + */ + +// @ts-ignore +import { decorateQuery, luceneStringToDsl } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; + +import { toastNotifications } from 'ui/notify'; +import { IndexPattern } from 'ui/index_patterns'; + +import { SavedSearchQuery } from '../../contexts/kibana'; +import { IndexPatternTitle } from '../../../common/types/kibana'; + +import { ml } from '../../services/ml_api_service'; +import { FieldRequestConfig } from '../common'; + +// List of system fields we don't want to display. +const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; +// Maximum number of examples to obtain for text type fields. +const MAX_EXAMPLES_DEFAULT: number = 10; + +export class DataLoader { + private _indexPattern: IndexPattern; + private _indexPatternTitle: IndexPatternTitle = ''; + private _maxExamples: number = MAX_EXAMPLES_DEFAULT; + + constructor(indexPattern: IndexPattern, kibanaConfig: any) { + this._indexPattern = indexPattern; + this._indexPatternTitle = indexPattern.title; + } + + async loadOverallData( + query: string | SavedSearchQuery, + samplerShardSize: number, + earliest: number | undefined, + latest: number | undefined + ): Promise { + const aggregatableFields: string[] = []; + const nonAggregatableFields: string[] = []; + this._indexPattern.fields.forEach(field => { + const fieldName = field.displayName !== undefined ? field.displayName : field.name; + if (this.isDisplayField(fieldName) === true) { + if (field.aggregatable === true) { + aggregatableFields.push(fieldName); + } else { + nonAggregatableFields.push(fieldName); + } + } + }); + + // Need to find: + // 1. List of aggregatable fields that do exist in docs + // 2. List of aggregatable fields that do not exist in docs + // 3. List of non-aggregatable fields that do exist in docs. + // 4. List of non-aggregatable fields that do not exist in docs. + const stats = await ml.getVisualizerOverallStats({ + indexPatternTitle: this._indexPatternTitle, + query, + timeFieldName: this._indexPattern.timeFieldName, + samplerShardSize, + earliest, + latest, + aggregatableFields, + nonAggregatableFields, + }); + + return stats; + } + + async loadFieldStats( + query: string | SavedSearchQuery, + samplerShardSize: number, + earliest: number | undefined, + latest: number | undefined, + fields: FieldRequestConfig[], + interval?: string + ): Promise { + const stats = await ml.getVisualizerFieldStats({ + indexPatternTitle: this._indexPatternTitle, + query, + timeFieldName: this._indexPattern.timeFieldName, + earliest, + latest, + samplerShardSize, + interval, + fields, + maxExamples: this._maxExamples, + }); + + return stats; + } + + displayError(err: any) { + if (err.statusCode === 500) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.datavisualizer.dataLoader.internalServerErrorMessage', { + defaultMessage: + 'Error loading data in index {index}. {message}. ' + + 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', + values: { + index: this._indexPattern.title, + message: err.message, + }, + }) + ); + } else { + toastNotifications.addDanger( + i18n.translate('xpack.ml.datavisualizer.page.errorLoadingDataMessage', { + defaultMessage: 'Error loading data in index {index}. {message}', + values: { + index: this._indexPattern.title, + message: err.message, + }, + }) + ); + } + } + + public set maxExamples(max: number) { + this._maxExamples = max; + } + + public get maxExamples(): number { + return this._maxExamples; + } + + // Returns whether the field with the specified name should be displayed, + // as certain fields such as _id and _source should be omitted from the view. + public isDisplayField(fieldName: string): boolean { + return !OMIT_FIELDS.includes(fieldName); + } +} diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/data_loader/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/data_loader/index.ts new file mode 100644 index 0000000000000..ed244362e6210 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/data_loader/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 { DataLoader } from './data_loader'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/directive.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/directive.tsx new file mode 100644 index 0000000000000..2fe4d5201f04f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/directive.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 from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { I18nContext } from 'ui/i18n'; +import { IndexPatterns } from 'ui/index_patterns'; +import { IPrivate } from 'ui/private'; +import { InjectorService } from '../../common/types/angular'; + +import { KibanaConfigTypeFix, KibanaContext } from '../contexts/kibana/kibana_context'; +import { SearchItemsProvider } from '../jobs/new_job/utils/new_job_utils'; + +import { Page } from './page'; + +module.directive('mlDataVisualizer', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + const indexPatterns = $injector.get('indexPatterns'); + const kbnBaseUrl = $injector.get('kbnBaseUrl'); + const kibanaConfig = $injector.get('config'); + const Private = $injector.get('Private'); + + const createSearchItems = Private(SearchItemsProvider); + const { indexPattern, savedSearch, combinedQuery } = createSearchItems(); + + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kbnBaseUrl, + kibanaConfig, + }; + + ReactDOM.render( + + + + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/index.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/index.ts new file mode 100644 index 0000000000000..8ef2e327a8984 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './directive'; +import './route'; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/page.tsx b/x-pack/legacy/plugins/ml/public/data_visualizer/page.tsx new file mode 100644 index 0000000000000..13d153a33defd --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/page.tsx @@ -0,0 +1,672 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useEffect, useState } from 'react'; + +// @ts-ignore +import { decorateQuery, luceneStringToDsl } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; + +import { FieldType } from 'ui/index_patterns'; +import { timefilter } from 'ui/timefilter'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { KBN_FIELD_TYPES, ML_JOB_FIELD_TYPES } from '../../common/constants/field_types'; +import { SEARCH_QUERY_LANGUAGE } from '../../common/constants/search'; +// @ts-ignore +import { isFullLicense } from '../license/check_license'; +import { FullTimeRangeSelector } from '../components/full_time_range_selector'; +import { useKibanaContext, SavedSearchQuery } from '../contexts/kibana'; +// @ts-ignore +import { kbnTypeToMLJobType } from '../util/field_types_utils'; +// @ts-ignore +import { timeBasedIndexCheck } from '../util/index_utils'; +// @ts-ignore +import { MlTimeBuckets } from '../util/ml_time_buckets'; +import { FieldRequestConfig, FieldVisConfig } from './common'; +import { ActionsPanel } from './components/actions_panel'; +import { FieldsPanel } from './components/fields_panel'; +import { SearchPanel } from './components/search_panel'; +import { DataLoader } from './data_loader'; + +interface DataVisualizerPageState { + searchQuery: string | SavedSearchQuery; + searchString: string | SavedSearchQuery; + searchQueryLanguage: SEARCH_QUERY_LANGUAGE; + samplerShardSize: number; + overallStats: any; + metricConfigs: FieldVisConfig[]; + totalMetricFieldCount: number; + populatedMetricFieldCount: number; + showAllMetrics: boolean; + metricFieldQuery?: string; + nonMetricConfigs: FieldVisConfig[]; + totalNonMetricFieldCount: number; + populatedNonMetricFieldCount: number; + showAllNonMetrics: boolean; + nonMetricShowFieldType: ML_JOB_FIELD_TYPES | '*'; + nonMetricFieldQuery?: string; +} + +const defaultSearchQuery = { + match_all: {}, +}; + +function getDefaultPageState(): DataVisualizerPageState { + return { + searchString: '', + searchQuery: defaultSearchQuery, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + samplerShardSize: 5000, + overallStats: { + totalCount: 0, + aggregatableExistsFields: [], + aggregatableNotExistsFields: [], + nonAggregatableExistsFields: [], + nonAggregatableNotExistsFields: [], + }, + metricConfigs: [], + totalMetricFieldCount: 0, + populatedMetricFieldCount: 0, + showAllMetrics: false, + nonMetricConfigs: [], + totalNonMetricFieldCount: 0, + populatedNonMetricFieldCount: 0, + showAllNonMetrics: false, + nonMetricShowFieldType: '*', + }; +} + +export const Page: FC = () => { + const kibanaContext = useKibanaContext(); + + const { combinedQuery, currentIndexPattern, currentSavedSearch, kibanaConfig } = kibanaContext; + + const dataLoader = new DataLoader(currentIndexPattern, kibanaConfig); + + useEffect(() => { + if (currentIndexPattern.timeFieldName !== undefined) { + timefilter.enableTimeRangeSelector(); + } else { + timefilter.disableTimeRangeSelector(); + } + + timefilter.enableAutoRefreshSelector(); + timeBasedIndexCheck(currentIndexPattern, true); + }, []); + + // Obtain the list of non metric field types which appear in the index pattern. + let indexedFieldTypes: ML_JOB_FIELD_TYPES[] = []; + const indexPatternFields: FieldType[] = currentIndexPattern.fields; + indexPatternFields.forEach(field => { + if (field.scripted !== true) { + const dataVisualizerType: ML_JOB_FIELD_TYPES = kbnTypeToMLJobType(field); + if ( + dataVisualizerType !== undefined && + !indexedFieldTypes.includes(dataVisualizerType) && + dataVisualizerType !== ML_JOB_FIELD_TYPES.NUMBER + ) { + indexedFieldTypes.push(dataVisualizerType); + } + } + }); + indexedFieldTypes = indexedFieldTypes.sort(); + + const defaults = getDefaultPageState(); + + const [showActionsPanel] = useState( + isFullLicense() && currentIndexPattern.timeFieldName !== undefined + ); + + const [searchString, setSearchString] = useState(defaults.searchString); + const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); + const [searchQueryLanguage, setSearchQueryLanguage] = useState(defaults.searchQueryLanguage); + const [samplerShardSize, setSamplerShardSize] = useState(defaults.samplerShardSize); + + // TODO - type overallStats and stats + const [overallStats, setOverallStats] = useState(defaults.overallStats); + + 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 [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); + + useEffect(() => { + timefilter.on('timeUpdate', loadOverallStats); + return () => { + timefilter.off('timeUpdate', loadOverallStats); + }; + }, []); + + useEffect(() => { + // Check for a saved search being passed in. + const searchSource = currentSavedSearch.searchSource; + const query = searchSource.getField('query'); + if (query !== undefined) { + const queryLanguage = query.language; + const qryString = query.query; + let qry; + if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { + qry = { + query_string: { + query: qryString, + default_operator: 'AND', + }, + }; + } else { + qry = luceneStringToDsl(qryString); + decorateQuery(qry, kibanaConfig.get('query:queryString:options')); + } + + setSearchQuery(qry); + setSearchString(qryString); + setSearchQueryLanguage(queryLanguage); + } + }, []); + + useEffect(() => { + loadOverallStats(); + }, [searchQuery, samplerShardSize]); + + useEffect(() => { + createMetricCards(); + createNonMetricCards(); + }, [overallStats]); + + useEffect(() => { + loadMetricFieldStats(); + }, [metricConfigs]); + + useEffect(() => { + loadNonMetricFieldStats(); + }, [nonMetricConfigs]); + + useEffect(() => { + createMetricCards(); + }, [showAllMetrics, metricFieldQuery]); + + useEffect(() => { + createNonMetricCards(); + }, [showAllNonMetrics, nonMetricShowFieldType, nonMetricFieldQuery]); + + async function loadOverallStats() { + const tf = timefilter as any; + let earliest; + let latest; + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = tf.getActiveBounds().min.valueOf(); + latest = tf.getActiveBounds().max.valueOf(); + } + + try { + const allStats = await dataLoader.loadOverallData( + searchQuery, + samplerShardSize, + earliest, + latest + ); + setOverallStats(allStats); + } catch (err) { + dataLoader.displayError(err); + } + } + + async function loadMetricFieldStats() { + // Only request data for fields that exist in documents. + if (metricConfigs.length === 0) { + return; + } + + const configsToLoad = metricConfigs.filter( + config => config.existsInDocs === true && config.loading === true + ); + if (configsToLoad.length === 0) { + return; + } + + // Pass the field name, type and cardinality in the request. + // Top values will be obtained on a sample if cardinality > 100000. + const existMetricFields: FieldRequestConfig[] = configsToLoad.map(config => { + const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; + if (config.stats !== undefined && config.stats.cardinality !== undefined) { + props.cardinality = config.stats.cardinality; + } + return props; + }); + + // Obtain the interval to use for date histogram aggregations + // (such as the document count chart). Aim for 75 bars. + const buckets = new MlTimeBuckets(); + + const tf = timefilter as any; + let earliest: number | undefined; + let latest: number | undefined; + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = tf.getActiveBounds().min.valueOf(); + latest = tf.getActiveBounds().max.valueOf(); + } + + const bounds = tf.getActiveBounds(); + const BAR_TARGET = 75; + buckets.setInterval('auto'); + buckets.setBounds(bounds); + buckets.setBarTarget(BAR_TARGET); + const aggInterval = buckets.getInterval(); + + try { + const metricFieldStats = await dataLoader.loadFieldStats( + searchQuery, + samplerShardSize, + earliest, + latest, + existMetricFields, + aggInterval.expression + ); + + // Add the metric stats to the existing stats in the corresponding config. + const configs: FieldVisConfig[] = []; + metricConfigs.forEach(config => { + const configWithStats = { ...config }; + if (config.fieldName !== undefined) { + configWithStats.stats = { + ...configWithStats.stats, + ...metricFieldStats.find( + (fieldStats: any) => fieldStats.fieldName === config.fieldName + ), + }; + } 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; + } + configWithStats.loading = false; + configs.push(configWithStats); + }); + + setMetricConfigs(configs); + } catch (err) { + dataLoader.displayError(err); + } + } + + async function loadNonMetricFieldStats() { + // Only request data for fields that exist in documents. + if (nonMetricConfigs.length === 0) { + return; + } + + const configsToLoad = nonMetricConfigs.filter( + config => config.existsInDocs === true && config.loading === true + ); + if (configsToLoad.length === 0) { + return; + } + + // Pass the field name, type and cardinality in the request. + // Top values will be obtained on a sample if cardinality > 100000. + const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map(config => { + const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; + if (config.stats !== undefined && config.stats.cardinality !== undefined) { + props.cardinality = config.stats.cardinality; + } + return props; + }); + + const tf = timefilter as any; + let earliest; + let latest; + if (currentIndexPattern.timeFieldName !== undefined) { + earliest = tf.getActiveBounds().min.valueOf(); + latest = tf.getActiveBounds().max.valueOf(); + } + + try { + const nonMetricFieldStats = await dataLoader.loadFieldStats( + searchQuery, + samplerShardSize, + earliest, + latest, + existNonMetricFields + ); + + // Add the field stats to the existing stats in the corresponding config. + const configs: FieldVisConfig[] = []; + nonMetricConfigs.forEach(config => { + const configWithStats = { ...config }; + if (config.fieldName !== undefined) { + configWithStats.stats = { + ...configWithStats.stats, + ...nonMetricFieldStats.find( + (fieldStats: any) => fieldStats.fieldName === config.fieldName + ), + }; + } + configWithStats.loading = false; + configs.push(configWithStats); + }); + + setNonMetricConfigs(configs); + } catch (err) { + dataLoader.displayError(err); + } + } + + function createMetricCards() { + const configs: FieldVisConfig[] = []; + const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; + + let 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; + }); + }); + + // 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, + existsInDocs: true, + 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); + return; + } + + let aggregatableFields: any[] = overallStats.aggregatableExistsFields; + if (allMetricFields.length !== metricExistsFields.length && showAllMetrics === true) { + aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); + } + + const metricFieldsToShow = showAllMetrics === true ? allMetricFields : metricExistsFields; + + metricFieldsToShow.forEach(field => { + const fieldData = aggregatableFields.find(f => { + return f.fieldName === field.displayName; + }); + + if (fieldData !== undefined) { + const metricConfig: FieldVisConfig = { + ...fieldData, + fieldFormat: field.format, + type: ML_JOB_FIELD_TYPES.NUMBER, + loading: true, + aggregatable: true, + }; + + configs.push(metricConfig); + } + }); + + 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) + ); + } + + // 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. + let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats. + const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; + const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || []; + + allNonMetricFields.forEach(f => { + const checkAggregatableField = aggregatableExistsFields.find( + existsField => existsField.fieldName === f.displayName + ); + + if (checkAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkAggregatableField); + } else { + const checkNonAggregatableField = nonAggregatableExistsFields.find( + existsField => existsField.fieldName === f.displayName + ); + + if (checkNonAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkNonAggregatableField); + } + } + }); + + setTotalNonMetricFieldCount(allNonMetricFields.length); + setPopulatedNonMetricFieldCount(nonMetricFieldData.length); + + if (allNonMetricFields.length === nonMetricFieldData.length && showAllNonMetrics === false) { + setShowAllNonMetrics(true); + return; + } + + if (allNonMetricFields.length !== nonMetricFieldData.length && showAllNonMetrics === true) { + // Combine the field data obtained from Elasticsearch into a single array. + nonMetricFieldData = nonMetricFieldData.concat( + overallStats.aggregatableNotExistsFields, + overallStats.nonAggregatableNotExistsFields + ); + } + + const nonMetricFieldsToShow = + showAllNonMetrics === true ? allNonMetricFields : populatedNonMetricFields; + + const configs: FieldVisConfig[] = []; + + nonMetricFieldsToShow.forEach(field => { + const fieldData = nonMetricFieldData.find(f => f.fieldName === field.displayName); + + const nonMetricConfig = { + ...fieldData, + fieldFormat: field.format, + aggregatable: field.aggregatable, + scripted: field.scripted, + loading: fieldData.existsInDocs, + }; + + // Map the field type from the Kibana index pattern to the field type + // used in the data visualizer. + const dataVisualizerType = kbnTypeToMLJobType(field); + if (dataVisualizerType !== undefined) { + nonMetricConfig.type = dataVisualizerType; + } else { + // Add a flag to indicate that this is one of the 'other' Kibana + // field types that do not yet have a specific card type. + nonMetricConfig.type = field.type; + nonMetricConfig.isUnsupportedType = true; + } + + configs.push(nonMetricConfig); + }); + + setNonMetricConfigs(configs); + } + + return ( + + + + + +

{currentIndexPattern.title}

+
+
+ {currentIndexPattern.timeFieldName !== undefined && ( + + + + )} +
+ + + + + + + + + {totalMetricFieldCount > 0 && ( + + + + + )} + + + + + {showActionsPanel === true && ( + + + + )} + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_visualizer/route.ts b/x-pack/legacy/plugins/ml/public/data_visualizer/route.ts new file mode 100644 index 0000000000000..37205d8c3ca68 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_visualizer/route.ts @@ -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. + */ + +// @ts-ignore +import uiRoutes from 'ui/routes'; +// @ts-ignore +import { checkBasicLicense } from '../license/check_license'; +// @ts-ignore +import { checkGetJobsPrivilege } from '../privilege/check_privilege'; +// @ts-ignore +import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../util/index_utils'; +// @ts-ignore +import { checkMlNodesAvailable } from '../ml_nodes_check/check_ml_nodes'; +// @ts-ignore +import { getDataVisualizerBreadcrumbs } from './breadcrumbs'; + +const template = ``; + +uiRoutes.when('/data_visualizer', { + template, + k7Breadcrumbs: getDataVisualizerBreadcrumbs, + resolve: { + CheckLicense: checkBasicLicense, + privileges: checkGetJobsPrivilege, + indexPattern: loadCurrentIndexPattern, + savedSearch: loadCurrentSavedSearch, + checkMlNodesAvailable, + }, +}); diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/selector/datavisualizer_selector.js b/x-pack/legacy/plugins/ml/public/datavisualizer/selector/datavisualizer_selector.js index b9bcaf5ed71c9..2297005deb41f 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/selector/datavisualizer_selector.js +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/selector/datavisualizer_selector.js @@ -146,6 +146,27 @@ export const DatavisualizerSelector = injectI18n(function (props) { data-test-subj="mlDataVisualizerCardIndexData" /> + + } + title="Select an index pattern NEW" + description={ + + } + footer={ + + + + } + data-test-subj="mlDataVisualizerCardIndexData" + /> + {startTrialVisible === true && ( diff --git a/x-pack/legacy/plugins/ml/public/formatters/kibana_field_format.ts b/x-pack/legacy/plugins/ml/public/formatters/kibana_field_format.ts new file mode 100644 index 0000000000000..adaf5cb1a5791 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/formatters/kibana_field_format.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +/* + * Formatter which uses the fieldFormat object of a Kibana index pattern + * field to format the value of a field. + */ + +export function kibanaFieldFormat(value: any, fieldFormat: any) { + if (fieldFormat !== undefined && fieldFormat !== null) { + return fieldFormat.convert(value, 'text'); + } else { + return value; + } +} diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss index a8b16b5de56d6..075ae1bd4e96d 100644 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ b/x-pack/legacy/plugins/ml/public/index.scss @@ -17,6 +17,7 @@ // Sub applications @import 'data_frame/index'; + @import 'data_visualizer/index'; @import 'datavisualizer/index'; @import 'explorer/index'; // SASSTODO: This file needs to be rewritten @import 'file_datavisualizer/index'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/index_or_search/index_or_search_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/index_or_search/index_or_search_controller.js index b5cef1584d93c..a8082b15f1cbf 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/index_or_search/index_or_search_controller.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/index_or_search/index_or_search_controller.js @@ -57,6 +57,18 @@ uiRoutes } }); +uiRoutes + .when('/data_visualizer_index_select', { + template, + k7Breadcrumbs: getDataVisualizerIndexOrSearchBreadcrumbs, + resolve: { + CheckLicense: checkBasicLicense, + privileges: checkFindFileStructurePrivilege, + indexPatterns: loadIndexPatterns, + nextStepPath: () => '#data_visualizer', + } + }); + import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml'); @@ -84,3 +96,4 @@ module.controller('MlNewJobStepIndexOrSearch', return `${path}?savedSearchId=${encodeURIComponent(savedSearch.id)}`; }; }); + diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index 133794725d856..75db9a282b48d 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -63,6 +63,9 @@ declare interface Ml { }> >; + getVisualizerFieldStats(obj: object): Promise; + getVisualizerOverallStats(obj: object): Promise; + jobs: { jobsSummary(jobIds: string[]): Promise; jobs(jobIds: string[]): Promise; diff --git a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts b/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts index 81c150efa2d24..b860fdeeec8e2 100644 --- a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts +++ b/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts @@ -19,5 +19,6 @@ export class MlTimeBuckets { getBounds: () => { min: any; max: any }; getInterval: () => { asMilliseconds: () => number; + expression: string; }; }