[ML] Delete old AngularJS data visualizer and refactor folders (#42962) (#43015)

* [ML] Delete old AngularJS data visualizer - Resolve merge conflicts

* [ML] Fix imports

* [ML] Updating translation files
This commit is contained in:
Pete Harverson 2019-08-09 13:17:21 +01:00 committed by GitHub
parent 7cd37f3c51
commit 76e1deb8a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
160 changed files with 94 additions and 2618 deletions

View file

@ -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';

View file

@ -1 +0,0 @@
@import 'field_data_card';

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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' }
);
});

View file

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

View file

@ -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>

View file

@ -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');
};
});

View file

@ -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';

View file

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

View file

@ -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>

View file

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

View file

@ -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();
});
});
});

View file

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

View file

@ -1 +1,2 @@
@import 'datavisualizer';
@import 'file_based/index';
@import 'index_based/index';

View file

@ -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>

View file

@ -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();
});

View file

@ -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>

View file

@ -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>

View file

@ -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';

View file

@ -5,7 +5,7 @@
*/
import { ML_BREADCRUMB } from '../breadcrumbs';
import { ML_BREADCRUMB } from '../../breadcrumbs';
export function getFileDataVisualizerBreadcrumbs() {

View file

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

View file

@ -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 }) {

View file

@ -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) {

View file

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

View file

@ -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 {

View file

@ -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,

View file

@ -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({

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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