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;
};
}