mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* [ML] Delete old AngularJS data visualizer - Resolve merge conflicts * [ML] Fix imports * [ML] Updating translation files
This commit is contained in:
parent
7cd37f3c51
commit
76e1deb8a3
160 changed files with 94 additions and 2618 deletions
|
@ -23,7 +23,6 @@ import 'plugins/ml/services/calendar_service';
|
|||
import 'plugins/ml/components/messagebar';
|
||||
import 'plugins/ml/data_frame';
|
||||
import 'plugins/ml/data_frame_analytics';
|
||||
import 'plugins/ml/data_visualizer';
|
||||
import 'plugins/ml/datavisualizer';
|
||||
import 'plugins/ml/explorer';
|
||||
import 'plugins/ml/timeseriesexplorer';
|
||||
|
@ -34,7 +33,6 @@ import 'plugins/ml/components/confirm_modal';
|
|||
import 'plugins/ml/components/navigation_menu';
|
||||
import 'plugins/ml/components/loading_indicator';
|
||||
import 'plugins/ml/settings';
|
||||
import 'plugins/ml/file_datavisualizer';
|
||||
import 'uiExports/autocompleteProviders';
|
||||
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import 'field_data_card';
|
|
@ -1,56 +0,0 @@
|
|||
<div ng-if="card.existsInDocs === true" class="card-contents">
|
||||
<div class="stats euiText">
|
||||
<div class="stat"><i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardBoolean.cardStatsCountDocumentsPercentsLabel"
|
||||
i18n-default-message="{cardStatsCount, plural, one {# document} other {# documents}}"
|
||||
i18n-values="{
|
||||
cardStatsCount: card.stats.count,
|
||||
}"
|
||||
></span>
|
||||
({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats euiText">
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardBoolean.valuesLabel"
|
||||
i18n-default-message="values"
|
||||
></div>
|
||||
<div class="top-value">
|
||||
<div class="field-label"
|
||||
tooltip-html-unsafe="true"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="right">
|
||||
true
|
||||
</div>
|
||||
<div class="top-value-bar-holder">
|
||||
<div class="top-value-bar boolean"
|
||||
ng-attr-style="width: {{card.stats.count > 0 ? 100 * card.stats.trueCount/card.stats.count : 0}}%;"
|
||||
tooltip-html-unsafe="{{ card.stats.count > 0 ? 100 * card.stats.trueCount/card.stats.count : 0 | number:1 }}%"
|
||||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true">
|
||||
</div>
|
||||
</div>
|
||||
<div class="count-label">{{card.stats.trueCount}}</div>
|
||||
</div>
|
||||
<div class="top-value">
|
||||
<div class="field-label"
|
||||
tooltip-html-unsafe="true"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="right">
|
||||
false
|
||||
</div>
|
||||
<div class="top-value-bar-holder">
|
||||
<div class="top-value-bar boolean"
|
||||
ng-attr-style="width: {{card.stats.count > 0 ? 100 * card.stats.falseCount/card.stats.count : 0}}%;"
|
||||
tooltip-html-unsafe="{{card.stats.count > 0 ? 100 * card.stats.falseCount/card.stats.count : 0 | number:1 }}%"
|
||||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true">
|
||||
</div>
|
||||
</div>
|
||||
<div class="count-label">{{card.stats.falseCount}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,30 +0,0 @@
|
|||
<div ng-if="card.existsInDocs === true" class="card-contents">
|
||||
<div class="stats euiText">
|
||||
<div class="stat"><i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardDate.cardStatsCountDocumentsPercentsLabel"
|
||||
i18n-default-message="{cardStatsCount, plural, one {# document} other {# documents}}"
|
||||
i18n-values="{
|
||||
cardStatsCount: card.stats.count,
|
||||
}"
|
||||
></span>
|
||||
({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%)
|
||||
</div>
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardDate.earliestLabel"
|
||||
i18n-default-message="earliest {earliestCardStats}"
|
||||
i18n-values="{
|
||||
earliestCardStats: (toMoment(card.stats.earliest)),
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardDate.latestLabel"
|
||||
i18n-default-message="latest {latestCardStats}"
|
||||
i18n-values="{
|
||||
latestCardStats: (toMoment(card.stats.latest)),
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,17 +0,0 @@
|
|||
<div class="card-contents">
|
||||
<div class="document-count-chart-container">
|
||||
<ml-document-count-chart
|
||||
width="325"
|
||||
height="335"
|
||||
index-pattern="indexPattern"
|
||||
query="query" >
|
||||
<div class="content-wrapper" />
|
||||
</ml-document-count-chart>
|
||||
</div>
|
||||
<div
|
||||
class="sampled-message euiText"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardDocumentCount.calculatedOverAllDocumentsLabel"
|
||||
i18n-default-message="Calculated over all documents"
|
||||
></div>
|
||||
</div>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<div ng-if="card.existsInDocs === true" class="card-contents">
|
||||
<div class="stats euiText">
|
||||
<div ng-if="card.aggregatable === true" class="stat"><i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardGeoPoint.cardStatsCountDocumentsPercentsLabel"
|
||||
i18n-default-message="{cardStatsCount, plural, one {# document} other {# documents}}"
|
||||
i18n-values="{
|
||||
cardStatsCount: card.stats.count,
|
||||
}"
|
||||
></span>
|
||||
({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%)
|
||||
</div>
|
||||
<div ng-if="card.aggregatable === true" class="stat"><i class="fa fa-cubes" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardGeoPoint.cardStatsCardinalityDistinctValuesLabel"
|
||||
i18n-default-message="{cardStatsCardinality} distinct {cardStatsCardinality, plural, one {value} other {values}}"
|
||||
i18n-values="{
|
||||
cardStatsCardinality: card.stats.cardinality,
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="card.stats.examples.length > 0" class="stats euiText">
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardGeoPoint.cardStatsExamplesLabel"
|
||||
i18n-default-message="{cardStatsExamplesLength, plural, one {value} other {examples}}"
|
||||
i18n-values="{
|
||||
cardStatsExamplesLength: card.stats.examples.length,
|
||||
}"
|
||||
></div>
|
||||
<div ng-repeat="example in card.stats.examples">
|
||||
{{ example }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,33 +0,0 @@
|
|||
<div ng-if="card.existsInDocs === true" class="card-contents">
|
||||
<div class="stats euiText">
|
||||
<div class="stat"><i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardIp.cardStatsCountDocumentsPercentsLabel"
|
||||
i18n-default-message="{cardStatsCount, plural, one {# document} other {# documents}}"
|
||||
i18n-values="{
|
||||
cardStatsCount: card.stats.count,
|
||||
}"
|
||||
></span>
|
||||
({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%)
|
||||
</div>
|
||||
<div class="stat"><i class="fa fa-cubes" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardIp.cardStatsCardinalityDistinctValuesLabel"
|
||||
i18n-default-message="{cardStatsCardinality} distinct {cardStatsCardinality, plural, one {value} other {values}}"
|
||||
i18n-values="{
|
||||
cardStatsCardinality: card.stats.cardinality,
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="stats euiText">
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardIp.topValuesLabel"
|
||||
i18n-default-message="top values"
|
||||
></div>
|
||||
<ml-top-values></ml-top-values>
|
||||
</div>
|
||||
</div>
|
|
@ -1,32 +0,0 @@
|
|||
<div ng-if="card.existsInDocs === true" class="card-contents">
|
||||
<div class="stats euiText">
|
||||
<div class="stat"><i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardKeyword.cardStatsCountDocumentsPercentsLabel"
|
||||
i18n-default-message="{cardStatsCount, plural, one {# document} other {# documents}}"
|
||||
i18n-values="{
|
||||
cardStatsCount: card.stats.count,
|
||||
}"
|
||||
></span>
|
||||
({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%)
|
||||
</div>
|
||||
<div class="stat"><i class="fa fa-cubes" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardKeyword.cardStatsCardinalityDistinctValuesLabel"
|
||||
i18n-default-message="{cardStatsCardinality} distinct {cardStatsCardinality, plural, one {value} other {values}}"
|
||||
i18n-values="{
|
||||
cardStatsCardinality: card.stats.cardinality,
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats euiText">
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardKeyword.topValuesLabel"
|
||||
i18n-default-message="top values"
|
||||
></div>
|
||||
<ml-top-values></ml-top-values>
|
||||
</div>
|
||||
</div>
|
|
@ -1,80 +0,0 @@
|
|||
<div ng-if="card.existsInDocs === true" class="card-contents">
|
||||
<div class="stats euiText--small">
|
||||
<div class="stat"><i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardNumber.cardStatsCountDocumentsPercentsLabel"
|
||||
i18n-default-message="{cardStatsCount, plural, one {# document} other {# documents}}"
|
||||
i18n-values="{
|
||||
cardStatsCount: card.stats.count,
|
||||
}"
|
||||
></span>
|
||||
({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%)
|
||||
</div>
|
||||
<div class="stat"><i class="fa fa-cubes" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardNumber.cardStatsCardinalityDistinctValuesLabel"
|
||||
i18n-default-message="{cardStatsCardinality} distinct {cardStatsCardinality, plural, one {value} other {values}}"
|
||||
i18n-values="{
|
||||
cardStatsCardinality: card.stats.cardinality,
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="stat min heading"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardNumber.minLabel"
|
||||
i18n-default-message="min"
|
||||
></div>
|
||||
<div
|
||||
class="stat median heading"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardNumber.medianLabel"
|
||||
i18n-default-message="median"
|
||||
></div>
|
||||
<div
|
||||
class="stat max heading"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardNumber.maxLabel"
|
||||
i18n-default-message="max"
|
||||
></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat min value">
|
||||
<ml-display-value value="card.stats.min | formatField:card.fieldFormat">
|
||||
</ml-display-value>
|
||||
</div>
|
||||
<div class="stat median value">
|
||||
<ml-display-value value="card.stats.median | formatField:card.fieldFormat">
|
||||
</ml-display-value>
|
||||
</div>
|
||||
<div class="stat max value">
|
||||
<ml-display-value value="card.stats.max | formatField:card.fieldFormat">
|
||||
</ml-display-value>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details-select">
|
||||
<select class="kuiSelect"
|
||||
ng-model="detailsMode"
|
||||
ng-change="detailsModeChanged(detailsMode)"
|
||||
aria-label="{{:: 'xpack.ml.fieldDataCard.cardNumber.details.selectMetricDetailsDisplayAriaLabel' | i18n: { defaultMessage: 'Select metric details display' } }}">
|
||||
<option
|
||||
value="distribution"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardNumber.details.distributionOfValuesLabel"
|
||||
i18n-default-message="distribution of values"
|
||||
></option>
|
||||
<option
|
||||
value="top"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardNumber.details.topValuesLabel"
|
||||
i18n-default-message="top values"
|
||||
></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="details-container" ng-switch on="detailsMode">
|
||||
<ml-metric-distribution-chart ng-switch-when="distribution" width="325" height="220">
|
||||
<div class="content-wrapper" />
|
||||
</ml-metric-distribution-chart>
|
||||
<div ng-switch-default>
|
||||
<ml-top-values></ml-top-values>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,44 +0,0 @@
|
|||
<div ng-if="card.existsInDocs === true" class="card-contents">
|
||||
<div class="stats euiText">
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardOther.cardTypeLabel"
|
||||
i18n-default-message="{cardType} type"
|
||||
i18n-values="{
|
||||
cardType: card.type,
|
||||
}"
|
||||
></div>
|
||||
<div ng-if="card.aggregatable === true" class="stat"><i class="fa fa-files-o" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardOther.cardStatsCountDocumentsPercentsLabel"
|
||||
i18n-default-message="{cardStatsCount, plural, one {# document} other {# documents}}"
|
||||
i18n-values="{
|
||||
cardStatsCount: card.stats.count,
|
||||
}"
|
||||
></span>
|
||||
({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%)
|
||||
</div>
|
||||
<div ng-if="card.aggregatable === true" class="stat"><i class="fa fa-cubes" aria-hidden="true"></i>
|
||||
<span
|
||||
i18n-id="xpack.ml.fieldDataCard.cardOther.cardStatsCardinalityDistinctValuesLabel"
|
||||
i18n-default-message="{cardStatsCardinality} distinct {cardStatsCardinality, plural, one {value} other {values}}"
|
||||
i18n-values="{
|
||||
cardStatsCardinality: card.stats.cardinality,
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="examples.length > 0" class="stats euiText">
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardOther.cardStatsExamplesLabel"
|
||||
i18n-default-message="{cardStatsExamplesLength, plural, one {value} other {examples}}"
|
||||
i18n-values="{
|
||||
cardStatsExamplesLength: card.stats.examples.length,
|
||||
}"
|
||||
></div>
|
||||
<div ng-repeat="example in card.stats.examples">
|
||||
{{ example }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,44 +0,0 @@
|
|||
<div ng-if="card.existsInDocs === true" class="card-contents">
|
||||
<div ng-if="card.stats.examples.length > 0" class="stats euiText">
|
||||
<div
|
||||
class="stat"
|
||||
i18n-id="xpack.ml.fieldDataCard.cardText.examplesLabel"
|
||||
i18n-default-message="{examplesLength, plural, one {value} other {examples}}"
|
||||
i18n-values="{
|
||||
examplesLength: card.stats.examples.length,
|
||||
}"
|
||||
></div>
|
||||
<div ng-repeat="example in card.stats.examples">
|
||||
{{ example }}
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="card.stats.examples.length === 0" class="not-exist-message">
|
||||
<div>
|
||||
<p
|
||||
i18n-id="xpack.ml.fieldDataCard.cardText.noExampleForFieldsTitle"
|
||||
i18n-default-message="{icon} No examples were obtained for this field."
|
||||
i18n-values="{
|
||||
html_icon: '<i class=\'fa fa-exclamation-triangle\' aria-hidden=\'true\'></i>',
|
||||
}"
|
||||
></p>
|
||||
<p
|
||||
i18n-id="xpack.ml.fieldDataCard.cardText.fieldNotPresentInDocumentQueriedFieldDescription"
|
||||
i18n-default-message="This field was not present in the {sourceParam} field of documents queried."
|
||||
i18n-values="{
|
||||
html_sourceParam: '<span class=\'text-code\'>_source</span>',
|
||||
}"
|
||||
></p>
|
||||
<p
|
||||
i18n-id="xpack.ml.fieldDataCard.cardText.fieldMayBePopulatedDescription"
|
||||
i18n-default-message="It may be populated, for example, using a {copyToParam} parameter in the document mapping, or be pruned from the {sourceParam} field after indexing through the use of {includesParam} and {excludesParam} parameters."
|
||||
i18n-values="{
|
||||
html_copyToParam: '<span class=\'text-code\'>copy_to</span>',
|
||||
html_sourceParam: '<span class=\'text-code\'>_source</span>',
|
||||
html_includesParam: '<span class=\'text-code\'>includes</span>',
|
||||
html_excludesParam: '<span class=\'text-code\'>excludes</span>',
|
||||
}"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import 'ngreact';
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { DisplayValue } from '../../components/display_value';
|
||||
|
||||
module.directive('mlDisplayValue', function (reactDirective) {
|
||||
return reactDirective(
|
||||
DisplayValue,
|
||||
undefined,
|
||||
{ restrict: 'E' }
|
||||
);
|
||||
});
|
|
@ -1,190 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* AngularJS directive for rendering a chart showing
|
||||
* document count on the field data card.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import d3 from 'd3';
|
||||
import moment from 'moment';
|
||||
|
||||
import { parseInterval } from 'ui/utils/parse_interval';
|
||||
import { numTicksForDateFormat } from '../../util/chart_utils';
|
||||
import { calculateTextWidth } from '../../util/string_utils';
|
||||
import { MlTimeBuckets } from '../../util/ml_time_buckets';
|
||||
import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service';
|
||||
import { formatHumanReadableDateTime } from '../../util/date_utils';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlDocumentCountChart', function () {
|
||||
function link(scope, element, attrs) {
|
||||
const svgWidth = attrs.width ? +attrs.width : 400;
|
||||
const svgHeight = scope.height = attrs.height ? +attrs.height : 400;
|
||||
|
||||
const margin = { top: 0, right: 5, bottom: 20, left: 15 };
|
||||
|
||||
let chartWidth = svgWidth - (margin.left + margin.right);
|
||||
const chartHeight = svgHeight - (margin.top + margin.bottom);
|
||||
|
||||
let xScale = null;
|
||||
let yScale = d3.scale.linear().range([chartHeight, 0]);
|
||||
let xAxisTickFormat = 'YYYY-MM-DD HH:mm';
|
||||
|
||||
let barChartGroup;
|
||||
let barWidth = 5; // Adjusted according to data aggregation interval.
|
||||
|
||||
scope.chartData = [];
|
||||
|
||||
element.on('$destroy', function () {
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
function processChartData() {
|
||||
// Build the dataset in format used by the d3 chart i.e. array
|
||||
// of Objects with keys time (epoch ms), date (JavaScript date) and value.
|
||||
const bucketsData = _.get(scope, ['card', 'stats', 'documentCounts', 'buckets'], {});
|
||||
const chartData = [];
|
||||
_.each(bucketsData, (value, time) => {
|
||||
chartData.push({
|
||||
date: new Date(+time),
|
||||
time: +time,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
scope.chartData = chartData;
|
||||
}
|
||||
|
||||
function render() {
|
||||
// Clear any existing elements from the visualization,
|
||||
// then build the svg elements for the bar chart.
|
||||
const chartElement = d3.select(element.get(0)).select('.content-wrapper');
|
||||
chartElement.selectAll('*').remove();
|
||||
|
||||
if (scope.chartData === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = chartElement.append('svg')
|
||||
.attr('width', svgWidth)
|
||||
.attr('height', svgHeight);
|
||||
|
||||
// Set the size of the left margin according to the width
|
||||
// of the largest y axis tick label.
|
||||
const maxYVal = d3.max(scope.chartData, (d) => d.value);
|
||||
yScale = yScale.domain([0, maxYVal]);
|
||||
|
||||
const yAxis = d3.svg.axis().scale(yScale).orient('left').outerTickSize(0);
|
||||
|
||||
// barChartGroup translate doesn't seem to be relative
|
||||
// to parent svg, so have to add an extra 5px on.
|
||||
const maxYAxisLabelWidth = calculateTextWidth(maxYVal, true, svg);
|
||||
margin.left = Math.max(maxYAxisLabelWidth + yAxis.tickPadding() + 5, 25);
|
||||
chartWidth = Math.max(svgWidth - margin.left - margin.right, 0);
|
||||
|
||||
const bounds = timefilter.getActiveBounds();
|
||||
xScale = d3.time.scale()
|
||||
.domain([new Date(bounds.min.valueOf()), new Date(bounds.max.valueOf())])
|
||||
.range([0, chartWidth]);
|
||||
|
||||
if (scope.chartData.length > 0) {
|
||||
// x axis tick format and bar width determined by data aggregation interval.
|
||||
const buckets = new MlTimeBuckets();
|
||||
const aggInterval = _.get(scope, ['card', 'stats', 'documentCounts', 'interval']);
|
||||
buckets.setInterval(aggInterval);
|
||||
buckets.setBounds(bounds);
|
||||
xAxisTickFormat = buckets.getScaledDateFormat();
|
||||
|
||||
const intervalMs = parseInterval(aggInterval).asMilliseconds();
|
||||
barWidth = xScale(scope.chartData[0].time + intervalMs) - xScale(scope.chartData[0].time);
|
||||
}
|
||||
|
||||
const xAxis = d3.svg.axis().scale(xScale).orient('bottom')
|
||||
.outerTickSize(0).ticks(numTicksForDateFormat(chartWidth, xAxisTickFormat))
|
||||
.tickFormat((d) => {
|
||||
return moment(d).format(xAxisTickFormat);
|
||||
});
|
||||
|
||||
barChartGroup = svg.append('g')
|
||||
.attr('class', 'bar-chart')
|
||||
.attr('transform', `translate(${margin.left}, ${margin.top})`);
|
||||
|
||||
drawBarChartAxes(xAxis, yAxis);
|
||||
drawBarChartPaths();
|
||||
}
|
||||
|
||||
function drawBarChartAxes(xAxis, yAxis) {
|
||||
const axes = barChartGroup.append('g');
|
||||
|
||||
axes.append('g')
|
||||
.attr('class', 'x axis')
|
||||
.attr('transform', `translate(0, ${chartHeight})`)
|
||||
.call(xAxis);
|
||||
|
||||
axes.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.call(yAxis);
|
||||
}
|
||||
|
||||
function drawBarChartPaths() {
|
||||
barChartGroup.selectAll('bar')
|
||||
.data(scope.chartData)
|
||||
.enter().append('rect')
|
||||
.attr('class', 'bar')
|
||||
.attr('x', (d) => { return xScale(d.time); })
|
||||
.attr('width', barWidth)
|
||||
.attr('y', (d) => { return yScale(d.value); })
|
||||
.attr('height', (d) => { return chartHeight - yScale(d.value); })
|
||||
.on('mouseover', function (d) {
|
||||
showChartTooltip(d, this);
|
||||
})
|
||||
.on('mousemove', function (d) {
|
||||
showChartTooltip(d, this);
|
||||
})
|
||||
.on('mouseout', () => mlChartTooltipService.hide());
|
||||
|
||||
function showChartTooltip(data, rect) {
|
||||
const formattedDate = formatHumanReadableDateTime(data.time);
|
||||
const contents = i18n.translate('xpack.ml.fieldDataCard.documentCountChart.chartTooltip', {
|
||||
defaultMessage: '{formattedDate}{br}{hr}count: {dataValue}',
|
||||
values: {
|
||||
formattedDate,
|
||||
dataValue: data.value,
|
||||
br: '<br />',
|
||||
hr: '<hr />',
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate the y offset.
|
||||
// rectY are mouseY are relative to top of the chart area.
|
||||
const rectY = d3.select(rect).attr('y');
|
||||
const mouseY = +(d3.mouse(rect)[1]);
|
||||
|
||||
mlChartTooltipService.show(contents, rect, {
|
||||
x: 5,
|
||||
y: (mouseY - rectY)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process the data and then render the chart.
|
||||
processChartData();
|
||||
render();
|
||||
}
|
||||
|
||||
return {
|
||||
scope: false,
|
||||
link: link
|
||||
};
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
<div class="ml-field-data-card">
|
||||
<ml-field-title-bar card="card" />
|
||||
|
||||
<div ng-if="card.loading === true" class="card-contents">
|
||||
<ml-loading-indicator
|
||||
label="{{:: 'xpack.ml.fieldDataCard.loadingLabel' | i18n: { defaultMessage: 'Loading' } }}"
|
||||
is-loading="card.loading"
|
||||
height="200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ng-if="card.existsInDocs === false" class="card-contents">
|
||||
<div
|
||||
class="not-exist-message"
|
||||
i18n-id="xpack.ml.fieldDataCard.fieldDoesNotAppearInDocumentsDescription"
|
||||
i18n-default-message="{icon} This field does not appear in any documents for the selected time range"
|
||||
i18n-values="{
|
||||
html_icon: '<i class=\'fa fa-exclamation-triangle\' aria-hidden=\'true\'></i>',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<ng-include ng-if="card.loading === false" src="getCardUrl()"></ng-include>
|
||||
</div>
|
|
@ -1,114 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* AngularJS directive for rendering a card showing data on a field in an index pattern.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import moment from 'moment';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
import template from './field_data_card.html';
|
||||
import { ML_JOB_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types';
|
||||
import { mlEscape } from 'plugins/ml/util/string_utils';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlFieldDataCard', function (config) {
|
||||
|
||||
function link(scope, element) {
|
||||
scope.ML_JOB_FIELD_TYPES = ML_JOB_FIELD_TYPES;
|
||||
scope.mlEscape = mlEscape;
|
||||
|
||||
if (scope.card.type === ML_JOB_FIELD_TYPES.NUMBER) {
|
||||
if (scope.card.fieldName) {
|
||||
scope.$watch('card.stats', () => {
|
||||
const cardinality = _.get(scope, ['card', 'stats', 'cardinality'], 0);
|
||||
scope.detailsMode = cardinality > 100 ? 'distribution' : 'top';
|
||||
});
|
||||
|
||||
const cardinality = _.get(scope, ['card', 'stats', 'cardinality'], 0);
|
||||
scope.detailsMode = cardinality > 100 ? 'distribution' : 'top';
|
||||
}
|
||||
// Create a div for the chart tooltip.
|
||||
$('.ml-field-data-card-tooltip').remove();
|
||||
$('body').append('<div class="ml-field-data-card-tooltip" style="opacity:0; display: none;">');
|
||||
}
|
||||
|
||||
if (scope.card.type === ML_JOB_FIELD_TYPES.DATE) {
|
||||
scope.$watch('card.stats', () => {
|
||||
// Convert earliest and latest to Dates for formatting with moment filter in the template.
|
||||
if (_.has(scope, 'card.stats.earliest')) {
|
||||
scope.card.stats.earliest = new Date(scope.card.stats.earliest);
|
||||
}
|
||||
if (_.has(scope, 'card.stats.latest')) {
|
||||
scope.card.stats.latest = new Date(scope.card.stats.latest);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
scope.toMoment = function (datetime) {
|
||||
return moment(datetime).format(config.get('dateFormat'));
|
||||
};
|
||||
|
||||
scope.getCardUrl = function () {
|
||||
const urlBasePath = chrome.getBasePath();
|
||||
const baseCardPath = `${urlBasePath}/plugins/ml/components/field_data_card/content_types`;
|
||||
const cardType = scope.card.type;
|
||||
switch (cardType) {
|
||||
case ML_JOB_FIELD_TYPES.BOOLEAN:
|
||||
return `${baseCardPath}/card_boolean.html`;
|
||||
case ML_JOB_FIELD_TYPES.DATE:
|
||||
return `${baseCardPath}/card_date.html`;
|
||||
case ML_JOB_FIELD_TYPES.GEO_POINT:
|
||||
return `${baseCardPath}/card_geo_point.html`;
|
||||
case ML_JOB_FIELD_TYPES.IP:
|
||||
return `${baseCardPath}/card_ip.html`;
|
||||
case ML_JOB_FIELD_TYPES.KEYWORD:
|
||||
return `${baseCardPath}/card_keyword.html`;
|
||||
case ML_JOB_FIELD_TYPES.NUMBER:
|
||||
if (scope.card.fieldName) {
|
||||
return `${baseCardPath}/card_number.html`;
|
||||
} else {
|
||||
return `${baseCardPath}/card_document_count.html`;
|
||||
}
|
||||
case ML_JOB_FIELD_TYPES.TEXT:
|
||||
return `${baseCardPath}/card_text.html`;
|
||||
default:
|
||||
return `${baseCardPath}/card_other.html`;
|
||||
}
|
||||
};
|
||||
|
||||
element.on('$destroy', () => {
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
scope: {
|
||||
card: '=',
|
||||
indexPattern: '=',
|
||||
query: '=',
|
||||
earliest: '=',
|
||||
latest: '='
|
||||
},
|
||||
template,
|
||||
link: link
|
||||
};
|
||||
})
|
||||
.filter('formatField', function () {
|
||||
// Filter to format the value of a field according to the defined format
|
||||
// of the field in the index pattern.
|
||||
return function (value, fieldFormat) {
|
||||
return fieldFormat.convert(value, 'text');
|
||||
};
|
||||
});
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import './document_count_chart_directive';
|
||||
import './display_value_directive';
|
||||
import './field_data_card_directive';
|
||||
import './metric_distribution_chart_directive';
|
||||
import './top_values_directive';
|
||||
import 'plugins/ml/components/field_title_bar';
|
||||
import 'plugins/ml/components/field_type_icon';
|
||||
import 'plugins/ml/components/chart_tooltip';
|
|
@ -1,301 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* AngularJS directive for rendering a chart showing the distribution of values for
|
||||
* a metric on the field data card.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import d3 from 'd3';
|
||||
|
||||
import { numTicks } from '../../util/chart_utils';
|
||||
import { ordinalSuffix } from 'ui/utils/ordinal_suffix';
|
||||
import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlMetricDistributionChart', function () {
|
||||
|
||||
function link(scope, element, attrs) {
|
||||
const svgWidth = attrs.width ? +attrs.width : 400;
|
||||
const svgHeight = scope.height = attrs.height ? +attrs.height : 400;
|
||||
|
||||
// TODO - do we want to label the y axis?
|
||||
const margin = { top: 0, right: 15, bottom: 20, left: 15 };
|
||||
const infoLabelHeight = 15;
|
||||
|
||||
const chartWidth = svgWidth - (margin.left + margin.right);
|
||||
const chartHeight = svgHeight - (margin.top + margin.bottom + infoLabelHeight);
|
||||
|
||||
let xScale = d3.scale.linear().range([0, chartWidth]);
|
||||
let yScale = d3.scale.linear().range([chartHeight, 0]);
|
||||
let xAxisMin = 0;
|
||||
let xAxisMax = 1;
|
||||
let chartGroup;
|
||||
|
||||
const distributionArea = d3.svg.area()
|
||||
.x(function (d) { return xScale(d.x); })
|
||||
.y0(function () { return yScale(0); })
|
||||
.y1(function (d) { return yScale(d.y); });
|
||||
|
||||
const MIN_BAR_WIDTH = 3; // Minimum bar width, in pixels.
|
||||
|
||||
element.on('$destroy', function () {
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
function processDistributionData() {
|
||||
const distributionData = _.get(scope, ['card', 'stats', 'distribution', 'percentiles'], []);
|
||||
const chartData = [];
|
||||
|
||||
// Process the raw distribution data so it is in a suitable format for plotting:
|
||||
if (distributionData.length === 0) {
|
||||
return chartData;
|
||||
}
|
||||
|
||||
// Adjust x axis min and max if there is a single bar.
|
||||
const minX = distributionData[0].minValue;
|
||||
const maxX = distributionData[distributionData.length - 1].maxValue;
|
||||
xAxisMin = minX;
|
||||
xAxisMax = 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 MIN_BAR_WIDTH.
|
||||
// TODO - make sure last bar isn't cropped at RHS.
|
||||
const minBarWidth = (MIN_BAR_WIDTH / chartWidth) * (xAxisMax - xAxisMin);
|
||||
const processedData = [];
|
||||
let lastBar = undefined;
|
||||
_.each(distributionData, (data, index) => {
|
||||
|
||||
if (index === 0) {
|
||||
const bar = {
|
||||
x0: data.minValue,
|
||||
x1: Math.max(data.minValue + minBarWidth, data.maxValue),
|
||||
dataMin: data.minValue,
|
||||
dataMax: data.maxValue,
|
||||
percent: data.percent
|
||||
};
|
||||
|
||||
// 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),
|
||||
dataMin: data.minValue,
|
||||
dataMax: data.maxValue,
|
||||
percent: data.percent
|
||||
};
|
||||
|
||||
// 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 = _.last(processedData).x1;
|
||||
}
|
||||
|
||||
// Adjust the maximum bar height to be (10 * median bar height).
|
||||
// TODO indicate if a bar height has been truncated?
|
||||
let barHeights = _.pluck(processedData, 'y');
|
||||
barHeights = barHeights.sort((a, b) => a - b);
|
||||
|
||||
let maxBarHeight = 0;
|
||||
const processedDataLength = processedData.length;
|
||||
if (Math.abs(processedDataLength % 2) === 1) {
|
||||
maxBarHeight = 20 * barHeights[(Math.floor(processedDataLength / 2))];
|
||||
} else {
|
||||
maxBarHeight = 20 * (barHeights[(Math.floor(processedDataLength / 2)) - 1] +
|
||||
barHeights[(Math.floor(processedDataLength / 2))]) / 2;
|
||||
}
|
||||
|
||||
_.each(processedData, (data) => {
|
||||
data.y = Math.min(data.y, maxBarHeight);
|
||||
});
|
||||
|
||||
scope.processedData = processedData;
|
||||
|
||||
chartData.push({ x: minX, y: 0 });
|
||||
_.each(processedData, (data) => {
|
||||
chartData.push({ x: data.x0, y: data.y });
|
||||
chartData.push({ x: data.x1, y: data.y });
|
||||
});
|
||||
chartData.push({ x: processedData[processedData.length - 1].x1, y: 0 });
|
||||
|
||||
return chartData;
|
||||
}
|
||||
|
||||
function init() {
|
||||
scope.chartData = processDistributionData();
|
||||
|
||||
// Clear any existing elements from the visualization,
|
||||
// then build the svg elements for the chart.
|
||||
const chartElement = d3.select(element.get(0)).select('.content-wrapper');
|
||||
chartElement.select('svg').remove();
|
||||
|
||||
const svg = chartElement.append('svg')
|
||||
.attr('width', svgWidth)
|
||||
.attr('height', svgHeight);
|
||||
|
||||
// Add a label above the chart to display percentiles being plotted.
|
||||
const minPercentile = _.get(scope, ['card', 'stats', 'distribution', 'minPercentile']);
|
||||
const maxPercentile = _.get(scope, ['card', 'stats', 'distribution', 'maxPercentile']);
|
||||
const minPercent = ordinalSuffix(minPercentile);
|
||||
const maxPercent = ordinalSuffix(maxPercentile);
|
||||
svg.append('text')
|
||||
.attr('x', chartWidth / 2)
|
||||
.attr('y', 10)
|
||||
.attr('class', 'info-text')
|
||||
.attr('transform', `translate(${margin.left}, ${margin.top})`)
|
||||
.text(i18n.translate('xpack.ml.fieldDataCard.metricDistributionChart.displayingPercentilesLabel', {
|
||||
defaultMessage: 'Displaying {minPercent} - {maxPercent} percentiles',
|
||||
values: {
|
||||
minPercent,
|
||||
maxPercent,
|
||||
},
|
||||
}));
|
||||
|
||||
const translateTop = margin.top + infoLabelHeight;
|
||||
chartGroup = svg.append('g')
|
||||
.attr('class', 'distribution-chart')
|
||||
.attr('transform', `translate(${margin.left}, ${translateTop})`);
|
||||
|
||||
const dataLength = scope.chartData.length;
|
||||
if (dataLength > 0) {
|
||||
xScale = xScale.domain([xAxisMin, xAxisMax]);
|
||||
|
||||
const yMax = d3.max(scope.chartData, (d) => d.y);
|
||||
yScale = yScale.domain([0, yMax]);
|
||||
}
|
||||
}
|
||||
|
||||
function drawAxes() {
|
||||
const axes = chartGroup.append('g')
|
||||
.attr('class', 'axes');
|
||||
|
||||
// Use the numTicks util function to calculate the number of ticks
|
||||
// for the x axis, according to the width of the chart.
|
||||
// Note that d3 doesn't guarantee that the axis will end up with
|
||||
// this exact number of ticks.
|
||||
const xAxis = d3.svg.axis().scale(xScale).orient('bottom')
|
||||
.outerTickSize(0).ticks(numTicks(chartWidth))
|
||||
.tickFormat((d) => {
|
||||
// Format the tick label according to the format of the index pattern field.
|
||||
return scope.card.fieldFormat.convert(d, 'text');
|
||||
});
|
||||
|
||||
const yAxis = d3.svg.axis().scale(yScale).orient('left')
|
||||
.outerTickSize(0).ticks(0);
|
||||
|
||||
axes.append('g')
|
||||
.attr('class', 'x axis')
|
||||
.attr('transform', `translate(0, ${chartHeight})`)
|
||||
.call(xAxis);
|
||||
|
||||
axes.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.call(yAxis);
|
||||
}
|
||||
|
||||
function drawDistributionArea() {
|
||||
const path = chartGroup.append('path');
|
||||
path.datum(scope.chartData)
|
||||
.attr('class', 'area')
|
||||
.attr('d', distributionArea)
|
||||
.on('mouseover', showChartTooltip)
|
||||
.on('mouseout', () => mlChartTooltipService.hide())
|
||||
.on('mousemove', showChartTooltip);
|
||||
|
||||
function showChartTooltip() {
|
||||
const xPos = d3.mouse(this)[0];
|
||||
const yPos = d3.mouse(this)[1];
|
||||
const xVal = xScale.invert(xPos);
|
||||
|
||||
let processedDataIdx = 0;
|
||||
for (let i = 0; i < scope.processedData.length; i++) {
|
||||
if (xVal < scope.processedData[i].x1) {
|
||||
processedDataIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let contents;
|
||||
const bar = scope.processedData[processedDataIdx];
|
||||
const minValFormatted = scope.card.fieldFormat.convert(bar.dataMin, 'text');
|
||||
if (bar.dataMax > bar.dataMin) {
|
||||
const maxValFormatted = scope.card.fieldFormat.convert(bar.dataMax, 'text');
|
||||
contents = i18n.translate('xpack.ml.fieldDataCard.metricDistributionChart.documentsBarPercentBetweenValuesDescription', {
|
||||
defaultMessage: '{barPercent}% of documents have{br}values between {minValFormatted} and {maxValFormatted}',
|
||||
values: {
|
||||
barPercent: bar.percent,
|
||||
minValFormatted,
|
||||
maxValFormatted,
|
||||
br: '<br />',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
contents = i18n.translate('xpack.ml.fieldDataCard.metricDistributionChart.documentsBarPercentValueDescription', {
|
||||
defaultMessage: '{barPercent}% of documents have{br}a value of {minValFormatted}',
|
||||
values: {
|
||||
barPercent: bar.percent,
|
||||
minValFormatted,
|
||||
br: '<br />',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
contents = `<div class='eui-textCenter'>${contents}</div>`;
|
||||
|
||||
if (path.length && path[0].length) {
|
||||
mlChartTooltipService.show(contents, path[0][0], {
|
||||
x: xPos + 5,
|
||||
y: yPos + 10
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
drawAxes();
|
||||
drawDistributionArea();
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
scope: false,
|
||||
link: link
|
||||
};
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
<div ng-repeat="value in card.stats.topValues" class="top-value">
|
||||
<div class="field-label"
|
||||
tooltip-html-unsafe="{{mlEscape(value.key) | formatField:card.fieldFormat}}"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="right">
|
||||
{{value.key | formatField:card.fieldFormat}}
|
||||
</div>
|
||||
<div class="top-value-bar-holder">
|
||||
<div ng-if="card.stats.isTopValuesSampled === false"
|
||||
class="top-value-bar {{card.type}}"
|
||||
ng-attr-style="width: {{ 100 * value.doc_count/card.stats.count }}%;"
|
||||
tooltip-html-unsafe="{{ 100 * value.doc_count/card.stats.count >= 0.1 ? (100 * value.doc_count/card.stats.count | number:1): '< 0.1' }}%"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="right">
|
||||
</div>
|
||||
<div ng-if="card.stats.isTopValuesSampled === true"
|
||||
class="top-value-bar {{card.type}}"
|
||||
ng-attr-style="width: {{ 100 * value.doc_count/card.stats.topValuesSampleSize }}%;"
|
||||
tooltip-html-unsafe="{{ 100 * value.doc_count/card.stats.topValuesSampleSize >= 0.1 ? (100 * value.doc_count/card.stats.topValuesSampleSize | number:1): '< 0.1' }}%"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="right">
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="card.stats.isTopValuesSampled === false" class="count-label">{{value.doc_count}}</div>
|
||||
<div ng-if="card.stats.isTopValuesSampled === true" class="count-label">
|
||||
{{ 100 * value.doc_count/card.stats.topValuesSampleSize >= 0.1 ? (100 * value.doc_count/card.stats.topValuesSampleSize | number:1): '< 0.1' }}%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ng-if="card.stats.isTopValuesSampled === true"
|
||||
class="sampled-message euiText--small"
|
||||
i18n-id="xpack.ml.fieldDataCard.calculatedFromSampleDescription"
|
||||
i18n-default-message="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
|
||||
i18n-values="{
|
||||
topValuesSamplerShardSize: card.stats.topValuesSamplerShardSize,
|
||||
}"
|
||||
></div>
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import template from './top_values.html';
|
||||
import { mlEscape } from 'plugins/ml/util/string_utils';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlTopValues', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template,
|
||||
link(scope) {
|
||||
scope.mlEscape = mlEscape;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
import sinon from 'sinon';
|
||||
|
||||
// Import this way to be able to stub/mock functions later on in the tests using sinon.
|
||||
import * as indexUtils from 'plugins/ml/util/index_utils';
|
||||
|
||||
describe('ML - Data Visualizer View Fields Controller', () => {
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
||||
it('Initialize Data Visualizer View Fields Controller', (done) => {
|
||||
const stub = sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false);
|
||||
ngMock.inject(function ($rootScope, $controller, $route) {
|
||||
// Set up the $route current props required for the tests.
|
||||
$route.current = {
|
||||
locals: {
|
||||
indexPattern: {
|
||||
id: ''
|
||||
},
|
||||
savedSearch: {
|
||||
id: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scope = $rootScope.$new();
|
||||
|
||||
expect(() => {
|
||||
$controller('MlDataVisualizerViewFields', { $scope: scope });
|
||||
}).to.not.throwError();
|
||||
|
||||
expect(scope.metricCards).to.eql([]);
|
||||
stub.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,142 +0,0 @@
|
|||
.data-visualizer-container {
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding: $euiSizeS $euiSize;
|
||||
flex: 1 0 auto;
|
||||
|
||||
.flexGroup__filler {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.kuiVerticalRhythm + .kuiVerticalRhythm {
|
||||
margin-top: $euiSizeL;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
padding-bottom: $euiSize;
|
||||
padding-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
width: calc(100% - 300px);
|
||||
display: inline-block;
|
||||
padding-right: $euiSizeS;
|
||||
}
|
||||
.no-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.datavisualizer-sidebar {
|
||||
width: 290px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.datavisualizer-panel {
|
||||
padding: $euiSizeS $euiSize;
|
||||
background-color: $euiColorEmptyShade;
|
||||
}
|
||||
|
||||
.datavisualizer-panel.card-panel {
|
||||
padding-right: $euiSizeXS;
|
||||
}
|
||||
|
||||
.query-bar-form {
|
||||
padding-top: $euiSizeXS;
|
||||
}
|
||||
|
||||
.document-count-container {
|
||||
padding-top: $euiSizeS;
|
||||
|
||||
// SASSTODO: Make a proper selector
|
||||
label {
|
||||
font-weight: $euiFontWeightRegular;
|
||||
}
|
||||
|
||||
// SASSTODO: Make a proper selector
|
||||
.fa-info-circle {
|
||||
color: $euiColorDarkShade;
|
||||
font-size: $euiFontSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
.field-count-label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.field-sort-controls {
|
||||
margin-bottom: 0px;
|
||||
min-width: 300px;
|
||||
padding: 10px 0px;
|
||||
|
||||
.field-group-input {
|
||||
display: inline;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
// SASSTODO: Make a proper selector
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.field-filter-controls {
|
||||
margin-bottom: 0px;
|
||||
min-width: 400px;
|
||||
padding-bottom: $euiSizeXL;
|
||||
padding-right: $euiSizeS;
|
||||
|
||||
.type-input {
|
||||
float: left;
|
||||
margin-right: $euiSizeXS;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.field-emphasis {
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: inline-grid;
|
||||
display: -ms-inline-grid;
|
||||
padding: 0px $euiSize $euiSize 0px;
|
||||
}
|
||||
|
||||
.create-job-content {
|
||||
padding-bottom: $euiSizeXS;
|
||||
|
||||
.synopsisTitle {
|
||||
font-size: $euiFontSizeM;
|
||||
font-weight: $euiFontWeightRegular;
|
||||
color: $euiColorPrimary;
|
||||
}
|
||||
|
||||
.synopsis:hover {
|
||||
text-decoration: none;
|
||||
|
||||
.synopsisTitle {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.synopsisIcon {
|
||||
padding-top: $euiSizeS;
|
||||
}
|
||||
|
||||
.recognized-job-content {
|
||||
margin-bottom: $euiSizeXXL;
|
||||
|
||||
.recognizer-result {
|
||||
margin-top: $euiSizeS;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SASSTODO: Make a proper selector
|
||||
p {
|
||||
margin-bottom: $euiSize;
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
@import 'datavisualizer';
|
||||
@import 'file_based/index';
|
||||
@import 'index_based/index';
|
||||
|
|
|
@ -1,259 +0,0 @@
|
|||
<ml-nav-menu name="datavisualizer" />
|
||||
<ml-chart-tooltip></ml-chart-tooltip>
|
||||
<ml-datavisualizer class="data-visualizer-container">
|
||||
<div ng-controller="MlDataVisualizerViewFields" >
|
||||
<div class="title-container euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--alignItemsCenter euiFlexGroup--responsive">
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero euiText">
|
||||
<h1>{{indexPattern.title}}</h1>
|
||||
</div>
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<ml-full-time-range-selector ng-if="indexPattern.timeFieldName !== undefined" index-pattern='indexPattern' query='searchQuery' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="main-container"
|
||||
ng-class="{'no-sidebar': showSidebar===false}"
|
||||
>
|
||||
|
||||
<div class="kuiPanel kuiVerticalRhythm datavisualizer-panel">
|
||||
|
||||
<form
|
||||
ng-submit="submitSearchQuery()"
|
||||
name="querybarform"
|
||||
class="flexGroup__filler query-bar-form"
|
||||
style="width: 100%"
|
||||
>
|
||||
<div class="kuiLocalSearch">
|
||||
<input
|
||||
type="text"
|
||||
class="kuiLocalSearchInput"
|
||||
ng-model="searchQueryText"
|
||||
placeholder="{{ ::'xpack.ml.datavisualizer.searchFieldPlaceholder' | i18n: {
|
||||
defaultMessage: 'Search… (e.g. status:200 AND extension:PHP)'
|
||||
} }}"
|
||||
aria-label="{{ ::'xpack.ml.datavisualizer.searchFieldAriaLabel' | i18n: {
|
||||
defaultMessage: 'Search input'
|
||||
} }}"
|
||||
autocomplete="off"
|
||||
>
|
||||
<button
|
||||
class="kuiLocalSearchButton"
|
||||
type="submit"
|
||||
aria-label="{{ ::'xpack.ml.datavisualizer.searchButtonAriaLabel' | i18n: {
|
||||
defaultMessage: 'Search'
|
||||
} }}"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-search"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="document-count-container">
|
||||
<label
|
||||
for="sampleShardSize"
|
||||
i18n-id="xpack.ml.datavisualizer.sampleLabel"
|
||||
i18n-default-message="Sample"
|
||||
></label>
|
||||
<select id="sampleShardSize" class="kuiSelect type-input" ng-model="samplerShardSize" ng-change="samplerShardSizeChanged(samplerShardSize)">
|
||||
<option ng-value="1000">1000</option>
|
||||
<option ng-value="5000">5000</option>
|
||||
<option ng-value="10000">10000</option>
|
||||
<option ng-value="100000">100000</option>
|
||||
<option
|
||||
ng-value="-1"
|
||||
i18n-id="xpack.ml.datavisualizer.allOptionLabel"
|
||||
i18n-default-message="all"
|
||||
></option>
|
||||
</select>
|
||||
<span
|
||||
i18n-id="xpack.ml.datavisualizer.documentsPerShardDescription"
|
||||
i18n-default-message="documents per shard from a total of {wrappedTotalCount} {totalCount, plural, one {document} other {documents}}"
|
||||
i18n-values="{
|
||||
html_wrappedTotalCount: '<span class=\'field-emphasis\'>' + overallStats.totalCount + '</span>',
|
||||
totalCount: overallStats.totalCount
|
||||
}"
|
||||
></span>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-info-circle"
|
||||
tooltip-placement="right"
|
||||
tooltip="{{ ::'xpack.ml.datavisualizer.documentsPerShardTooltip' | i18n: {
|
||||
defaultMessage: 'Selecting a smaller sample size will reduce query run times and the load on the cluster.'
|
||||
} }}"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-show="metricCards.length > 0" class="kuiPanel kuiVerticalRhythm datavisualizer-panel card-panel">
|
||||
<div class="euiText">
|
||||
<h2
|
||||
class="kuiSubTitle kuiVerticalRhythm"
|
||||
i18n-id="xpack.ml.datavisualizer.metricsTitle"
|
||||
i18n-default-message="Metrics"
|
||||
></h2>
|
||||
</div>
|
||||
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--responsive euiFlexGroup--wrap">
|
||||
<div class="field-sort-controls euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive">
|
||||
<div ng-if="showAllMetrics === false" class="field-count-label euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<span
|
||||
i18n-id="xpack.ml.datavisualizer.showMetricsDescription"
|
||||
i18n-default-message="{wrappedMetricCardsCount} {metricCardsCount, plural, one {field exists} other {fields exist}} in documents ({totalMetricFieldCount} in total)"
|
||||
i18n-values="{
|
||||
html_wrappedMetricCardsCount: '<span class=\'field-emphasis\'>' + metricCards.length + '</span>',
|
||||
metricCardsCount: metricCards.length,
|
||||
html_totalMetricFieldCount: '<span class=\'field-emphasis\'>' + totalMetricFieldCount + '</span>'
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div ng-if="showAllMetrics === true" class="field-count-label euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<span
|
||||
i18n-id="xpack.ml.datavisualizer.showAllMetricsDescription"
|
||||
i18n-default-message="{wrappedMetricCardsCount} {metricCardsCount, plural, one {field} other {fields}} ({wrappedPopulatedMetricFieldCount} {populatedMetricFieldCount, plural, one {exists} other {exist}} in documents)"
|
||||
i18n-values="{
|
||||
html_wrappedMetricCardsCount: '<span class=\'field-emphasis\'>' + metricCards.length + '</span>',
|
||||
metricCardsCount: metricCards.length,
|
||||
html_wrappedPopulatedMetricFieldCount: '<span class=\'field-emphasis\'>' + populatedMetricFieldCount + '</span>',
|
||||
populatedMetricFieldCount
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="populatedMetricFieldCount === totalMetricFieldCount" class="field-group-input euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<input id="toggleAllMetricsCheckbox" type="checkbox" class="kuiCheckBox" ng-click="toggleAllMetrics()" ng-checked="showAllMetrics === true">
|
||||
<label
|
||||
for="toggleAllMetricsCheckbox"
|
||||
i18n-id="xpack.ml.datavisualizer.showEmptyFieldsLabel"
|
||||
i18n-default-message="show empty fields"
|
||||
></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-filter-controls euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<ml-form-filter-input
|
||||
filter="metricFieldFilter"
|
||||
filter-icon="metricFilterIcon"
|
||||
filter-changed="metricFieldFilterChanged"
|
||||
clear-filter="clearMetricFilter" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-container" ng-repeat="card in metricCards">
|
||||
<ml-field-data-card
|
||||
index-pattern="indexPattern"
|
||||
query="searchQuery"
|
||||
earliest="earliest"
|
||||
latest="latest"
|
||||
card="card">
|
||||
</ml-field-data-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="kuiPanel kuiVerticalRhythm datavisualizer-panel card-panel">
|
||||
<div class="euiText">
|
||||
<h2
|
||||
class="kuiSubTitle kuiVerticalRhythm"
|
||||
i18n-id="xpack.ml.datavisualizer.fieldsTitle"
|
||||
i18n-default-message="Fields"
|
||||
></h2>
|
||||
</div>
|
||||
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--responsive euiFlexGroup--wrap">
|
||||
<div class="field-sort-controls euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive">
|
||||
<div ng-if="showAllFields === false" class="field-count-label euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<span
|
||||
i18n-id="xpack.ml.datavisualizer.showMFieldDescription"
|
||||
i18n-default-message="{wrappedFieldCardsCount} {fieldCardsCount, plural, one {field exists} other {fields exist}} in documents ({totalNonMetricFieldCount} in total)"
|
||||
i18n-values="{
|
||||
html_wrappedFieldCardsCount: '<span class=\'field-emphasis\'>' + fieldCards.length + '</span>',
|
||||
fieldCardsCount: fieldCards.length,
|
||||
html_totalNonMetricFieldCount: '<span class=\'field-emphasis\'>' + totalNonMetricFieldCount + '</span>'
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-if="showAllFields === true" class="field-count-label euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<span
|
||||
i18n-id="xpack.ml.datavisualizer.showAllFieldsDescription"
|
||||
i18n-default-message="{wrappedFieldCardsCount} {fieldCardsCount, plural, one {field} other {fields}} ({wrappedPopulatedNonMetricFieldCount} {populatedNonMetricFieldCount, plural, one {exists} other {exist}} in documents)"
|
||||
i18n-values="{
|
||||
html_wrappedFieldCardsCount: '<span class=\'field-emphasis\'>' + fieldCards.length + '</span>',
|
||||
fieldCardsCount: fieldCards.length,
|
||||
html_wrappedPopulatedNonMetricFieldCount: '<span class=\'field-emphasis\'>' + populatedNonMetricFieldCount +'</span>',
|
||||
populatedNonMetricFieldCount
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div ng-hide="populatedNonMetricFieldCount === totalNonMetricFieldCount" class="field-group-input euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<input id="toggleAllFieldsCheckbox" type="checkbox" class="kuiCheckBox" ng-click="toggleAllFields()" ng-checked="showAllFields === true">
|
||||
<label
|
||||
for="toggleAllFieldsCheckbox"
|
||||
i18n-id="xpack.ml.datavisualizer.showEmptyFieldsLabel"
|
||||
i18n-default-message="show empty fields"
|
||||
></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-filter-controls euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--responsive">
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<select class="kuiSelect type-input"
|
||||
ng-model="filterFieldType"
|
||||
ng-change="filterFieldTypeChanged(filterFieldType)"
|
||||
aria-label="{{ ::'xpack.ml.datavisualizer.selectFieldTypesAriaLabel' | i18n: {
|
||||
defaultMessage: 'Select field types'
|
||||
} }}"
|
||||
>
|
||||
<option
|
||||
value="*"
|
||||
i18n-id="xpack.ml.datavisualizer.allFieldsTypeOptionLabel"
|
||||
i18n-default-message="All field types"
|
||||
></option>
|
||||
<option
|
||||
ng-repeat="type in indexedFieldTypes"
|
||||
value="{{type}}"
|
||||
i18n-id="xpack.ml.datavisualizer.typesOptionLabel"
|
||||
i18n-default-message="{type} types"
|
||||
i18n-values="{ type }"
|
||||
></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<ml-form-filter-input
|
||||
filter="fieldFilter"
|
||||
filter-icon="fieldFilterIcon"
|
||||
filter-changed="fieldFilterChanged"
|
||||
clear-filter="clearFieldFilter" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-container" ng-repeat="card in fieldCards">
|
||||
<ml-field-data-card
|
||||
index-pattern="indexPattern"
|
||||
query="searchQuery"
|
||||
earliest="earliest"
|
||||
latest="latest"
|
||||
card="card">
|
||||
</ml-field-data-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-if="showSidebar" class="datavisualizer-sidebar">
|
||||
<ng-include src="urlBasePath+'/plugins/ml/datavisualizer/datavisualizer_sidebar.html'"></ng-include>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ml-datavisualizer>
|
|
@ -1,686 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Angular controller for the Machine Learning data visualizer which allows the user
|
||||
* to explore the data in the fields in an index pattern prior to creating a job.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import rison from 'rison-node';
|
||||
|
||||
import 'plugins/ml/components/form_filter_input';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import uiRoutes from 'ui/routes';
|
||||
import { decorateQuery, luceneStringToDsl } from '@kbn/es-query';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import { ML_JOB_FIELD_TYPES, KBN_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types';
|
||||
import { getDataVisualizerBreadcrumbs } from './breadcrumbs';
|
||||
import { kbnTypeToMLJobType } from 'plugins/ml/util/field_types_utils';
|
||||
import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets';
|
||||
import { checkBasicLicense, isFullLicense } from 'plugins/ml/license/check_license';
|
||||
import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
|
||||
import { SearchItemsProvider } from 'plugins/ml/jobs/new_job/utils/new_job_utils';
|
||||
import { loadCurrentIndexPattern, loadCurrentSavedSearch, timeBasedIndexCheck } from 'plugins/ml/util/index_utils';
|
||||
import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
|
||||
import { ml } from 'plugins/ml/services/ml_api_service';
|
||||
import template from './datavisualizer.html';
|
||||
import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
|
||||
|
||||
uiRoutes
|
||||
.when('/jobs/new_job/datavisualizer', {
|
||||
template,
|
||||
k7Breadcrumbs: getDataVisualizerBreadcrumbs,
|
||||
resolve: {
|
||||
CheckLicense: checkBasicLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
indexPattern: loadCurrentIndexPattern,
|
||||
savedSearch: loadCurrentSavedSearch,
|
||||
checkMlNodesAvailable
|
||||
}
|
||||
});
|
||||
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module
|
||||
.controller('MlDataVisualizerViewFields', function (
|
||||
$scope,
|
||||
$timeout,
|
||||
$window,
|
||||
Private,
|
||||
AppState,
|
||||
config) {
|
||||
|
||||
timefilter.enableTimeRangeSelector();
|
||||
timefilter.enableAutoRefreshSelector();
|
||||
|
||||
const createSearchItems = Private(SearchItemsProvider);
|
||||
const {
|
||||
indexPattern,
|
||||
query } = createSearchItems();
|
||||
|
||||
timeBasedIndexCheck(indexPattern, true);
|
||||
|
||||
// List of system fields we don't want to display.
|
||||
// TODO - are we happy to ignore these fields?
|
||||
const OMIT_FIELDS = ['_source', '_type', '_index', '_id', '_version', '_score'];
|
||||
|
||||
$scope.metricCards = [];
|
||||
$scope.totalMetricFieldCount = 0;
|
||||
$scope.populatedMetricFieldCount = 0;
|
||||
$scope.showAllMetrics = false;
|
||||
$scope.fieldCards = [];
|
||||
$scope.totalNonMetricFieldCount = 0;
|
||||
$scope.populatedNonMetricFieldCount = 0;
|
||||
$scope.ML_JOB_FIELD_TYPES = ML_JOB_FIELD_TYPES;
|
||||
$scope.showAllFields = false;
|
||||
$scope.filterFieldType = '*';
|
||||
$scope.urlBasePath = chrome.getBasePath();
|
||||
$scope.appState = new AppState();
|
||||
|
||||
$scope.indexPattern = indexPattern;
|
||||
$scope.earliest = timefilter.getActiveBounds().min.valueOf();
|
||||
$scope.latest = timefilter.getActiveBounds().max.valueOf();
|
||||
|
||||
$scope.metricFilterIcon = 0;
|
||||
$scope.metricFieldFilter = '';
|
||||
$scope.fieldFilterIcon = 0;
|
||||
$scope.fieldFilter = '';
|
||||
$scope.recognizerResults = {
|
||||
count: 0,
|
||||
onChange() {
|
||||
$scope.$applyAsync();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showSidebar = isFullLicense();
|
||||
|
||||
// Check for a saved query in the AppState or via a savedSearchId in the URL.
|
||||
// TODO - add in support for lucene queries with filters and Kuery.
|
||||
$scope.searchQueryText = '';
|
||||
const queryBarQry = ($scope.appState.query !== undefined) ? ($scope.appState.query) : query;
|
||||
if (queryBarQry.language === 'lucene') {
|
||||
$scope.searchQueryText = _.get(queryBarQry, 'query', '');
|
||||
} else {
|
||||
toastNotifications.addWarning({
|
||||
title: i18n.translate('xpack.ml.datavisualizer.languageSyntaxNotSupportedWarningTitle', {
|
||||
defaultMessage: '{language} syntax not supported',
|
||||
values: {
|
||||
language: (queryBarQry.language !== undefined) ? queryBarQry.language : '',
|
||||
}
|
||||
}),
|
||||
text: i18n.translate('xpack.ml.datavisualizer.languageSyntaxNotSupportedWarningDescription', {
|
||||
defaultMessage: 'The Data Visualizer currently only supports queries using the lucene query syntax.',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
$scope.searchQuery = buildSearchQuery();
|
||||
|
||||
$scope.samplerShardSize = $scope.appState.samplerShardSize ?
|
||||
$scope.appState.samplerShardSize : 5000; // -1 indicates no sampling.
|
||||
|
||||
let metricFieldRegexp;
|
||||
let metricFieldFilterTimeout;
|
||||
let fieldRegexp;
|
||||
let fieldFilterTimeout;
|
||||
|
||||
// Obtain the list of non metric field types which appear in the index pattern.
|
||||
let indexedFieldTypes = [];
|
||||
_.each(indexPattern.fields, (field) => {
|
||||
if (!field.scripted) {
|
||||
const dataVisualizerType = kbnTypeToMLJobType(field);
|
||||
if (dataVisualizerType !== undefined) {
|
||||
indexedFieldTypes.push(dataVisualizerType);
|
||||
}
|
||||
}
|
||||
});
|
||||
indexedFieldTypes = _.chain(indexedFieldTypes)
|
||||
.unique()
|
||||
.without(ML_JOB_FIELD_TYPES.NUMBER)
|
||||
.value();
|
||||
$scope.indexedFieldTypes = indexedFieldTypes.sort();
|
||||
|
||||
function refresh() {
|
||||
$scope.earliest = timefilter.getActiveBounds().min.valueOf();
|
||||
$scope.latest = timefilter.getActiveBounds().max.valueOf();
|
||||
loadOverallStats();
|
||||
}
|
||||
|
||||
// Refresh the data when the time range is altered.
|
||||
$scope.$listenAndDigestAsync(timefilter, 'fetch', refresh);
|
||||
|
||||
const timefilterRefreshServiceSub = mlTimefilterRefresh$.subscribe(refresh);
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
timefilterRefreshServiceSub.unsubscribe();
|
||||
});
|
||||
|
||||
$scope.submitSearchQuery = function () {
|
||||
$scope.searchQuery = buildSearchQuery();
|
||||
saveAppState();
|
||||
loadOverallStats();
|
||||
};
|
||||
|
||||
$scope.samplerShardSizeChanged = function () {
|
||||
saveAppState();
|
||||
loadOverallStats();
|
||||
};
|
||||
|
||||
$scope.toggleAllMetrics = function () {
|
||||
$scope.showAllMetrics = !$scope.showAllMetrics;
|
||||
createMetricCards();
|
||||
};
|
||||
|
||||
$scope.toggleAllFields = function () {
|
||||
$scope.showAllFields = !$scope.showAllFields;
|
||||
createNonMetricCards();
|
||||
};
|
||||
|
||||
$scope.filterFieldTypeChanged = function (fieldType) {
|
||||
$scope.filterFieldType = fieldType;
|
||||
createNonMetricCards();
|
||||
};
|
||||
|
||||
$scope.metricFieldFilterChanged = function () {
|
||||
// Clear the previous filter timeout.
|
||||
if (metricFieldFilterTimeout !== undefined) {
|
||||
$timeout.cancel(metricFieldFilterTimeout);
|
||||
}
|
||||
|
||||
// Create a timeout to recreate the metric configurations based on the filter.
|
||||
// A timeout of 1.5s is used as the user may still be in the process of typing the filter
|
||||
// when this function is first called.
|
||||
metricFieldFilterTimeout = $timeout(() => {
|
||||
if ($scope.metricFieldFilter && $scope.metricFieldFilter !== '') {
|
||||
metricFieldRegexp = new RegExp('(' + $scope.metricFieldFilter + ')', 'gi');
|
||||
} else {
|
||||
metricFieldRegexp = undefined;
|
||||
}
|
||||
|
||||
createMetricCards();
|
||||
metricFieldFilterTimeout = undefined;
|
||||
}, 1500);
|
||||
|
||||
// Display the spinner icon after 250ms of typing.
|
||||
// The spinner is a nice way of showing that something is
|
||||
// happening as we're stalling for the user to stop typing.
|
||||
$timeout(() => {
|
||||
$scope.metricFilterIcon = 1;
|
||||
}, 250);
|
||||
|
||||
};
|
||||
|
||||
$scope.clearMetricFilter = function () {
|
||||
$scope.metricFieldFilter = '';
|
||||
metricFieldRegexp = undefined;
|
||||
createMetricCards();
|
||||
};
|
||||
|
||||
$scope.fieldFilterChanged = function () {
|
||||
// Clear the previous filter timeout.
|
||||
if (fieldFilterTimeout !== undefined) {
|
||||
$timeout.cancel(fieldFilterTimeout);
|
||||
}
|
||||
|
||||
// Create a timeout to recreate the non-metric field configurations based on the filter.
|
||||
// A timeout of 1.5s is used as the user may still be in the process of typing the filter
|
||||
// when this function is first called.
|
||||
fieldFilterTimeout = $timeout(() => {
|
||||
if ($scope.fieldFilter && $scope.fieldFilter !== '') {
|
||||
fieldRegexp = new RegExp('(' + $scope.fieldFilter + ')', 'gi');
|
||||
} else {
|
||||
fieldRegexp = undefined;
|
||||
}
|
||||
|
||||
createNonMetricCards();
|
||||
fieldFilterTimeout = undefined;
|
||||
}, 1500);
|
||||
|
||||
// Display the spinner icon after 250ms of typing.
|
||||
// the spinner is a nice way of showing that something is
|
||||
// happening as we're stalling for the user to stop trying.
|
||||
$timeout(() => {
|
||||
$scope.fieldFilterIcon = 1;
|
||||
}, 250);
|
||||
};
|
||||
|
||||
$scope.clearFieldFilter = function () {
|
||||
$scope.fieldFilter = '';
|
||||
fieldRegexp = undefined;
|
||||
createNonMetricCards();
|
||||
};
|
||||
|
||||
$scope.createJob = function () {
|
||||
// TODO - allow the user to select metrics and fields and use
|
||||
// the selection to open the appropriate job wizard (single, multi-metric etc).
|
||||
// For now just open the Advanced wizard, passing in the index pattern ID.
|
||||
const _a = rison.encode({
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: $scope.searchQueryText
|
||||
}
|
||||
});
|
||||
|
||||
const path = `${$scope.urlBasePath}/app/ml#/jobs/new_job/advanced?index=${$scope.indexPattern}&_a=${_a}`;
|
||||
$window.open(path, '_self');
|
||||
};
|
||||
|
||||
function buildSearchQuery() {
|
||||
const searchQuery = luceneStringToDsl($scope.searchQueryText);
|
||||
const queryStringOptions = config.get('query:queryString:options');
|
||||
decorateQuery(searchQuery, queryStringOptions);
|
||||
return searchQuery;
|
||||
}
|
||||
|
||||
function saveAppState() {
|
||||
$scope.appState.query = {
|
||||
language: 'lucene',
|
||||
query: $scope.searchQueryText
|
||||
};
|
||||
$scope.appState.samplerShardSize = $scope.samplerShardSize;
|
||||
$scope.appState.save();
|
||||
}
|
||||
|
||||
function createMetricCards() {
|
||||
$scope.metricCards.length = 0;
|
||||
|
||||
const aggregatableExistsFields = $scope.overallStats.aggregatableExistsFields || [];
|
||||
|
||||
let allMetricFields = [];
|
||||
if (metricFieldRegexp === undefined) {
|
||||
allMetricFields = _.filter(indexPattern.fields, (f) => {
|
||||
return (f.type === KBN_FIELD_TYPES.NUMBER && !_.contains(OMIT_FIELDS, f.displayName));
|
||||
});
|
||||
} else {
|
||||
allMetricFields = _.filter(indexPattern.fields, (f) => {
|
||||
return (f.type === KBN_FIELD_TYPES.NUMBER &&
|
||||
!_.contains(OMIT_FIELDS, f.displayName) &&
|
||||
f.displayName.match(metricFieldRegexp));
|
||||
});
|
||||
}
|
||||
|
||||
const metricExistsFields = _.filter(allMetricFields, (f) => {
|
||||
return _.find(aggregatableExistsFields, { fieldName: f.displayName });
|
||||
});
|
||||
|
||||
const metricCards = [];
|
||||
|
||||
// Add a config for 'document count', identified by no field name if index is timeseries based
|
||||
if (indexPattern.timeFieldName !== undefined) {
|
||||
metricCards.push({
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
loading: true
|
||||
});
|
||||
} else {
|
||||
// disable timeRangeSelector and remove sidebar if index not timeseries based
|
||||
timefilter.disableTimeRangeSelector();
|
||||
$scope.showSidebar = false;
|
||||
}
|
||||
|
||||
// Add on 1 for the document count card.
|
||||
// TODO - remove the '+1' if document count goes in its own section.
|
||||
$scope.totalMetricFieldCount = allMetricFields.length + 1;
|
||||
$scope.populatedMetricFieldCount = metricExistsFields.length + 1;
|
||||
if ($scope.totalMetricFieldCount === $scope.populatedMetricFieldCount) {
|
||||
$scope.showAllMetrics = true;
|
||||
}
|
||||
|
||||
let aggregatableFields = $scope.overallStats.aggregatableExistsFields;
|
||||
if ($scope.showAllMetrics === true) {
|
||||
aggregatableFields = aggregatableFields.concat($scope.overallStats.aggregatableNotExistsFields);
|
||||
}
|
||||
|
||||
const metricFields = $scope.showAllMetrics ? allMetricFields : metricExistsFields;
|
||||
_.each(metricFields, (field) => {
|
||||
const fieldData = _.find(aggregatableFields, { fieldName: field.displayName });
|
||||
|
||||
const card = {
|
||||
...fieldData,
|
||||
fieldFormat: field.format,
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
loading: true
|
||||
};
|
||||
|
||||
metricCards.push(card);
|
||||
|
||||
});
|
||||
|
||||
$scope.metricCards = metricCards;
|
||||
loadMetricFieldStats();
|
||||
}
|
||||
|
||||
function createNonMetricCards() {
|
||||
$scope.fieldCards.length = 0;
|
||||
|
||||
let allNonMetricFields = [];
|
||||
if ($scope.filterFieldType === '*') {
|
||||
allNonMetricFields = _.filter(indexPattern.fields, (f) => {
|
||||
return (f.type !== KBN_FIELD_TYPES.NUMBER && !_.contains(OMIT_FIELDS, f.displayName));
|
||||
});
|
||||
} else {
|
||||
if ($scope.filterFieldType === ML_JOB_FIELD_TYPES.TEXT ||
|
||||
$scope.filterFieldType === ML_JOB_FIELD_TYPES.KEYWORD) {
|
||||
const aggregatableCheck = $scope.filterFieldType === ML_JOB_FIELD_TYPES.KEYWORD ? true : false;
|
||||
allNonMetricFields = _.filter(indexPattern.fields, (f) => {
|
||||
return !_.contains(OMIT_FIELDS, f.displayName) &&
|
||||
(f.type === KBN_FIELD_TYPES.STRING) &&
|
||||
(f.aggregatable === aggregatableCheck);
|
||||
});
|
||||
} else {
|
||||
allNonMetricFields = _.filter(indexPattern.fields, (f) => {
|
||||
return (!_.contains(OMIT_FIELDS, f.displayName) && (f.type === $scope.filterFieldType));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If a field filter has been entered, perform another filter on the entered regexp.
|
||||
if (fieldRegexp !== undefined) {
|
||||
allNonMetricFields = _.filter(allNonMetricFields, (f) => {
|
||||
return (f.displayName.match(fieldRegexp));
|
||||
});
|
||||
}
|
||||
|
||||
$scope.totalNonMetricFieldCount = allNonMetricFields.length;
|
||||
|
||||
// Obtain the list of all non-metric fields which appear in documents
|
||||
// (aggregatable or not aggregatable).
|
||||
const populatedNonMetricFields = []; // Kibana index pattern non metric fields.
|
||||
let nonMetricFieldData = []; // Basic non metric field data loaded from requesting overall stats.
|
||||
let populatedNonMetricFieldCount = 0;
|
||||
_.each(allNonMetricFields, (f) => {
|
||||
const checkAggregatableField = _.find($scope.overallStats.aggregatableExistsFields, { fieldName: f.displayName });
|
||||
if (checkAggregatableField !== undefined) {
|
||||
populatedNonMetricFields.push(f);
|
||||
nonMetricFieldData.push(checkAggregatableField);
|
||||
populatedNonMetricFieldCount++;
|
||||
} else {
|
||||
const checkNonAggregatableField = _.find($scope.overallStats.nonAggregatableExistsFields, { fieldName: f.displayName });
|
||||
if (checkNonAggregatableField !== undefined) {
|
||||
populatedNonMetricFields.push(f);
|
||||
nonMetricFieldData.push(checkNonAggregatableField);
|
||||
populatedNonMetricFieldCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.populatedNonMetricFieldCount = populatedNonMetricFieldCount;
|
||||
if ($scope.totalNonMetricFieldCount === $scope.populatedNonMetricFieldCount) {
|
||||
$scope.showAllFields = true;
|
||||
}
|
||||
|
||||
const nonMetricFieldsToShow = $scope.showAllFields === true ? allNonMetricFields : populatedNonMetricFields;
|
||||
|
||||
// Combine the field data obtained from Elasticsearch into a single array.
|
||||
if ($scope.showAllFields === true) {
|
||||
nonMetricFieldData = nonMetricFieldData.concat(
|
||||
$scope.overallStats.aggregatableNotExistsFields,
|
||||
$scope.overallStats.nonAggregatableNotExistsFields);
|
||||
}
|
||||
|
||||
const fieldCards = [];
|
||||
|
||||
_.each(nonMetricFieldsToShow, (field) => {
|
||||
const fieldData = _.find(nonMetricFieldData, { fieldName: field.displayName });
|
||||
|
||||
const card = {
|
||||
...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) {
|
||||
card.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.
|
||||
card.type = field.type;
|
||||
card.isUnsupportedType = true;
|
||||
}
|
||||
|
||||
fieldCards.push(card);
|
||||
});
|
||||
|
||||
$scope.fieldCards = _.sortBy(fieldCards, 'fieldName');
|
||||
loadNonMetricFieldStats();
|
||||
|
||||
}
|
||||
|
||||
function loadMetricFieldStats() {
|
||||
// Only request data for fields that exist in documents.
|
||||
let numberFields = _.filter($scope.metricCards, { existsInDocs: true });
|
||||
|
||||
// Pass the field name, type and cardinality in the request.
|
||||
// Top values will be obtained on a sample if cardinality > 100000.
|
||||
numberFields = _.map(numberFields, (card) => {
|
||||
const props = { fieldName: card.fieldName, type: card.type };
|
||||
if (_.has(card, ['stats', 'cardinality'])) {
|
||||
props.cardinality = card.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 bounds = timefilter.getActiveBounds();
|
||||
const BAR_TARGET = 75;
|
||||
buckets.setInterval('auto');
|
||||
buckets.setBounds(bounds);
|
||||
buckets.setBarTarget(BAR_TARGET);
|
||||
const aggInterval = buckets.getInterval();
|
||||
|
||||
ml.getVisualizerFieldStats({
|
||||
indexPatternTitle: indexPattern.title,
|
||||
query: $scope.searchQuery,
|
||||
timeFieldName: indexPattern.timeFieldName,
|
||||
earliest: $scope.earliest,
|
||||
latest: $scope.latest,
|
||||
samplerShardSize: $scope.samplerShardSize,
|
||||
interval: aggInterval.expression,
|
||||
fields: numberFields
|
||||
})
|
||||
.then((resp) => {
|
||||
// Add the metric stats to the existing stats in the corresponding card. [ {documentCounts:...}, {fieldName: ..} ]
|
||||
_.each($scope.metricCards, (card) => {
|
||||
if (card.fieldName !== undefined) {
|
||||
card.stats = { ...card.stats, ...(_.find(resp, { fieldName: card.fieldName })) };
|
||||
} else {
|
||||
// Document count card.
|
||||
card.stats = _.find(resp, (stats) => {
|
||||
return stats.fieldName === undefined;
|
||||
});
|
||||
}
|
||||
|
||||
card.loading = false;
|
||||
});
|
||||
|
||||
// Clear the filter spinner if it's running.
|
||||
$scope.metricFilterIcon = 0;
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO - display error in cards saying data could not be loaded.
|
||||
console.log('DataVisualizer - error getting stats for metric cards from elasticsearch:', err);
|
||||
if (err.statusCode === 500) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.datavisualizer.metricInternalServerErrorTitle', {
|
||||
defaultMessage: 'Error loading data for metrics in index {index}. {message}. ' +
|
||||
'The request may have timed out. Try using a smaller sample size or narrowing the time range.',
|
||||
values: {
|
||||
index: indexPattern.title,
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.datavisualizer.loadingMetricDataErrorTitle', {
|
||||
defaultMessage: 'Error loading data for metrics in index {index}. {message}',
|
||||
values: {
|
||||
index: indexPattern.title,
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
$scope.$applyAsync();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function loadNonMetricFieldStats() {
|
||||
// Only request data for fields that exist in documents.
|
||||
let fields = _.filter($scope.fieldCards, { existsInDocs: true });
|
||||
|
||||
// Pass the field name, type and cardinality in the request.
|
||||
// Top values will be obtained on a sample if cardinality > 100000.
|
||||
fields = _.map(fields, (card) => {
|
||||
const props = { fieldName: card.fieldName, type: card.type };
|
||||
if (_.has(card, ['stats', 'cardinality'])) {
|
||||
props.cardinality = card.stats.cardinality;
|
||||
}
|
||||
return props;
|
||||
});
|
||||
|
||||
if (fields.length > 0) {
|
||||
|
||||
ml.getVisualizerFieldStats({
|
||||
indexPatternTitle: indexPattern.title,
|
||||
query: $scope.searchQuery,
|
||||
fields: fields,
|
||||
timeFieldName: indexPattern.timeFieldName,
|
||||
earliest: $scope.earliest,
|
||||
latest: $scope.latest,
|
||||
samplerShardSize: $scope.samplerShardSize,
|
||||
maxExamples: 10
|
||||
})
|
||||
.then((resp) => {
|
||||
// Add the metric stats to the existing stats in the corresponding card.
|
||||
_.each($scope.fieldCards, (card) => {
|
||||
card.stats = { ...card.stats, ...(_.find(resp, { fieldName: card.fieldName })) };
|
||||
card.loading = false;
|
||||
});
|
||||
|
||||
// Clear the filter spinner if it's running.
|
||||
$scope.fieldFilterIcon = 0;
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO - display error in cards saying data could not be loaded.
|
||||
console.log('DataVisualizer - error getting non metric field stats from elasticsearch:', err);
|
||||
if (err.statusCode === 500) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.datavisualizer.fieldsInternalServerErrorTitle', {
|
||||
defaultMessage: 'Error loading data for fields in index {index}. {message}. ' +
|
||||
'The request may have timed out. Try using a smaller sample size or narrowing the time range.',
|
||||
values: {
|
||||
index: indexPattern.title,
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.datavisualizer.loadingFieldsDataErrorTitle', {
|
||||
defaultMessage: 'Error loading data for fields in index {index}. {message}',
|
||||
values: {
|
||||
index: indexPattern.title,
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
$scope.$applyAsync();
|
||||
});
|
||||
} else {
|
||||
$scope.fieldFilterIcon = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function loadOverallStats() {
|
||||
const aggregatableFields = [];
|
||||
const nonAggregatableFields = [];
|
||||
_.each(indexPattern.fields, (field) => {
|
||||
if (OMIT_FIELDS.indexOf(field.displayName) === -1) {
|
||||
if (field.aggregatable === true) {
|
||||
aggregatableFields.push(field.displayName);
|
||||
} else {
|
||||
nonAggregatableFields.push(field.displayName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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.
|
||||
ml.getVisualizerOverallStats({
|
||||
indexPatternTitle: indexPattern.title,
|
||||
query: $scope.searchQuery,
|
||||
timeFieldName: indexPattern.timeFieldName,
|
||||
samplerShardSize: $scope.samplerShardSize,
|
||||
earliest: $scope.earliest,
|
||||
latest: $scope.latest,
|
||||
aggregatableFields: aggregatableFields,
|
||||
nonAggregatableFields: nonAggregatableFields
|
||||
})
|
||||
.then((resp) => {
|
||||
$scope.overallStats = resp;
|
||||
createMetricCards();
|
||||
createNonMetricCards();
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO - display error in cards saying data could not be loaded.
|
||||
console.log('DataVisualizer - error getting overall stats from elasticsearch:', err);
|
||||
if (err.statusCode === 500) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.datavisualizer.overallFieldsInternalServerErrorTitle', {
|
||||
defaultMessage: 'Error loading data for fields in index {index}. {message}. ' +
|
||||
'The request may have timed out. Try using a smaller sample size or narrowing the time range.',
|
||||
values: {
|
||||
index: indexPattern.title,
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.datavisualizer.loadingOverallFieldsDataErrorTitle', {
|
||||
defaultMessage: 'Error loading data for fields in index {index}. {message}',
|
||||
values: {
|
||||
index: indexPattern.title,
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
$scope.$applyAsync();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
loadOverallStats();
|
||||
|
||||
});
|
|
@ -20,11 +20,11 @@ import {
|
|||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { isFullLicense } from '../../license/check_license';
|
||||
import { isFullLicense } from '../license/check_license';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
|
||||
import { NavigationMenu } from '../../components/navigation_menu/navigation_menu';
|
||||
import { NavigationMenu } from '../components/navigation_menu/navigation_menu';
|
||||
|
||||
function startTrialDescription() {
|
||||
return (
|
||||
|
@ -146,27 +146,6 @@ export const DatavisualizerSelector = injectI18n(function (props) {
|
|||
data-test-subj="mlDataVisualizerCardIndexData"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
icon={<EuiIcon size="xxl" type="dataVisualizer" />}
|
||||
title="Select an index pattern NEW"
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.datavisualizer.selector.selectIndexPatternDescription"
|
||||
defaultMessage="Visualize the data in an existing Elasticsearch index."
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<EuiButton target="_self" href="#data_visualizer_index_select">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.datavisualizer.selector.selectIndexButtonLabel"
|
||||
defaultMessage="Select index"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
data-test-subj="mlDataVisualizerCardIndexData"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{startTrialVisible === true && (
|
||||
<Fragment>
|
|
@ -1,59 +0,0 @@
|
|||
<div class="kuiPanel kuiVerticalRhythm datavisualizer-panel euiText">
|
||||
<h2
|
||||
i18n-id="xpack.ml.datavisualizer.sidebar.createJobTitle"
|
||||
i18n-default-message="Create Job"
|
||||
></h2>
|
||||
|
||||
<div class="create-job-content">
|
||||
<div class="recognized-job-content" ng-show="recognizerResults.count!==0">
|
||||
<p
|
||||
i18n-id="xpack.ml.datavisualizer.sidebar.selectKnownConfigurationDescription"
|
||||
i18n-default-message="Select known configurations for recognized data:"
|
||||
></p>
|
||||
|
||||
<ml-data-recognizer
|
||||
class-name='"euiFlexGroup euiFlexGrid--gutterLarge euiFlexGroup--responsive euiFlexGroup--wrap"'
|
||||
index-pattern='indexPattern'
|
||||
results=recognizerResults/>
|
||||
</div>
|
||||
|
||||
<p
|
||||
i18n-id="xpack.ml.datavisualizer.sidebar.createJobDescription"
|
||||
i18n-default-message="Use the Advanced job wizard to create a job to find anomalies in this data:"
|
||||
></p>
|
||||
|
||||
<a ng-click="createJob()" ng-class='{disabled: isTimeBasedIndex===false}' class="euiLink synopsis">
|
||||
<div class="euiPanel euiPanel--paddingMedium synopsisPanel">
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive">
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero">
|
||||
<icon
|
||||
type="'createAdvancedJob'"
|
||||
size="'xl'"
|
||||
aria-label="{{ ::'xpack.ml.datavisualizer.sidebar.advancedJobAriaLabel' | i18n: {
|
||||
defaultMessage: 'Advanced job'
|
||||
} }}"
|
||||
class="synopsisIcon"
|
||||
/>
|
||||
</div>
|
||||
<div class="euiFlexItem synopsisContent">
|
||||
<h4
|
||||
class="euiTitle euiTitle--small synopsisTitle"
|
||||
i18n-id="xpack.ml.datavisualizer.sidebar.advancedTitle"
|
||||
i18n-default-message="Advanced"
|
||||
></h4>
|
||||
<div class="euiText synopsisBody">
|
||||
<p>
|
||||
<span
|
||||
class="euiTextColor euiTextColor--subdued"
|
||||
i18n-id="xpack.ml.datavisualizer.sidebar.advancedDescription"
|
||||
i18n-default-message="Use the full range of options to create a job for more advanced use cases"
|
||||
></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -10,9 +10,9 @@ import { wrapInI18nContext } from 'ui/i18n';
|
|||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { getDataVisualizerBreadcrumbs } from '../breadcrumbs';
|
||||
import { checkBasicLicense } from 'plugins/ml/license/check_license';
|
||||
import { checkFindFileStructurePrivilege } from 'plugins/ml/privilege/check_privilege';
|
||||
import { getDataVisualizerBreadcrumbs } from './breadcrumbs';
|
||||
import { checkBasicLicense } from '../license/check_license';
|
||||
import { checkFindFileStructurePrivilege } from '../privilege/check_privilege';
|
||||
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
|
||||
import { ML_BREADCRUMB } from '../breadcrumbs';
|
||||
import { ML_BREADCRUMB } from '../../breadcrumbs';
|
||||
|
||||
|
||||
export function getFileDataVisualizerBreadcrumbs() {
|
|
@ -1,9 +1,13 @@
|
|||
// SASSTODO: This entire sass file needs to be rewritten, using a color blind viz palette and proper vars.
|
||||
// This will need to be done in a more thorough cleanup.
|
||||
.card-container {
|
||||
display: inline-grid;
|
||||
display: -ms-inline-grid;
|
||||
padding: 0px 10px 10px 0px;
|
||||
}
|
||||
|
||||
.ml-field-data-card {
|
||||
width: 360px;
|
||||
height: 435px;
|
||||
// These styles should all be removed once the file data visualizer is using
|
||||
// the same field_data_card component as the index based data visualizer.
|
||||
height: 408px;
|
||||
|
||||
.boolean {
|
||||
background-color: #e6c220;
|
||||
|
@ -45,7 +49,8 @@
|
|||
@include euiPanel($selector: 'card-contents');
|
||||
|
||||
.card-contents {
|
||||
height: 400px;
|
||||
height: 378px;
|
||||
line-height: 21px;
|
||||
border-radius: 0px 0px $euiBorderRadius $euiBorderRadius;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -103,56 +108,6 @@
|
|||
padding-top: 5px;
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 11px;
|
||||
font-family: $euiFontFamily;
|
||||
|
||||
text {
|
||||
fill: $euiColorDarkShade;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.distribution-chart {
|
||||
|
||||
.x.axis path, .x.axis line {
|
||||
fill: none;
|
||||
stroke: #cccccc;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.area {
|
||||
fill: #3185fc;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ml-metric-distribution-chart {
|
||||
display: block;
|
||||
padding: 0px 15px 10px 15px;
|
||||
}
|
||||
|
||||
ml-document-count-chart {
|
||||
display: block;
|
||||
padding: 18px 15px 0px 15px;
|
||||
rect.bar {
|
||||
fill: #db1374;
|
||||
}
|
||||
|
||||
.bar {
|
||||
stroke: #ffffff;
|
||||
}
|
||||
|
||||
.axis path, .axis line {
|
||||
fill: none;
|
||||
stroke: #cccccc;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
}
|
||||
|
||||
.top-value {
|
||||
height: 21px;
|
||||
font-size: 13px;
|
|
@ -10,9 +10,9 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { FieldTypeIcon } from '../../../components/field_type_icon';
|
||||
import { DisplayValue } from '../../../components/display_value';
|
||||
import { getMLJobTypeAriaLabel } from '../../../util/field_types_utils';
|
||||
import { FieldTypeIcon } from '../../../../components/field_type_icon';
|
||||
import { DisplayValue } from '../../../../components/display_value';
|
||||
import { getMLJobTypeAriaLabel } from '../../../../util/field_types_utils';
|
||||
|
||||
export function FieldStatsCard({ field }) {
|
||||
|
|
@ -11,8 +11,8 @@ import React, {
|
|||
|
||||
import { FieldStatsCard } from './field_stats_card';
|
||||
import { getFieldNames } from './get_field_names';
|
||||
import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types';
|
||||
import { roundToDecimalPlace } from '../../../formatters/round_to_decimal_place';
|
||||
import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types';
|
||||
import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place';
|
||||
|
||||
export class FieldsStats extends Component {
|
||||
constructor(props) {
|
|
@ -13,7 +13,7 @@ import {
|
|||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { MLJobEditor, EDITOR_MODE } from '../../../jobs/jobs_list/components/ml_job_editor';
|
||||
import { MLJobEditor, EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
|
||||
|
||||
export function FileContents({ data, format, numberOfLines }) {
|
||||
let mode = EDITOR_MODE.TEXT;
|
|
@ -1,10 +1,8 @@
|
|||
.file-datavisualizer-file-picker {
|
||||
.euiFilePicker__wrap{
|
||||
max-width: inherit;
|
||||
}
|
||||
max-width: inherit;
|
||||
|
||||
.euiFilePicker__prompt {
|
||||
background-color: $euiColorEmptyShade;
|
||||
padding: $euiSizeXXL;
|
||||
box-shadow: none;
|
||||
|
||||
.euiFilePicker__icon {
|
|
@ -16,14 +16,14 @@ import {
|
|||
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { ml } from '../../../../services/ml_api_service';
|
||||
import { AboutPanel, LoadingPanel } from '../about_panel';
|
||||
import { BottomBar } from '../bottom_bar';
|
||||
import { ResultsView } from '../results_view';
|
||||
import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts';
|
||||
import { EditFlyout } from '../edit_flyout';
|
||||
import { ImportView } from '../import_view';
|
||||
import { MAX_BYTES } from '../../../../common/constants/file_datavisualizer';
|
||||
import { MAX_BYTES } from '../../../../../common/constants/file_datavisualizer';
|
||||
import {
|
||||
readFile,
|
||||
createUrlOverrides,
|
|
@ -17,7 +17,7 @@ import {
|
|||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { MLJobEditor, EDITOR_MODE } from '../../../jobs/jobs_list/components/ml_job_editor';
|
||||
import { MLJobEditor, EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
|
||||
const EDITOR_HEIGHT = '300px';
|
||||
|
||||
function AdvancedSettingsUi({
|
|
@ -25,8 +25,8 @@ import { ImportErrors } from '../import_errors';
|
|||
import { ImportSummary } from '../import_summary';
|
||||
import { ImportSettings } from '../import_settings';
|
||||
import { ExperimentalBadge } from '../experimental_badge';
|
||||
import { getIndexPatternNames, refreshIndexPatterns } from '../../../util/index_utils';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { getIndexPatternNames, refreshIndexPatterns } from '../../../../util/index_utils';
|
||||
import { ml } from '../../../../services/ml_api_service';
|
||||
import { hasImportPermission } from '../utils';
|
||||
|
||||
const DEFAULT_TIME_FIELD = '@timestamp';
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
|
||||
import { ES_FIELD_TYPES } from '../../../../../common/constants/field_types';
|
||||
import { ES_FIELD_TYPES } from '../../../../../../common/constants/field_types';
|
||||
import { Importer } from './importer';
|
||||
import Papa from 'papaparse';
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
|
||||
import { ml } from '../../../../services/ml_api_service';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
import { chunk } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
|
@ -19,10 +19,10 @@ import {
|
|||
|
||||
import moment from 'moment';
|
||||
import uiChrome from 'ui/chrome';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { isFullLicense } from '../../../license/check_license';
|
||||
import { checkPermission } from '../../../privilege/check_privilege';
|
||||
import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
|
||||
import { ml } from '../../../../services/ml_api_service';
|
||||
import { isFullLicense } from '../../../../license/check_license';
|
||||
import { checkPermission } from '../../../../privilege/check_privilege';
|
||||
import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes';
|
||||
|
||||
const RECHECK_DELAY_MS = 3000;
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue