Merge remote-tracking branch 'origin/master' into feature/merge-code

This commit is contained in:
Fuyao Zhao 2019-01-09 10:17:16 -08:00
commit 78af65ebad
183 changed files with 6936 additions and 1509 deletions

View file

@ -7,7 +7,7 @@
Elastic Application Performance Monitoring (APM) automatically collects in-depth
performance metrics and errors from inside your applications.
The **APM** page in {kib} is provided with the {xpack} basic license. It
The **APM** page in {kib} is provided with the basic license. It
enables developers to drill down into the performance data for their applications
and quickly locate the performance bottlenecks.

View file

@ -14,8 +14,8 @@ with over 120 reusable grok patterns. See
https://github.com/elastic/elasticsearch/tree/master/modules/ingest-common/src/main/resources/patterns[Ingest node grok patterns] and https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns[Logstash grok patterns]
for the full list of patterns.
{xpack} includes a Grok Debugger tool that you can use to build and debug
grok patterns before you use them in your data processing pipelines. Because
You can build and debug grok patterns in the Grok Debugger tool in {kib}
before you use them in your data processing pipelines. Because
ingest node and Logstash share the same grok implementation and pattern
libraries, any grok pattern that you create in the Grok Debugger will work
in ingest node and Logstash.

View file

@ -9,7 +9,7 @@ ifdef::gs-mini[]
== Getting Started
endif::gs-mini[]
The Search Profiler is automatically enabled in {kib}. It is located under the
The {searchprofiler} is automatically enabled in {kib}. It is located under the
*Dev Tools* tab in {kib}.
[[first-profile]]
@ -18,24 +18,24 @@ To start profiling queries:
. Open Kibana in your web browser and log in. If you are running Kibana
locally, go to `http://localhost:5601/`.
. Click **DevTools** in the side navigation to open the Search Profiler.
. Click **DevTools** in the side navigation to open the {searchprofiler}.
Console is the default tool to open when first accessing DevTools.
+
image::dev-tools/searchprofiler/images/gs1.png["Opening DevTools"]
+
On the top navigation bar, click the second item: *Search Profiler*
+
image::dev-tools/searchprofiler/images/gs2.png["Opening the Search Profiler"]
image::dev-tools/searchprofiler/images/gs2.png["Opening the {searchprofiler}"]
. This opens the Search Profiler interface.
. This opens the {searchprofiler} interface.
+
image::dev-tools/searchprofiler/images/gs3.png["Search Profiler Interface"]
image::dev-tools/searchprofiler/images/gs3.png["{searchprofiler} Interface"]
. Replace the default `match_all` query with the query you want to profile and click *Profile*.
+
image::dev-tools/searchprofiler/images/gs4.png["Profiling the match_all query"]
+
Search Profiler displays the names of the indices searched, the shards in each index,
{searchprofiler} displays the names of the indices searched, the shards in each index,
and how long the query took. The following example shows the results of profiling
the match_all query. Three indices were searched: `.monitoring-kibana-2-2016.11.30`,
`.monitoring-data-2` and `test`.
@ -64,7 +64,7 @@ This displays details about the query component(s) that ran on the shard.
+
In this example, there is a single `"MatchAllDocsQuery"` that ran on the shard.
Since it was the only query run, it took 100% of the time. When you mouse over
a row, the Search Profiler displays additional information about the query component."
a row, the {searchprofiler} displays additional information about the query component."
+
image::dev-tools/searchprofiler/images/gs6.png["Profile details for the first shard"]
+

View file

@ -1,3 +1,4 @@
[role="xpack"]
[[xpack-profiler]]
= Profiling your Queries and Aggregations
@ -7,12 +8,12 @@ Elasticsearch has a powerful profiler API which can be used to inspect and analy
your search queries. The response, however, is a very large JSON blob which is
difficult to analyze by hand.
{xpack} includes the Search Profiler tool which can transform this JSON output
The {searchprofiler} tool can transform this JSON output
into a visualization that is easy to navigate, allowing you to diagnose and debug
poorly performing queries much faster.
image::dev-tools/searchprofile/images/overview.png["Search Profiler Visualization"]
image::dev-tools/searchprofile/images/overview.png["{searchprofiler} Visualization"]
--

View file

@ -6,12 +6,12 @@ Elasticsearch has a powerful profiler API which can be used to inspect and analy
your search queries. The response, however, is a very large JSON blob which is
difficult to analyze by hand.
{xpack} includes the Search Profiler tool which can transform this JSON output
The {searchprofiler} tool can transform this JSON output
into a visualization that is easy to navigate, allowing you to diagnose and debug
poorly performing queries much faster.
image::dev-tools/searchprofiler/images/overview.png["Search Profiler Visualization"]
image::dev-tools/searchprofiler/images/overview.png["{searchprofiler} Visualization"]
include::getting-started.asciidoc[]

View file

@ -2,7 +2,7 @@
[[profiler-index]]
=== Index and Type filtering
By default, all queries executed by the Search Profiler are sent
By default, all queries executed by the {searchprofiler} are sent
to `GET /_search`. It searches across your entire cluster (all indices, all types).
If you need to query a specific index or type (or several), you can use the Index

View file

@ -2,7 +2,7 @@
[[profiler-complicated]]
=== Profiling a more complicated query
To understand how the query trees are displayed inside the Search Profiler,
To understand how the query trees are displayed inside the {searchprofiler},
let's look at a more complicated query.
. Index the following data:
@ -118,6 +118,6 @@ image::dev-tools/searchprofiler/images/gs10.png["Drilling into the first shard's
Click a shard's Expand button to view the aggregation details. Hover over an
aggregation row to view the timing breakdown.
For more information about how the Search Profiler works, how timings are calculated, and
For more information about how the {searchprofiler} works, how timings are calculated, and
how to interpret various results, see
{ref}/search-profile-queries.html[Profiling queries].

View file

@ -2,7 +2,7 @@
[[profiler-render]]
=== Rendering pre-captured profiler JSON
The Search Profiler queries the cluster that the Kibana node is attached to.
The {searchprofiler} queries the cluster that the Kibana node is attached to.
It does this by executing the query against the cluster and collecting the results.
This is convenient, but sometimes performance problems are temporal in nature. For example,
@ -10,7 +10,7 @@ a query might only be slow at certain time of day when many customers are using
You can setup a process to automatically profile slow queries when they occur and then
save those profile responses for later analysis.
The Search Profiler supports this workflow by enabling you to paste the
The {searchprofiler} supports this workflow by enabling you to paste the
pre-captured JSON. The tool will detect that this is a profiler response JSON
rather than a query, and render the visualization rather than querying the cluster.

View file

@ -17,9 +17,14 @@ image::infrastructure/images/infra-sysmon.jpg[Infrastructure Overview in Kibana]
[float]
== Add data sources
Kibana provides step-by-step instructions to help you add your data sources.
The {infra-guide}[Infrastructure Monitoring Guide] is good source for more detailed
The {infra-guide}[Infrastructure Monitoring Guide] is a good source for more detailed
instructions and information.
[float]
== Configure data sources
By default the Infrastructure UI uses the `metricbeat-*` index pattern to query the data. If you configured Metricbeat to export data to a different set of indices, you will need to set `xpack.infra.sources.default.metricAlias` in `config/kibana.yml` to match your index pattern. You can also configure the timestamp field by overriding `xpack.infra.sources.default.fields.timestamp`. See <<infrastructure-ui-settings-kb>> for a complete list.
--
include::monitor.asciidoc[]

View file

@ -17,9 +17,13 @@ image::logs/images/logs-console.png[Log Console in Kibana]
== Add data sources
Kibana provides step-by-step instructions to help you add your data sources.
The {infra-guide}[Infrastructure Monitoring Guide] is good source for more detailed information and
The {infra-guide}[Infrastructure Monitoring Guide] is a good source for more detailed information and
instructions.
[float]
== Configure data sources
By default the Logs UI uses the `filebeat-*` index pattern to query the data. If your logs are located in a different set of indices, you will need to set `xpack.infra.sources.default.logAlias` in `config/kibana.yml` to match your log's index pattern. You can also configure the timestamp field by overriding `xpack.infra.sources.default.fields.timestamp`, by default it is set to `@timestamp`. See <<logs-ui-settings-kb>> for a complete list.
--

View file

@ -2,7 +2,7 @@
[[logs-ui]]
== Using the Logs UI
Customize the Infrastructure UI to focus on the data you want to see and control how you see it.
Customize the Logs UI to focus on the data you want to see and control how you see it.
[role="screenshot"]
image::logs/images/logs-console.png[Log Console in Kibana]

View file

@ -9,8 +9,6 @@ patterns, advanced settings that tweak the behaviors of Kibana itself, and
the various "objects" that you can save throughout Kibana such as searches,
visualizations, and dashboards.
This section is pluginable, so in addition to the out of the box capabilities,
packs such as {xpack} can add additional management capabilities to Kibana.
--
include::management/managing-licenses.asciidoc[]

View file

@ -1,9 +1,10 @@
[role="xpack"]
[[xpack-reporting]]
= Reporting from Kibana
[partintro]
--
{xpack} enables you to generate reports that contain {kib} dashboards,
You can generate reports that contain {kib} dashboards,
visualizations, and saved searches. The reports are exported as
print-optimized PDF documents.
@ -15,12 +16,12 @@ The following Reporting button appears in the {kib} toolbar:
image:images/reporting.jpg["Reporting",link="reporting.jpg"]
You can also {xpack-ref}/automating-report-generation.html[generate reports automatically].
You can also {stack-ov}/automating-report-generation.html[generate reports automatically].
IMPORTANT: Reports are stored in the `.reporting-*` indices. Any user with
access to these indices has access to every report generated by all users.
To use {reporting} in a production environment, {xpack-ref}/securing-reporting.html[secure
To use {reporting} in a production environment, {stack-ov}/securing-reporting.html[secure
the Reporting endpoints].
--

View file

@ -4,7 +4,7 @@
[partintro]
--
{xpack} enables you to generate reports that contain {kib} dashboards,
You can generate reports that contain {kib} dashboards,
visualizations, and saved searches. Dashboards and visualizations are
exported as PDF documents, while saved searches in Discover
are exported to CSV.

View file

@ -2,11 +2,12 @@
[[xpack-security]]
== Security
{xpack} security enables you to easily secure a cluster. With security, you can
The {stack} {security-features} enable you to easily secure a cluster. With
security, you can
password-protect your data as well as implement more advanced security measures
such as encrypting communications, role-based access control, IP filtering, and
auditing. For more information, see
{xpack-ref}/elasticsearch-security.html[Securing {es} and {kib}] and
{stack-ov}/elasticsearch-security.html[Securing {es} and {kib}] and
<<using-kibana-with-security,Configuring Security in {kib}>>.
[float]
@ -15,7 +16,7 @@ auditing. For more information, see
You can create and manage users on the *Management* / *Security* / *Users* page.
You can also change their passwords and roles. For more information about
authentication and built-in users, see
{xpack-ref}/setting-up-authentication.html[Setting Up User Authentication].
{stack-ov}/setting-up-authentication.html[Setting up user authentication].
[float]
=== Roles
@ -25,7 +26,7 @@ You can manage roles on the *Management* / *Security* / *Roles* page, or use
{kib} see <<xpack-security-authorization, {kib} Authorization>>.
For a more holistic overview of configuring roles for the entire stack,
see {xpack-ref}/authorization.html[Configuring Role-based Access Control].
see {stack-ov}/authorization.html[Configuring role-based access control].
[NOTE]
============================================================================

View file

@ -17,7 +17,7 @@ Set to `true` (default) to enable the <<xpack-grokdebugger,Grok Debugger>>.
[float]
[[profiler-settings]]
==== Search Profiler Settings
==== {searchprofiler} Settings
`xpack.searchprofiler.enabled`::
Set to `true` (default) to enable the <<xpack-profiler,Search Profiler>>.
Set to `true` (default) to enable the <<xpack-profiler,{searchprofiler}>>.

View file

@ -0,0 +1,17 @@
`xpack.infra.enabled`:: Set to `false` to disable the Logs and Infrastructure UI plugin {kib}. Defaults to `true`.
`xpack.infra.sources.default.logAlias`:: Index pattern for matching indices that contain log data. Defaults to `filebeat-*`.
`xpack.infra.sources.default.metricAlias`:: Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`.
`xpack.infra.sources.default.fields.timestamp`:: Timestamp used to sort log entries. Defaults to `@timestamp`.
`xpack.infra.sources.default.fields.message`:: Fields used to display messages in the Logs UI. Defaults to `['message', '@message']`.
`xpack.infra.sources.default.fields.tiebreaker`:: Field used to break ties between two entries with the same timestamp. Defaults to `_doc`.
`xpack.infra.sources.default.fields.host`:: Field used to identify hosts. Defaults to `beat.hostname`.
`xpack.infra.sources.default.fields.container`:: Field used to identify Docker containers. Defaults to `docker.container.name`.
`xpack.infra.sources.default.fields.pod`:: Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.name`.

View file

@ -0,0 +1,14 @@
[role="xpack"]
[[infrastructure-ui-settings-kb]]
=== Infrastructure UI Settings in Kibana
++++
<titleabbrev>Infrastructure UI Settings</titleabbrev>
++++
You do not need to configure any settings to use the Infrastructure UI. It is enabled by default.
[float]
[[general-infra-ui-settings-kb]]
==== General Infrastructure UI Settings
include::general-infra-logs-ui-settings.asciidoc[]

View file

@ -0,0 +1,14 @@
[role="xpack"]
[[logs-ui-settings-kb]]
=== Logs UI Settings in Kibana
++++
<titleabbrev>Logs UI Settings</titleabbrev>
++++
You do not need to configure any settings to use the Logs UI. It is enabled by default.
[float]
[[general-logs-ui-settings-kb]]
==== General Logs UI Settings
include::general-infra-logs-ui-settings.asciidoc[]

View file

@ -12,6 +12,8 @@ For more {kib} configuration settings, see <<settings>>.
include::apm-settings.asciidoc[]
include::dev-settings.asciidoc[]
include::graph-settings.asciidoc[]
include::infrastructure-ui-settings.asciidoc[]
include::logs-ui-settings.asciidoc[]
include::ml-settings.asciidoc[]
include::reporting-settings.asciidoc[]
include::spaces-settings.asciidoc[]

View file

@ -1,9 +1,8 @@
[float]
=== {component} TLS/SSL Settings
You can configure the following TLS/SSL settings. If the settings are not configured,
the {xpack} defaults will be used. See
{xpack-ref}/security-settings.html[Default TLS/SSL Settings].
//<<ssl-tls-settings, {xpack} defaults>> will be used.
You can configure the following TLS/SSL settings. If the settings are not
configured, the default values are used. See
{stack-ov}/security-settings.html[Default TLS/SSL Settings].
ifdef::server[]
+{ssl-prefix}.ssl.enabled+::
@ -45,9 +44,9 @@ Java Cryptography Architecture documentation]. Defaults to the value of
The following settings are used to specify a private key, certificate, and the
trusted certificates that should be used when communicating over an SSL/TLS connection.
If none of the settings below are specified, this will default to the {xpack} defaults.
See {xpack-ref}/security-settings.html[Default TLS/SSL Settings].
//<<ssl-tls-settings, {xpack} defaults>>.
If none of the settings below are specified, the default values are used.
See {stack-ov}/security-settings.html[Default TLS/SSL settings].
ifdef::server[]
A private key and certificate must be configured.
endif::server[]
@ -55,9 +54,8 @@ ifndef::server[]
A private key and certificate are optional and would be used if the server requires client authentication for PKI
authentication.
endif::server[]
If none of the settings below are specified, the {xpack} defaults will be used.
See {xpack-ref}/security-settings.html[Default TLS/SSL Settings].
//<<ssl-tls-settings, {xpack} defaults>> will be used.
If none of the settings below are specified, the defaults values are used.
See {stack-ov}/security-settings.html[Default TLS/SSL settings].
[float]
===== PEM Encoded Files

View file

@ -1,9 +1,9 @@
[[production]]
== Using Kibana in a production environment
* <<configuring-kibana-shield, Using Kibana with {xpack}>>
* <<enabling-ssl, Enabling SSL>>
* <<load-balancing, Load balancing across multiple {es} nodes>>
* <<configuring-kibana-shield>>
* <<enabling-ssl>>
* <<load-balancing>>
How you deploy Kibana largely depends on your use case. If you are the only user,
you can run Kibana on your local machine and configure it to point to whatever
@ -19,19 +19,19 @@ and an Elasticsearch client node on the same machine. For more information, see
[float]
[[configuring-kibana-shield]]
=== Using Kibana with {security}
=== Using {stack} {security-features}
You can use {stack-ov}/elasticsearch-security.html[{security}] to control what
Elasticsearch data users can access through Kibana.
You can use {stack-ov}/elasticsearch-security.html[{stack} {security-features}]
to control what {es} data users can access through Kibana.
When {security} is enabled, Kibana users have to log in. They need to
When {security-features} are enabled, Kibana users have to log in. They need to
have the `kibana_user` role as well as access to the indices they
will be working with in Kibana.
If a user loads a Kibana dashboard that accesses data in an index that they
are not authorized to view, they get an error that indicates the index does
not exist. {security} does not currently provide a way to control which
users can load which dashboards.
not exist. The {security-features} do not currently provide a way to control
which users can load which dashboards.
For information about setting up Kibana users, see
{kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana].

View file

@ -295,7 +295,7 @@
"@types/opn": "^5.1.0",
"@types/podium": "^1.0.0",
"@types/prop-types": "^15.5.3",
"@types/puppeteer": "^1.6.2",
"@types/puppeteer-core": "^1.9.0",
"@types/react": "16.3.14",
"@types/react-dom": "^16.0.5",
"@types/react-redux": "^6.0.6",
@ -303,6 +303,7 @@
"@types/react-virtualized": "^9.18.7",
"@types/redux": "^3.6.31",
"@types/redux-actions": "^2.2.1",
"@types/rimraf": "^2.0.2",
"@types/semver": "^5.5.0",
"@types/sinon": "^7.0.0",
"@types/strip-ansi": "^3.0.0",

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
require('../../setup_node_env');
require('./report').reportFailedTests();

View file

@ -133,7 +133,7 @@ const updateGithubIssues = (githubClient, issues) => {
/**
* Scans all junit XML files in ./target/junit/ and reports any found test failures to Github Issues.
*/
export async function reportFailedTests(done) {
export async function reportFailedTests() {
const githubClient = getGithubClient();
const issues = await paginate(githubClient, githubClient.issues.getForRepo({
owner: GITHUB_OWNER,
@ -148,5 +148,5 @@ export async function reportFailedTests(done) {
.pipe(mapXml)
.pipe(filterFailures)
.pipe(updateGithubIssues(githubClient, issues))
.on('done', done);
.on('done', () => console.log(`Finished reporting test failures.`));
}

View file

@ -27,6 +27,7 @@ export const LICENSE_WHITELIST = [
'(MIT AND CC-BY-3.0)',
'(MIT AND Zlib)',
'(MIT OR Apache-2.0)',
'(MIT OR GPL-3.0)',
'(WTFPL OR MIT)',
'AFLv2.1',
'Apache 2.0',

File diff suppressed because one or more lines are too long

View file

@ -48,11 +48,15 @@ class VisEditor extends Component {
this.onBrush = brushHandler(props.vis.API.timeFilter);
this.handleUiState = this.handleUiState.bind(this, props.vis);
this.handleAppStateChange = this.handleAppStateChange.bind(this);
this.getConfig = (...args) => props.config.get(...args);
this.getConfig = this.getConfig.bind(this);
this.visDataSubject = new Rx.Subject();
this.visData$ = this.visDataSubject.asObservable().pipe(share());
}
getConfig(...args) {
return this.props.config.get(...args);
}
handleUiState(vis, ...args) {
vis.uiStateVal(...args);
}

View file

@ -34,7 +34,8 @@ function getColors(props) {
if (model.gauge_color_rules) {
model.gauge_color_rules.forEach((rule) => {
if (rule.operator && rule.value != null) {
const value = series[0] && getLastValue(series[0].data) || 0;
const value = (series[0] && getLastValue(series[0].data)) ||
series[1] && getLastValue(series[1].data) || 0;
if (_[rule.operator](value, rule.value)) {
gauge = rule.gauge;
text = rule.text;
@ -97,7 +98,8 @@ GaugeVisualization.propTypes = {
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
visData: PropTypes.object,
getConfig: PropTypes.func
};
export default visWithSplits(GaugeVisualization);

View file

@ -25,12 +25,17 @@ import Markdown from 'react-markdown';
import replaceVars from '../../lib/replace_vars';
import convertSeriesToVars from '../../lib/convert_series_to_vars';
import ErrorComponent from '../../error';
import uuid from 'uuid';
const getMarkdownId = id => `markdown-${id}`;
function MarkdownVisualization(props) {
const { backgroundColor, model, visData, dateFormat } = props;
const series = _.get(visData, `${model.id}.series`, []);
const variables = convertSeriesToVars(series, model, dateFormat, props.getConfig);
const style = {};
const markdownElementId = getMarkdownId(uuid.v1());
let reversed = props.reversed;
const panelBackgroundColor = model.background_color || backgroundColor;
if (panelBackgroundColor) {
@ -38,6 +43,8 @@ function MarkdownVisualization(props) {
reversed = color(panelBackgroundColor).luminosity() < 0.45;
}
let markdown;
let markdownCss = '';
if (model.markdown) {
const markdownSource = replaceVars(
model.markdown,
@ -47,6 +54,12 @@ function MarkdownVisualization(props) {
...variables
}
);
if (model.markdown_css) {
markdownCss = model.markdown_css
.replace(new RegExp(getMarkdownId(model.id), 'g'), markdownElementId);
}
let className = 'tvbMarkdown';
let contentClassName = `tvbMarkdown__content ${model.markdown_vertical_align}`;
if (model.markdown_scrollbars) contentClassName += ' scrolling';
@ -55,9 +68,9 @@ function MarkdownVisualization(props) {
markdown = (
<div className={className} data-test-subj="tsvbMarkdown">
{markdownError && <ErrorComponent error={markdownError} />}
<style type="text/css">{model.markdown_css}</style>
<style type="text/css">{markdownCss}</style>
<div className={contentClassName}>
<div id={`markdown-${model.id}`}>{!markdownError && <Markdown escapeHtml={true} source={markdownSource} />}</div>
<div id={markdownElementId}>{!markdownError && <Markdown escapeHtml={true} source={markdownSource} />}</div>
</div>
</div>
);
@ -77,7 +90,8 @@ MarkdownVisualization.propTypes = {
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object,
dateFormat: PropTypes.string
dateFormat: PropTypes.string,
getConfig: PropTypes.func
};
export default MarkdownVisualization;

View file

@ -92,7 +92,8 @@ MetricVisualization.propTypes = {
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
visData: PropTypes.object,
getConfig: PropTypes.func
};
export default visWithSplits(MetricVisualization);

View file

@ -231,7 +231,8 @@ TableVis.propTypes = {
onUiState: PropTypes.func,
uiState: PropTypes.object,
pageNumber: PropTypes.number,
reversed: PropTypes.bool
reversed: PropTypes.bool,
getConfig: PropTypes.func
};
export default TableVis;

View file

@ -19,6 +19,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { toastNotifications } from 'ui/notify';
import { MarkdownSimple } from 'ui/markdown';
import tickFormatter from '../../lib/tick_formatter';
import _ from 'lodash';
import Timeseries from '../../../visualizations/components/timeseries';
@ -54,12 +57,35 @@ class TimeseriesVisualization extends Component {
if (!scaledDataFormat || !dateFormat) return val;
const formatter = createXaxisFormatter(this.getInterval(), scaledDataFormat, dateFormat);
return formatter(val);
};
componentDidUpdate() {
if (this.showToastNotification && this.notificationReason !== this.showToastNotification.reason) {
if (this.notification) {
toastNotifications.remove(this.notification);
}
this.notificationReason = this.showToastNotification.reason;
this.notification = toastNotifications.addDanger({
title: this.showToastNotification.title,
text: <MarkdownSimple>{this.showToastNotification.reason}</MarkdownSimple>,
});
}
if (!this.showToastNotification && this.notification) {
toastNotifications.remove(this.notification);
this.notificationReason = null;
this.notification = null;
}
}
render() {
const { backgroundColor, model, visData } = this.props;
const series = _.get(visData, `${model.id}.series`, []);
let annotations;
this.showToastNotification = null;
if (model.annotations && Array.isArray(model.annotations)) {
annotations = model.annotations.map(annotation => {
const data = _.get(visData, `${model.id}.annotations.${annotation.id}`, [])
@ -70,7 +96,15 @@ class TimeseriesVisualization extends Component {
icon: annotation.icon,
series: data.map(s => {
return [s[0], s[1].map(doc => {
return replaceVars(annotation.template, null, doc);
const vars = replaceVars(annotation.template, null, doc);
if (vars instanceof Error) {
this.showToastNotification = vars.error.caused_by;
return annotation.template;
}
return vars;
})];
})
};
@ -218,7 +252,8 @@ TimeseriesVisualization.propTypes = {
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object,
dateFormat: PropTypes.string
dateFormat: PropTypes.string,
getConfig: PropTypes.func
};
export default TimeseriesVisualization;

View file

@ -107,7 +107,8 @@ TopNVisualization.propTypes = {
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
visData: PropTypes.object,
getConfig: PropTypes.func
};
export default TopNVisualization;

View file

@ -72,6 +72,7 @@ export function visWithSplits(WrappedComponent) {
onBrush={props.onBrush}
additionalLabel={label}
backgroundColor={props.backgroundColor}
getConfig={props.getConfig}
/>
</div>
);

View file

@ -25,6 +25,7 @@ import $ from 'ui/flot-charts';
import eventBus from '../lib/events';
import Resize from './resize';
import calculateBarWidth from '../lib/calculate_bar_width';
import calculateFillColor from '../lib/calculate_fill_color';
import colors from '../lib/colors';
class FlotChart extends Component {
@ -99,7 +100,7 @@ class FlotChart extends Component {
this.plot.setData(this.calculateData(series, newProps.show));
this.plot.setupGrid();
this.plot.draw();
if (!_.isEqual(this.props.series, series)) this.handleDraw(this.plot);
if (!_.isEqual(this.props.series, newProps.series)) this.handleDraw(this.plot);
} else {
this.renderChart();
}
@ -119,7 +120,11 @@ class FlotChart extends Component {
.filter(this.filterByShow(show))
.map(set => {
if (_.isPlainObject(set)) {
return set;
return {
...set,
lines: this.computeColor(set.lines, set.color),
bars: this.computeColor(set.bars, set.color),
};
}
return {
color: '#990000',
@ -130,6 +135,18 @@ class FlotChart extends Component {
.value();
}
computeColor(style, color) {
if (style && style.show) {
const { fill, fillColor } = calculateFillColor(color, style.fill);
return {
...style,
fill,
fillColor,
};
}
return style;
}
handleDraw(plot) {
if (this.props.onDraw) this.props.onDraw(plot);
}
@ -233,10 +250,8 @@ class FlotChart extends Component {
if (resize.clientWidth > 0 && resize.clientHeight > 0) {
this.rendered = true;
const { series } = this.props;
const data = this.calculateData(series, this.props.show);
this.plot = $.plot(this.target, [], this.getOptions(this.props));
this.plot = $.plot(this.target, data, this.getOptions(this.props));
this.handleDraw(this.plot);
_.defer(() => this.handleResize());

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { expect } from 'chai';
import calculateFillColor from '../calculate_fill_color';
describe('calculateFillColor(color, fill)', () => {
it('should return "fill" and "fillColor" properties', () => {
const color = 'rgb(255,0,0)';
const fill = 1;
const data = calculateFillColor(color, fill);
expect(data.fill).to.be.true;
expect(data.fillColor).to.be.a('string');
});
it('should set "fill" property to false in case of 0 opacity', () => {
const color = 'rgb(255, 0, 0)';
const fill = 0;
const data = calculateFillColor(color, fill);
expect(data.fill).to.be.false;
});
it('should return the opacity less than 1', () => {
const color = 'rgba(255, 0, 0, 0.9)';
const fill = 10;
const data = calculateFillColor(color, fill);
expect(data.fillColor).to.equal('rgba(255, 0, 0, 0.9)');
});
it('should sum fill and color opacity', () => {
const color = 'rgba(255, 0, 0, 0.5)';
const fill = 0.5;
const data = calculateFillColor(color, fill);
expect(data.fillColor).to.equal('rgba(255, 0, 0, 0.25)');
});
});

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Color from 'color';
export default (color, fill = 1) => {
const initialColor = new Color(color).rgb();
const opacity = Math.min(Number(fill), 1) * initialColor.valpha;
const [r, g, b] = initialColor.color;
return {
fill: opacity > 0,
fillColor: new Color([
r, g, b, Number(opacity.toFixed(2)),
]).string(),
};
};

View file

@ -35,7 +35,7 @@ describe('getSplits(resp, panel, series)', () => {
const panel = { type: 'timeseries' };
const series = {
id: 'SERIES',
color: '#F00',
color: 'rgb(255, 0, 0)',
split_mode: 'everything',
metrics: [
{ id: 'AVG', type: 'avg', field: 'cpu' },
@ -47,7 +47,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES',
label: 'Overall Average of Average of cpu',
meta: { bucketSize: 10 },
color: '#FF0000',
color: 'rgb(255, 0, 0)',
timeseries: { buckets: [] },
SIBAGG: { value: 1 }
}
@ -92,7 +92,7 @@ describe('getSplits(resp, panel, series)', () => {
key: 'example-01',
label: 'example-01',
meta: { bucketSize: 10 },
color: '#FF0000',
color: 'rgb(255, 0, 0)',
timeseries: { buckets: [] },
SIBAGG: { value: 1 }
},
@ -101,7 +101,7 @@ describe('getSplits(resp, panel, series)', () => {
key: 'example-02',
label: 'example-02',
meta: { bucketSize: 10 },
color: '#FF0000',
color: 'rgb(255, 0, 0)',
timeseries: { buckets: [] },
SIBAGG: { value: 2 }
}
@ -146,7 +146,7 @@ describe('getSplits(resp, panel, series)', () => {
key: 'example-01',
label: 'example-01',
meta: { bucketSize: 10 },
color: '#FF0000',
color: 'rgb(255, 0, 0)',
timeseries: { buckets: [] },
SIBAGG: { value: 1 }
},
@ -155,7 +155,7 @@ describe('getSplits(resp, panel, series)', () => {
key: 'example-02',
label: 'example-02',
meta: { bucketSize: 10 },
color: '#930000',
color: 'rgb(147, 0, 0)',
timeseries: { buckets: [] },
SIBAGG: { value: 2 }
}
@ -180,7 +180,7 @@ describe('getSplits(resp, panel, series)', () => {
};
const series = {
id: 'SERIES',
color: '#F00',
color: 'rgb(255, 0, 0)',
split_mode: 'filters',
split_filters: [
{ id: 'filter-1', color: '#F00', filter: 'status_code:[* TO 200]', label: '200s' },

View file

@ -29,13 +29,13 @@ export default function getSplitColors(inputColor, size = 10, style = 'gradient'
'#194D33', '#0062B1', '#808900', '#0C797D', '#9F0500', '#C45100', '#FB9E00', '#653294', '#AB149E', '#0F1419', '#666666'
];
} else {
colors.push(color.hex());
colors.push(color.string());
const rotateBy = (color.luminosity() / (size - 1));
for(let i = 0; i < (size - 1); i++) {
const hsl = workingColor.hsl().object();
hsl.l -= (rotateBy * 100);
workingColor = Color.hsl(hsl);
colors.push(workingColor.hex());
colors.push(workingColor.rgb().toString());
}
}

View file

@ -35,7 +35,7 @@ export default function getSplits(resp, panel, series) {
return buckets.map(bucket => {
bucket.id = `${series.id}:${bucket.key}`;
bucket.label = formatKey(bucket.key, series);
bucket.color = panel.type === 'top_n' ? color.hex() : colors.shift();
bucket.color = panel.type === 'top_n' ? color.string() : colors.shift();
bucket.meta = meta;
return bucket;
});
@ -67,7 +67,7 @@ export default function getSplits(resp, panel, series) {
{
id: series.id,
label: series.label || calculateLabel(metric, series.metrics),
color: color.hex(),
color: color.string(),
...mergeObj,
meta
}

View file

@ -35,7 +35,7 @@ describe('percentile(resp, panel, series)', () => {
line_width: 1,
point_size: 1,
fill: 0,
color: '#F00',
color: 'rgb(255, 0, 0)',
id: 'test',
split_mode: 'everything',
metrics: [{
@ -92,7 +92,7 @@ describe('percentile(resp, panel, series)', () => {
expect(results).to.have.length(3);
expect(results[0]).to.have.property('id', '10-90:test');
expect(results[0]).to.have.property('color', '#FF0000');
expect(results[0]).to.have.property('color', 'rgb(255, 0, 0)');
expect(results[0]).to.have.property('fillBetween', '10-90:test:90');
expect(results[0]).to.have.property('label', 'Percentile of cpu (10)');
expect(results[0]).to.have.property('legend', false);
@ -110,7 +110,7 @@ describe('percentile(resp, panel, series)', () => {
]);
expect(results[1]).to.have.property('id', '10-90:test:90');
expect(results[1]).to.have.property('color', '#FF0000');
expect(results[1]).to.have.property('color', 'rgb(255, 0, 0)');
expect(results[1]).to.have.property('label', 'Percentile of cpu (10)');
expect(results[1]).to.have.property('legend', false);
expect(results[1]).to.have.property('lines');
@ -127,7 +127,7 @@ describe('percentile(resp, panel, series)', () => {
]);
expect(results[2]).to.have.property('id', '50:test');
expect(results[2]).to.have.property('color', '#FF0000');
expect(results[2]).to.have.property('color', 'rgb(255, 0, 0)');
expect(results[2]).to.have.property('label', 'Percentile of cpu (50)');
expect(results[2]).to.have.property('stack', false);
expect(results[2]).to.have.property('lines');

View file

@ -35,7 +35,7 @@ describe('stdDeviationBands(resp, panel, series)', () => {
line_width: 1,
point_size: 1,
fill: 0,
color: '#F00',
color: 'rgb(255, 0, 0)',
id: 'test',
split_mode: 'everything',
metrics: [{
@ -91,7 +91,7 @@ describe('stdDeviationBands(resp, panel, series)', () => {
expect(results[0]).to.eql({
id: 'test:upper',
label: 'Std. Deviation of cpu',
color: '#FF0000',
color: 'rgb(255, 0, 0)',
lines: { show: true, fill: 0.5, lineWidth: 0 },
points: { show: false },
fillBetween: 'test:lower',
@ -103,7 +103,7 @@ describe('stdDeviationBands(resp, panel, series)', () => {
expect(results[1]).to.eql({
id: 'test:lower',
color: '#FF0000',
color: 'rgb(255, 0, 0)',
lines: { show: true, fill: false, lineWidth: 0 },
points: { show: false },
data: [

View file

@ -35,7 +35,7 @@ describe('stdDeviationSibling(resp, panel, series)', () => {
line_width: 1,
point_size: 1,
fill: 0,
color: '#F00',
color: 'rgb(255, 0, 0)',
id: 'test',
split_mode: 'everything',
metrics: [
@ -92,7 +92,7 @@ describe('stdDeviationSibling(resp, panel, series)', () => {
expect(results[0]).to.eql({
id: 'test:lower',
color: '#FF0000',
color: 'rgb(255, 0, 0)',
lines: { show: true, fill: false, lineWidth: 0 },
points: { show: false },
data: [
@ -104,7 +104,7 @@ describe('stdDeviationSibling(resp, panel, series)', () => {
expect(results[1]).to.eql({
id: 'test:upper',
label: 'Overall Std. Deviation of Average of cpu',
color: '#FF0000',
color: 'rgb(255, 0, 0)',
fillBetween: 'test:lower',
lines: { show: true, fill: 0.5, lineWidth: 0 },
points: { show: false },

View file

@ -35,7 +35,7 @@ describe('stdMetric(resp, panel, series)', () => {
line_width: 1,
point_size: 1,
fill: 0,
color: '#F00',
color: 'rgb(255, 0, 0)',
id: 'test',
split_mode: 'everything',
metrics: [{ id: 'avgmetric', type: 'avg', field: 'cpu' }]
@ -87,7 +87,7 @@ describe('stdMetric(resp, panel, series)', () => {
const next = results => results;
const results = stdMetric(resp, panel, series)(next)([]);
expect(results).to.have.length(1);
expect(results[0]).to.have.property('color', '#FF0000');
expect(results[0]).to.have.property('color', 'rgb(255, 0, 0)');
expect(results[0]).to.have.property('id', 'test');
expect(results[0]).to.have.property('label', 'Average of cpu');
expect(results[0]).to.have.property('lines');

View file

@ -35,7 +35,7 @@ describe('stdSibling(resp, panel, series)', () => {
line_width: 1,
point_size: 1,
fill: 0,
color: '#F00',
color: 'rgb(255, 0, 0)',
id: 'test',
split_mode: 'everything',
metrics: [
@ -96,7 +96,7 @@ describe('stdSibling(resp, panel, series)', () => {
expect(results[0]).to.eql({
id: 'test',
label: 'Overall Std. Deviation of Average of cpu',
color: '#FF0000',
color: 'rgb(255, 0, 0)',
stack: false,
lines: { show: true, fill: 0, lineWidth: 1, steps: false },
points: { show: true, radius: 1, lineWidth: 1 },

View file

@ -72,7 +72,7 @@ describe('timeShift(resp, panel, series)', () => {
const next = timeShift(resp, panel, series)(results => results);
const results = stdMetric(resp, panel, series)(next)([]);
expect(results).to.have.length(1);
expect(results[0]).to.have.property('color', '#FF0000');
expect(results[0]).to.have.property('color', 'rgb(255, 0, 0)');
expect(results[0]).to.have.property('id', 'test');
expect(results[0]).to.have.property('label', 'Average of cpu');
expect(results[0]).to.have.property('lines');

View file

@ -26,7 +26,7 @@ import { fromRoot } from '../../utils';
export async function i18nMixin(kbnServer, server, config) {
const locale = config.get('i18n.locale');
const translationsDirs = [fromRoot('src/ui/translations')];
const translationsDirs = [fromRoot('src/ui/translations'), fromRoot('src/server/translations')];
const groupedEntries = await Promise.all([
...config.get('plugins.scanDirs').map(async path => {

View file

@ -439,7 +439,9 @@ export class KibanaMap extends EventEmitter {
}
this._baseLayerIsDesaturated = isDesaturated;
this._updateDesaturation();
this._leafletBaseLayer.redraw();
if (this._leafletBaseLayer) {
this._leafletBaseLayer.redraw();
}
}
addDrawControl() {

View file

@ -380,7 +380,9 @@ export class EmbeddedVisualizeHandler {
this.vis.filters = { timeRange: this.dataLoaderParams.timeRange };
return this.dataLoader.fetch(this.dataLoaderParams).then(data => {
this.dataSubject.next(data);
if (data.value) {
this.dataSubject.next(data.value);
}
return data;
});
};

View file

@ -17,8 +17,6 @@
* under the License.
*/
import { reportFailedTests } from '../src/dev/failed_tests/report';
module.exports = function (grunt) {
grunt.registerTask('jenkins:docs', [
'docker:docs'
@ -41,12 +39,4 @@ module.exports = function (grunt) {
'test:browser-ci',
'run:apiIntegrationTests',
]);
grunt.registerTask(
'jenkins:report',
'Reports failed tests found in junit xml files to Github issues',
function () {
reportFailedTests(this.async());
}
);
};

View file

@ -2,6 +2,17 @@
set -e
function report {
if [[ -z "$PR_SOURCE_BRANCH" ]]; then
node src/dev/failed_tests/cli
else
echo "Failure issues not created on pull requests"
fi
}
trap report EXIT
source src/dev/ci_setup/checkout_sibling_es.sh
"$(FORCE_COLOR=0 yarn bin)/grunt" functionalTests:ensureAllTestsInCiGroup;

View file

@ -1,5 +0,0 @@
#!/usr/bin/env bash
set -e
xvfb-run "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:report;

View file

@ -2,8 +2,19 @@
set -e
function report {
if [[ -z "$PR_SOURCE_BRANCH" ]]; then
node src/dev/failed_tests/cli
else
echo "Failure issues not created on pull requests"
fi
}
trap report EXIT
source src/dev/ci_setup/checkout_sibling_es.sh
export TEST_BROWSER_HEADLESS=1
export TEST_ES_FROM=${TEST_ES_FROM:-source}
"$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --from=source --dev;

View file

@ -2,6 +2,17 @@
set -e
function report {
if [[ -z "$PR_SOURCE_BRANCH" ]]; then
cd "$KIBANA_DIR"
node src/dev/failed_tests/cli
else
echo "Failure issues not created on pull requests"
fi
}
trap report EXIT
source src/dev/ci_setup/checkout_sibling_es.sh
export TEST_BROWSER_HEADLESS=1

View file

@ -2,6 +2,17 @@
set -e
function report {
if [[ -z "$PR_SOURCE_BRANCH" ]]; then
cd "$KIBANA_DIR"
node src/dev/failed_tests/cli
else
echo "Failure issues not created on pull requests"
fi
}
trap report EXIT
source src/dev/ci_setup/checkout_sibling_es.sh
export TEST_BROWSER_HEADLESS=1

View file

@ -234,7 +234,6 @@
"monaco-editor": "^0.14.3",
"ngreact": "^0.5.1",
"nodegit": "git+https://github.com/elastic/nodegit.git#v0.24.0-alpha.6",
"nodemailer": "^4.6.4",
"node-fetch": "^2.1.2",
"nodemailer": "^4.6.4",
"object-path-immutable": "^0.5.3",

View file

@ -101,7 +101,12 @@ exports[`DetailView should render StickyProperties 1`] = `
exports[`DetailView should render TabContent 1`] = `
<TabContent
currentTab="exception_stacktrace"
currentTab={
Object {
"key": "exception_stacktrace",
"label": "Exception stacktrace",
}
}
error={
Object {
"@timestamp": "myTimestamp",

View file

@ -11,7 +11,8 @@ import {
EuiTabs,
EuiTitle
} from '@elastic/eui';
import { capitalize, get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { first, get } from 'lodash';
import React from 'react';
import { RRRRenderResponse } from 'react-redux-request';
import styled from 'styled-components';
@ -45,7 +46,8 @@ import { KibanaLink, legacyEncodeURIComponent } from '../../../../utils/url';
import { DiscoverErrorButton } from '../../../shared/DiscoverButtons/DiscoverErrorButton';
import {
getPropertyTabNames,
PropertiesTable
PropertiesTable,
Tab
} from '../../../shared/PropertiesTable';
import { Stacktrace } from '../../../shared/Stacktrace';
import { StickyProperties } from '../../../shared/StickyProperties';
@ -69,8 +71,21 @@ const PaddedContainer = styled.div`
padding: ${px(units.plus)} ${px(units.plus)} 0;
`;
const EXC_STACKTRACE_TAB = 'exception_stacktrace';
const LOG_STACKTRACE_TAB = 'log_stacktrace';
const logStacktraceTab = {
key: 'log_stacktrace',
label: i18n.translate('xpack.apm.propertiesTable.tabs.logStacktraceLabel', {
defaultMessage: 'Log stacktrace'
})
};
const exceptionStacktraceTab = {
key: 'exception_stacktrace',
label: i18n.translate(
'xpack.apm.propertiesTable.tabs.exceptionStacktraceLabel',
{
defaultMessage: 'Exception stacktrace'
}
)
};
interface Props {
errorGroup: RRRRenderResponse<ErrorGroupAPIResponse>;
@ -152,7 +167,7 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
<EuiSpacer />
<EuiTabs>
{tabs.map(key => {
{tabs.map(({ key, label }) => {
return (
<EuiTab
onClick={() => {
@ -164,10 +179,10 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
})
});
}}
isSelected={currentTab === key}
isSelected={currentTab.key === key}
key={key}
>
{capitalize(key.replace('_', ' '))}
{label}
</EuiTab>
);
})}
@ -212,29 +227,28 @@ export function TabContent({
currentTab
}: {
error: APMError;
currentTab?: string;
currentTab: Tab;
}) {
const codeLanguage = error.context.service.name;
const agentName = error.context.service.agent.name;
const excStackframes: MaybeStackframes = get(error, ERROR_EXC_STACKTRACE);
const logStackframes: MaybeStackframes = get(error, ERROR_LOG_STACKTRACE);
switch (currentTab) {
case LOG_STACKTRACE_TAB:
case undefined:
switch (currentTab.key) {
case logStacktraceTab.key:
return (
<Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} />
);
case EXC_STACKTRACE_TAB:
case exceptionStacktraceTab.key:
return (
<Stacktrace stackframes={excStackframes} codeLanguage={codeLanguage} />
);
default:
const propData = error.context[currentTab] as any;
const propData = error.context[currentTab.key] as any;
return (
<PropertiesTable
propData={propData}
propKey={currentTab}
propKey={currentTab.key}
agentName={agentName}
/>
);
@ -242,16 +256,18 @@ export function TabContent({
}
// Ensure the selected tab exists or use the first
export function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
return tabs.includes(selectedTab!) ? selectedTab : tabs[0];
export function getCurrentTab(tabs: Tab[] = [], selectedTabKey?: string) {
const selectedTab = tabs.find(({ key }) => key === selectedTabKey);
return selectedTab ? selectedTab : first(tabs) || {};
}
export function getTabs(error: APMError) {
const hasLogStacktrace = get(error, ERROR_LOG_STACKTRACE, []).length > 0;
const contextKeys = Object.keys(error.context);
return [
...(hasLogStacktrace ? [LOG_STACKTRACE_TAB] : []),
EXC_STACKTRACE_TAB,
...(hasLogStacktrace ? [logStacktraceTab] : []),
exceptionStacktraceTab,
...getPropertyTabNames(contextKeys)
];
}

View file

@ -5,7 +5,8 @@
*/
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import { capitalize, first, get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { first, get } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { Transaction } from '../../../../../typings/es_schemas/Transaction';
@ -14,7 +15,8 @@ import { px, units } from '../../../../style/variables';
import { fromQuery, history, toQuery } from '../../../../utils/url';
import {
getPropertyTabNames,
PropertiesTable
PropertiesTable,
Tab
} from '../../../shared/PropertiesTable';
import { WaterfallContainer } from './WaterfallContainer';
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
@ -24,15 +26,22 @@ const TableContainer = styled.div`
`;
// Ensure the selected tab exists or use the first
function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
return selectedTab && tabs.includes(selectedTab) ? selectedTab : first(tabs);
function getCurrentTab(tabs: Tab[] = [], selectedTabKey?: string) {
const selectedTab = tabs.find(({ key }) => key === selectedTabKey);
return selectedTab ? selectedTab : first(tabs) || {};
}
const TIMELINE_TAB = 'timeline';
const timelineTab = {
key: 'timeline',
label: i18n.translate('xpack.apm.propertiesTable.tabs.timelineLabel', {
defaultMessage: 'Timeline'
})
};
function getTabs(transactionData: Transaction) {
const dynamicProps = Object.keys(transactionData.context || {});
return [TIMELINE_TAB, ...getPropertyTabNames(dynamicProps)];
return [timelineTab, ...getPropertyTabNames(dynamicProps)];
}
interface TransactionPropertiesTableProps {
@ -55,7 +64,7 @@ export function TransactionPropertiesTable({
return (
<div>
<EuiTabs>
{tabs.map(key => {
{tabs.map(({ key, label }) => {
return (
<EuiTab
onClick={() => {
@ -67,10 +76,10 @@ export function TransactionPropertiesTable({
})
});
}}
isSelected={currentTab === key}
isSelected={currentTab.key === key}
key={key}
>
{capitalize(key)}
{label}
</EuiTab>
);
})}
@ -78,7 +87,7 @@ export function TransactionPropertiesTable({
<EuiSpacer />
{currentTab === TIMELINE_TAB && (
{currentTab.key === timelineTab.key && (
<WaterfallContainer
transaction={transaction}
location={location}
@ -87,11 +96,11 @@ export function TransactionPropertiesTable({
/>
)}
{currentTab !== TIMELINE_TAB && (
{currentTab.key !== timelineTab.key && (
<TableContainer>
<PropertiesTable
propData={get(transaction.context, currentTab)}
propKey={currentTab}
propData={get(transaction.context, currentTab.key)}
propKey={currentTab.key}
agentName={agentName}
/>
</TableContainer>

View file

@ -5,19 +5,21 @@
*/
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import { capitalize, first, get } from 'lodash';
import { first, get } from 'lodash';
import React from 'react';
import { Transaction } from '../../../../../typings/es_schemas/Transaction';
import { IUrlParams } from '../../../../store/urlParams';
import { fromQuery, history, toQuery } from '../../../../utils/url';
import {
getPropertyTabNames,
PropertiesTable
PropertiesTable,
Tab
} from '../../../shared/PropertiesTable';
// Ensure the selected tab exists or use the first
function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
return selectedTab && tabs.includes(selectedTab) ? selectedTab : first(tabs);
function getCurrentTab(tabs: Tab[] = [], selectedTabKey?: string) {
const selectedTab = tabs.find(({ key }) => key === selectedTabKey);
return selectedTab ? selectedTab : first(tabs) || {};
}
function getTabs(transactionData: Transaction) {
@ -43,7 +45,7 @@ export const TransactionPropertiesTableForFlyout: React.SFC<Props> = ({
return (
<div>
<EuiTabs>
{tabs.map(key => {
{tabs.map(({ key, label }) => {
return (
<EuiTab
onClick={() => {
@ -55,18 +57,18 @@ export const TransactionPropertiesTableForFlyout: React.SFC<Props> = ({
})
});
}}
isSelected={currentTab === key}
isSelected={currentTab.key === key}
key={key}
>
{capitalize(key)}
{label}
</EuiTab>
);
})}
</EuiTabs>
<EuiSpacer />
<PropertiesTable
propData={get(transaction.context, currentTab)}
propKey={currentTab}
propData={get(transaction.context, currentTab.key)}
propKey={currentTab.key}
agentName={agentName}
/>
</div>

View file

@ -21,7 +21,7 @@ import React from 'react';
import styled from 'styled-components';
import { TransactionActionMenu } from 'x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu';
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
import { APM_AGENT_DROPPED_SPANS_DOCS } from 'x-pack/plugins/apm/public/utils/documentation/agents';
import { DROPPED_SPANS_DOCS } from 'x-pack/plugins/apm/public/utils/documentation/apm-get-started';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import { StickyTransactionProperties } from '../../../StickyTransactionProperties';
import { TransactionPropertiesTableForFlyout } from '../../../TransactionPropertiesTableForFlyout';
@ -71,20 +71,14 @@ function DroppedSpansWarning({
return null;
}
const url =
APM_AGENT_DROPPED_SPANS_DOCS[transactionDoc.context.service.agent.name];
const docsLink = url ? (
<EuiLink href={url} target="_blank">
Learn more.
</EuiLink>
) : null;
return (
<React.Fragment>
<EuiCallOut size="s">
The APM agent that reported this transaction dropped {dropped} spans or
more based on its configuration. {docsLink}
more based on its configuration.{' '}
<EuiLink href={DROPPED_SPANS_DOCS} target="_blank">
Learn more about dropped spans.
</EuiLink>
</EuiCallOut>
<EuiHorizontalRule />
</React.Fragment>

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { isBoolean, isNumber, isObject } from 'lodash';
import React from 'react';
import styled from 'styled-components';
@ -75,7 +76,13 @@ export function FormattedValue({ value }: { value: any }): JSX.Element {
} else if (isBoolean(value) || isNumber(value)) {
return <React.Fragment>{String(value)}</React.Fragment>;
} else if (!value) {
return <EmptyValue>N/A</EmptyValue>;
return (
<EmptyValue>
{i18n.translate('xpack.apm.propertiesTable.notAvailableLabel', {
defaultMessage: 'N/A'
})}
</EmptyValue>
);
}
return <React.Fragment>{value}</React.Fragment>;

View file

@ -90,10 +90,17 @@ describe('PropertiesTable', () => {
describe('getPropertyTabNames', () => {
it('should return selected and required keys only', () => {
expect(getPropertyTabNames(['testProperty'])).toEqual([
'testProperty',
'requiredProperty'
]);
const expectedTabsConfig = [
{
key: 'testProperty',
label: 'testPropertyLabel'
},
{
key: 'requiredProperty',
label: 'requiredPropertyLabel'
}
];
expect(getPropertyTabNames(['testProperty'])).toEqual(expectedTabsConfig);
});
});
@ -142,15 +149,18 @@ function mockPropertyConfig() {
propertyConfig.PROPERTY_CONFIG = [
{
key: 'testProperty',
label: 'testPropertyLabel',
required: false,
presortedKeys: ['name', 'age']
},
{
key: 'optionalProperty',
label: 'optionalPropertyLabel',
required: false
},
{
key: 'requiredProperty',
label: 'requiredPropertyLabel',
required: true
}
];

View file

@ -5,6 +5,7 @@
*/
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { get, indexBy, uniq } from 'lodash';
import React from 'react';
import styled from 'styled-components';
@ -43,21 +44,43 @@ const EuiIconWithSpace = styled(EuiIcon)`
margin-right: ${px(units.half)};
`;
export function getPropertyTabNames(selected: string[]): string[] {
export interface Tab {
key: string;
label: string;
}
export function getPropertyTabNames(selected: string[]): Tab[] {
return PROPERTY_CONFIG.filter(
({ key, required }: { key: string; required: boolean }) =>
required || selected.includes(key)
).map(({ key }: { key: string }) => key);
({ key, required }) => required || selected.includes(key)
).map(({ key, label }) => ({ key, label }));
}
function getAgentFeatureText(featureName: string) {
switch (featureName) {
case 'user':
return 'You can configure your agent to add contextual information about your users.';
return i18n.translate(
'xpack.apm.propertiesTable.userTab.agentFeatureText',
{
defaultMessage:
'You can configure your agent to add contextual information about your users.'
}
);
case 'tags':
return 'You can configure your agent to add filterable tags on transactions.';
return i18n.translate(
'xpack.apm.propertiesTable.tagsTab.agentFeatureText',
{
defaultMessage:
'You can configure your agent to add filterable tags on transactions.'
}
);
case 'custom':
return 'You can configure your agent to add custom contextual information on transactions.';
return i18n.translate(
'xpack.apm.propertiesTable.customTab.agentFeatureText',
{
defaultMessage:
'You can configure your agent to add custom contextual information on transactions.'
}
);
}
}
@ -78,7 +101,10 @@ export function AgentFeatureTipMessage({
<EuiIconWithSpace type="iInCircle" />
{getAgentFeatureText(featureName)}{' '}
<ExternalLink href={docsUrl}>
Learn more in the documentation.
{i18n.translate(
'xpack.apm.propertiesTable.agentFeature.learnMoreLinkLabel',
{ defaultMessage: 'Learn more in the documentation.' }
)}
</ExternalLink>
</TableInfo>
);
@ -113,7 +139,12 @@ export function PropertiesTable({
depth={1}
/>
) : (
<TableInfoHeader>No data available</TableInfoHeader>
<TableInfoHeader>
{i18n.translate(
'xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel',
{ defaultMessage: 'No data available' }
)}
</TableInfoHeader>
)}
<AgentFeatureTipMessage featureName={propKey} agentName={agentName} />
</TableContainer>

View file

@ -4,9 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const PROPERTY_CONFIG = [
{
key: 'request',
label: i18n.translate('xpack.apm.propertiesTable.tabs.requestLabel', {
defaultMessage: 'Request'
}),
required: false,
presortedKeys: [
'http_version',
@ -19,36 +24,57 @@ export const PROPERTY_CONFIG = [
},
{
key: 'response',
label: i18n.translate('xpack.apm.propertiesTable.tabs.responseLabel', {
defaultMessage: 'Response'
}),
required: false,
presortedKeys: ['status_code', 'headers', 'headers_sent', 'finished']
},
{
key: 'system',
label: i18n.translate('xpack.apm.propertiesTable.tabs.systemLabel', {
defaultMessage: 'System'
}),
required: false,
presortedKeys: ['hostname', 'architecture', 'platform']
},
{
key: 'service',
label: i18n.translate('xpack.apm.propertiesTable.tabs.serviceLabel', {
defaultMessage: 'Service'
}),
required: false,
presortedKeys: ['runtime', 'framework', 'agent', 'version']
},
{
key: 'process',
label: i18n.translate('xpack.apm.propertiesTable.tabs.processLabel', {
defaultMessage: 'Process'
}),
required: false,
presortedKeys: ['pid', 'title', 'argv']
},
{
key: 'user',
label: i18n.translate('xpack.apm.propertiesTable.tabs.userLabel', {
defaultMessage: 'User'
}),
required: true,
presortedKeys: ['id', 'username', 'email']
},
{
key: 'tags',
label: i18n.translate('xpack.apm.propertiesTable.tabs.tagsLabel', {
defaultMessage: 'Tags'
}),
required: true,
presortedKeys: []
},
{
key: 'custom',
label: i18n.translate('xpack.apm.propertiesTable.tabs.customLabel', {
defaultMessage: 'Custom'
}),
required: true,
presortedKeys: []
}

View file

@ -5,6 +5,7 @@
*/
import { EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from 'styled-components';
import { IStackframe } from 'x-pack/plugins/apm/typings/es_schemas/Stackframe';
@ -62,7 +63,13 @@ export class LibraryStackFrames extends React.Component<Props, State> {
horizontal={isVisible}
style={{ marginRight: units.half }}
/>{' '}
{stackframes.length} library frames
{i18n.translate(
'xpack.apm.stacktraceTab.libraryFramesToogleButtonLabel',
{
defaultMessage: '{stackframesLength} library frames',
values: { stackframesLength: stackframes.length }
}
)}
</EuiLink>
</LibraryFrameToggle>

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from 'styled-components';
import { IStackframe } from 'x-pack/plugins/apm/typings/es_schemas/Stackframe';
@ -62,7 +63,10 @@ export class Variables extends React.Component<Props> {
horizontal={this.state.isVisible}
style={{ marginRight: units.half }}
/>{' '}
Local variables
{i18n.translate(
'xpack.apm.stacktraceTab.localVariablesToogleButtonLabel',
{ defaultMessage: 'Local variables' }
)}
</VariablesToggle>
{this.state.isVisible && (
<VariablesTableContainer>

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { isEmpty, last } from 'lodash';
import React, { Fragment } from 'react';
import { IStackframe } from '../../../../typings/es_schemas/Stackframe';
@ -20,7 +21,17 @@ interface Props {
export function Stacktrace({ stackframes = [], codeLanguage }: Props) {
if (isEmpty(stackframes)) {
return <EmptyMessage heading="No stacktrace available." hideSubheading />;
return (
<EmptyMessage
heading={i18n.translate(
'xpack.apm.stacktraceTab.noStacktraceAvailableLabel',
{
defaultMessage: 'No stacktrace available.'
}
)}
hideSubheading
/>
);
}
const groups = getGroupedStackframes(stackframes);

View file

@ -9,6 +9,7 @@ import {
YAxis,
HorizontalGridLines,
LineSeries,
LineMarkSeries,
AreaSeries,
VerticalRectSeries
} from 'react-vis';
@ -76,7 +77,18 @@ class StaticPlot extends PureComponent {
fill={serie.areaColor}
/>
);
case 'linemark':
return (
<LineMarkSeries
getNull={d => d.y !== null}
key={serie.title}
xType="time"
curve={'curveMonotoneX'}
data={serie.data}
color={serie.color}
size={0.5}
/>
);
default:
throw new Error(`Unknown type ${serie.type}`);
}

View file

@ -44,21 +44,21 @@ describe('chartSelectors', () => {
data: [{ x: 0, y: 100 }, { x: 1000, y: 200 }],
legendValue: '0 ms',
title: 'Avg.',
type: 'line'
type: 'linemark'
},
{
color: '#ecae23',
data: [{ x: 0, y: 200 }, { x: 1000, y: 300 }],
title: '95th percentile',
titleShort: '95th',
type: 'line'
type: 'linemark'
},
{
color: '#f98510',
data: [{ x: 0, y: 300 }, { x: 1000, y: 400 }],
title: '99th percentile',
titleShort: '99th',
type: 'line'
type: 'linemark'
}
]);
});
@ -84,21 +84,21 @@ describe('chartSelectors', () => {
data: [{ x: 0, y: 5 }, { x: 0, y: 2 }],
legendValue: '3.5 tpm',
title: 'HTTP 2xx',
type: 'line'
type: 'linemark'
},
{
color: '#f98510',
data: [{ x: 0, y: 1 }],
legendValue: '1.0 tpm',
title: 'HTTP 4xx',
type: 'line'
type: 'linemark'
},
{
color: '#db1374',
data: [{ x: 0, y: 0 }],
legendValue: '0.0 tpm',
title: 'HTTP 5xx',
type: 'line'
type: 'linemark'
}
]);
});

View file

@ -150,28 +150,28 @@ export function getCPUSeries(CPUChartResponse: MetricsChartAPIResponse['cpu']) {
{
title: 'Process average',
data: series.processCPUAverage,
type: 'line',
type: 'linemark',
color: colors.apmPink,
legendValue: asPercent(overallValues.processCPUAverage || 0)
},
{
title: 'Process max',
data: series.processCPUMax,
type: 'line',
type: 'linemark',
color: colors.apmPurple,
legendValue: asPercent(overallValues.processCPUMax || 0)
},
{
title: 'System average',
data: series.systemCPUAverage,
type: 'line',
type: 'linemark',
color: colors.apmGreen,
legendValue: asPercent(overallValues.systemCPUAverage || 0)
},
{
title: 'System max',
data: series.systemCPUMax,
type: 'line',
type: 'linemark',
color: colors.apmBlue,
legendValue: asPercent(overallValues.systemCPUMax || 0)
}
@ -206,7 +206,7 @@ export function getResponseTimeSeries(
}),
data: avg,
legendValue: asMillis(overallAvgDuration),
type: 'line',
type: 'linemark',
color: colors.apmBlue
},
{
@ -218,7 +218,7 @@ export function getResponseTimeSeries(
),
titleShort: '95th',
data: p95,
type: 'line',
type: 'linemark',
color: colors.apmYellow
},
{
@ -230,7 +230,7 @@ export function getResponseTimeSeries(
),
titleShort: '99th',
data: p99,
type: 'line',
type: 'linemark',
color: colors.apmOrange
}
];
@ -301,7 +301,7 @@ export function getTpmSeries(
title: getTpmLegendTitle(bucket.key),
data: bucket.dataPoints,
legendValue: `${asDecimal(avg)} ${tpmUnit(transactionType || '')}`,
type: 'line',
type: 'linemark',
color: getColor(bucket.key)
};
});

View file

@ -10,11 +10,6 @@ interface AgentNamedValues {
[agentName: string]: string;
}
export const APM_AGENT_DROPPED_SPANS_DOCS: AgentNamedValues = {
nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#transaction-max-spans`,
python: `${AGENT_URL_ROOT}/python/2.x/configuration.html#config-transaction-max-spans`
};
const APM_AGENT_FEATURE_DOCS: {
[featureName: string]: AgentNamedValues;
} = {

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore
import { metadata } from 'ui/metadata';
const STACK_VERSION = metadata.branch;
export const DROPPED_SPANS_DOCS = `https://www.elastic.co/guide/en/apm/get-started/${STACK_VERSION}/transaction-spans.html#dropped-spans`;

View file

@ -1,12 +0,0 @@
{
"name": "@code/esqueue",
"version": "0.0.0",
"private": true,
"license": "Elastic-License",
"types": "./types/index.d.ts",
"dependencies": {
"lodash": "^4.17.11",
"moment": "^2.20.1",
"puid": "1.0.5"
}
}

View file

@ -32,5 +32,6 @@ export function checkLicense(xPackInfo) {
return {
gis: true,
uid: xPackInfo.license.getUid(),
};
}

View file

@ -9,6 +9,8 @@ import { initRoutes } from './server/routes';
import webLogsSavedObjects from './server/sample_data/web_logs_saved_objects.json';
import mappings from './mappings.json';
import { checkLicense } from './check_license';
import { watchStatusAndLicenseToInitialize } from
'../../server/lib/watch_status_and_license_to_initialize';
export function gis(kibana) {
@ -44,7 +46,15 @@ export function gis(kibana) {
if (gisEnabled) {
const thisPlugin = this;
const xpackMainPlugin = server.plugins.xpack_main;
initRoutes(server);
let routesInitialized = false;
watchStatusAndLicenseToInitialize(xpackMainPlugin, thisPlugin,
async license => {
if (license && license.gis && !routesInitialized) {
routesInitialized = true;
initRoutes(server, license.uid);
}
});
xpackMainPlugin.info
.feature(thisPlugin.id)

View file

@ -11,8 +11,7 @@
"type": "integer"
},
"bounds": {
"type": "geo_shape",
"tree": "quadtree"
"type": "geo_shape"
},
"mapStateJSON": {
"type": "text"

View file

@ -11,7 +11,7 @@ import _ from 'lodash';
const ROOT = `/${GIS_API_PATH}`;
export function initRoutes(server) {
export function initRoutes(server, licenseUid) {
const serverConfig = server.config();
const mapConfig = serverConfig.get('map');
@ -32,7 +32,7 @@ export function initRoutes(server) {
return null;
}
const ems = await getEMSResources();
const ems = await getEMSResources(licenseUid);
const layer = ems.fileLayers.find(layer => layer.id === request.query.id);
if (!layer) {
@ -52,7 +52,7 @@ export function initRoutes(server) {
let ems;
try {
ems = await getEMSResources();
ems = await getEMSResources(licenseUid);
} catch (e) {
console.error('Cannot connect to EMS');
console.error(e);
@ -77,8 +77,9 @@ export function initRoutes(server) {
}
});
async function getEMSResources() {
async function getEMSResources(licenseUid) {
emsClient.addQueryParams({ license: licenseUid });
const fileLayerObjs = await emsClient.getFileLayers();
const tmsServicesObjs = await emsClient.getTMSServices();

View file

@ -18,8 +18,10 @@ export const reloadIndices = (indexNames) => async (dispatch, getState) => {
try {
indices = await request(indexNames);
} catch (error) {
// an index has been deleted, reload the full list
if (error.status === 404) {
// an index has been deleted
// or the user does not have privileges for one of the indices on the current page,
// reload the full list
if (error.status === 404 || error.status === 403) {
return dispatch(loadIndices());
}
return toastNotifications.addDanger(error.data.message);

View file

@ -6,10 +6,6 @@
export type TextScale = 'small' | 'medium' | 'large';
export function getLabelOfTextScale(textScale: TextScale) {
return textScale.charAt(0).toUpperCase() + textScale.slice(1);
}
export function isTextScale(maybeTextScale: string): maybeTextScale is TextScale {
return ['small', 'medium', 'large'].includes(maybeTextScale);
}

View file

@ -8,7 +8,7 @@ import { EuiFormRow, EuiRadioGroup } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import * as React from 'react';
import { getLabelOfTextScale, isTextScale, TextScale } from '../../../common/log_text_scale';
import { isTextScale, TextScale } from '../../../common/log_text_scale';
interface LogTextScaleControlsProps {
availableTextScales: TextScale[];
@ -38,7 +38,20 @@ export class LogTextScaleControls extends React.PureComponent<LogTextScaleContro
<EuiRadioGroup
options={availableTextScales.map((availableTextScale: TextScale) => ({
id: availableTextScale.toString(),
label: getLabelOfTextScale(availableTextScale),
label: (
<FormattedMessage
id="xpack.infra.logs.customizeLogs.textSizeRadioGroup"
defaultMessage="{textScale, select,
small {Small}
medium {Medium}
large {Large}
other {{textScale}}
}"
values={{
textScale: availableTextScale,
}}
/>
),
}))}
idSelected={textScale}
onChange={this.setTextScale}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const SCALE_FACTOR = 0.6;
export const SCALE_FACTOR = 0.55;
export const MAX_SIZE = Infinity;
export const MIN_SIZE = 24;

View file

@ -121,6 +121,12 @@ exports[`AnnotationsTable Minimal initialization without props. 1`] = `
color="primary"
iconType="iInCircle"
size="m"
title="No annotations created for this job"
title={
<FormattedMessage
defaultMessage="No annotations created for this job"
id="xpack.ml.annotationsTable.annotationsNotCreatedTitle"
values={Object {}}
/>
}
/>
`;

View file

@ -44,13 +44,22 @@ import { mlTableService } from '../../services/table_service';
import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search';
import { isTimeSeriesViewJob } from '../../../common/util/job_utils';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
/**
* Table component for rendering the lists of annotations for an ML job.
*/
class AnnotationsTable extends Component {
const AnnotationsTable = injectI18n(class AnnotationsTable extends Component {
static propTypes = {
annotations: PropTypes.array,
jobs: PropTypes.array,
isSingleMetricViewerLinkVisible: PropTypes.bool,
isNumberBadgeVisible: PropTypes.bool
};
constructor(props) {
super(props);
this.state = {
@ -214,7 +223,8 @@ class AnnotationsTable extends Component {
render() {
const {
isSingleMetricViewerLinkVisible = true,
isNumberBadgeVisible = false
isNumberBadgeVisible = false,
intl
} = this.props;
if (this.props.annotations === undefined) {
@ -242,13 +252,28 @@ class AnnotationsTable extends Component {
if (annotations.length === 0) {
return (
<EuiCallOut
title="No annotations created for this job"
title={<FormattedMessage
id="xpack.ml.annotationsTable.annotationsNotCreatedTitle"
defaultMessage="No annotations created for this job"
/>}
iconType="iInCircle"
>
{this.state.jobId && isTimeSeriesViewJob(this.getJob(this.state.jobId)) &&
<p>
To create an annotation,
open the <EuiLink onClick={() => this.openSingleMetricView()}>Single Metric Viewer</EuiLink>
<FormattedMessage
id="xpack.ml.annotationsTable.howToCreateAnnotationDescription"
defaultMessage="To create an annotation, open the {linkToSingleMetricView}"
values={{
linkToSingleMetricView: (
<EuiLink onClick={() => this.openSingleMetricView()}>
<FormattedMessage
id="xpack.ml.annotationsTable.howToCreateAnnotationDescription.singleMetricViewerLinkText"
defaultMessage="Single Metric Viewer"
/>
</EuiLink>
)
}}
/>
</p>
}
</EuiCallOut>
@ -260,45 +285,66 @@ class AnnotationsTable extends Component {
const columns = [
{
field: 'annotation',
name: 'Annotation',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.annotationColumnName',
defaultMessage: 'Annotation',
}),
sortable: true
},
{
field: 'timestamp',
name: 'From',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.fromColumnName',
defaultMessage: 'From',
}),
dataType: 'date',
render: renderDate,
sortable: true,
},
{
field: 'end_timestamp',
name: 'To',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.toColumnName',
defaultMessage: 'To',
}),
dataType: 'date',
render: renderDate,
sortable: true,
},
{
field: 'create_time',
name: 'Creation date',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.creationDateColumnName',
defaultMessage: 'Creation date',
}),
dataType: 'date',
render: renderDate,
sortable: true,
},
{
field: 'create_username',
name: 'Created by',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.createdByColumnName',
defaultMessage: 'Created by',
}),
sortable: true,
},
{
field: 'modified_time',
name: 'Last modified date',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.lastModifiedDateColumnName',
defaultMessage: 'Last modified date',
}),
dataType: 'date',
render: renderDate,
sortable: true,
},
{
field: 'modified_username',
name: 'Last modified by',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.lastModifiedByColumnName',
defaultMessage: 'Last modified by',
}),
sortable: true,
},
];
@ -307,7 +353,10 @@ class AnnotationsTable extends Component {
if (jobIds.length > 1) {
columns.unshift({
field: 'job_id',
name: 'job ID',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.jobIdColumnName',
defaultMessage: 'job ID',
}),
sortable: true,
});
}
@ -315,7 +364,10 @@ class AnnotationsTable extends Component {
if (isNumberBadgeVisible) {
columns.unshift({
field: 'key',
name: 'Label',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.labelColumnName',
defaultMessage: 'Label',
}),
sortable: true,
width: '60px',
render: (key) => {
@ -332,23 +384,45 @@ class AnnotationsTable extends Component {
columns.push({
align: RIGHT_ALIGNMENT,
width: '60px',
name: 'View',
name: intl.formatMessage({
id: 'xpack.ml.annotationsTable.viewColumnName',
defaultMessage: 'View',
}),
render: (annotation) => {
const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id));
const openInSingleMetricViewerText = isDrillDownAvailable
? 'Open in Single Metric Viewer'
: 'Job configuration not supported in Single Metric Viewer';
const openInSingleMetricViewerTooltipText = isDrillDownAvailable ? (
<FormattedMessage
id="xpack.ml.annotationsTable.openInSingleMetricViewerTooltip"
defaultMessage="Open in Single Metric Viewer"
/>
) : (
<FormattedMessage
id="xpack.ml.annotationsTable.jobConfigurationNotSupportedInSingleMetricViewerTooltip"
defaultMessage="Job configuration not supported in Single Metric Viewer"
/>
);
const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable ? (
<FormattedMessage
id="xpack.ml.annotationsTable.openInSingleMetricViewerAriaLabel"
defaultMessage="Open in Single Metric Viewer"
/>
) : (
<FormattedMessage
id="xpack.ml.annotationsTable.jobConfigurationNotSupportedInSingleMetricViewerAriaLabel"
defaultMessage="Job configuration not supported in Single Metric Viewer"
/>
);
return (
<EuiToolTip
position="bottom"
content={openInSingleMetricViewerText}
content={openInSingleMetricViewerTooltipText}
>
<EuiButtonIcon
onClick={() => this.openSingleMetricView(annotation)}
disabled={!isDrillDownAvailable}
iconType="stats"
aria-label={openInSingleMetricViewerText}
aria-label={openInSingleMetricViewerAriaLabelText}
/>
</EuiToolTip>
);
@ -381,12 +455,6 @@ class AnnotationsTable extends Component {
/>
);
}
}
AnnotationsTable.propTypes = {
annotations: PropTypes.array,
jobs: PropTypes.array,
isSingleMetricViewerLinkVisible: PropTypes.bool,
isNumberBadgeVisible: PropTypes.bool
};
});
export { AnnotationsTable };

View file

@ -7,7 +7,7 @@
import jobConfig from '../../../common/types/__mocks__/job_config_farequote';
import mockAnnotations from './__mocks__/mock_annotations.json';
import { shallow } from 'enzyme';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { AnnotationsTable } from './annotations_table';
@ -33,17 +33,17 @@ jest.mock('../../services/ml_api_service', () => ({
describe('AnnotationsTable', () => {
test('Minimal initialization without props.', () => {
const wrapper = shallow(<AnnotationsTable />);
const wrapper = shallowWithIntl(<AnnotationsTable.WrappedComponent />);
expect(wrapper).toMatchSnapshot();
});
test('Initialization with job config prop.', () => {
const wrapper = shallow(<AnnotationsTable jobs={[jobConfig]} />);
const wrapper = shallowWithIntl(<AnnotationsTable.WrappedComponent jobs={[jobConfig]} />);
expect(wrapper).toMatchSnapshot();
});
test('Initialization with annotations prop.', () => {
const wrapper = shallow(<AnnotationsTable annotations={mockAnnotations} />);
const wrapper = shallowWithIntl(<AnnotationsTable.WrappedComponent annotations={mockAnnotations} />);
expect(wrapper).toMatchSnapshot();
});

View file

@ -23,6 +23,8 @@ const module = uiModules.get('apps/ml');
import chrome from 'ui/chrome';
const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
import { I18nProvider } from '@kbn/i18n/react';
module.directive('mlAnnotationTable', function () {
function link(scope, element) {
@ -39,7 +41,9 @@ module.directive('mlAnnotationTable', function () {
};
ReactDOM.render(
React.createElement(AnnotationsTable, props),
<I18nProvider>
{React.createElement(AnnotationsTable, props)}
</I18nProvider>,
element[0]
);
}

View file

@ -75,9 +75,25 @@
min-width: 150px;
}
.mlAnomalyCategoryExamples__link {
width: 100%;
}
.category-example {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mlAnomalyCategoryExamples {
padding: $euiSizeL;
}
.mlAnomalyCategoryExamples__item {
display: block;
white-space: wrap;
font-family: $euiCodeFontFamily;
}
.interim-result {
@ -95,12 +111,15 @@
margin-top: 0px;
flex-basis: 15%;
font-size: inherit;
line-height: 1.5rem;
@include euiTextTruncate;
}
.euiDescriptionList__description {
margin-top: 0px;
flex-basis: 85%;
font-size: inherit;
line-height: 1.5rem;
}
.filter-button {

View file

@ -17,245 +17,22 @@ import React, {
} from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiInMemoryTable,
EuiText,
} from '@elastic/eui';
import {
formatHumanReadableDate,
formatHumanReadableDateTime,
formatHumanReadableDateTimeSeconds
} from '../../util/date_utils';
import { getColumns } from './anomalies_table_columns';
import { DescriptionCell } from './description_cell';
import { DetectorCell } from './detector_cell';
import { EntityCell } from './entity_cell';
import { InfluencersCell } from './influencers_cell';
import { AnomalyDetails } from './anomaly_details';
import { LinksMenu } from './links_menu';
import { checkPermission } from 'plugins/ml/privilege/check_privilege';
import { mlTableService } from '../../services/table_service';
import { mlFieldFormatService } from 'plugins/ml/services/field_format_service';
import { getSeverityColor, isRuleSupported } from 'plugins/ml/../common/util/anomaly_utils';
import { formatValue } from 'plugins/ml/formatters/format_value';
import { RuleEditorFlyout } from 'plugins/ml/components/rule_editor';
const INFLUENCERS_LIMIT = 5; // Maximum number of influencers to display before a 'show more' link is added.
function renderTime(date, aggregationInterval) {
if (aggregationInterval === 'hour') {
return formatHumanReadableDateTime(date);
} else if (aggregationInterval === 'second') {
return formatHumanReadableDateTimeSeconds(date);
} else {
return formatHumanReadableDate(date);
}
}
function showLinksMenuForItem(item) {
const canConfigureRules = (isRuleSupported(item) && checkPermission('canUpdateJob'));
return (canConfigureRules ||
item.isTimeSeriesViewDetector ||
item.entityName === 'mlcategory' ||
item.customUrls !== undefined);
}
function getColumns(
items,
examplesByJobId,
isAggregatedData,
interval,
timefilter,
showViewSeriesLink,
showRuleEditorFlyout,
itemIdToExpandedRowMap,
toggleRow,
filter) {
const columns = [
{
name: '',
render: (item) => (
<EuiButtonIcon
onClick={() => toggleRow(item)}
iconType={itemIdToExpandedRowMap[item.rowId] ? 'arrowDown' : 'arrowRight'}
aria-label={itemIdToExpandedRowMap[item.rowId] ? 'Hide details' : 'Show details'}
data-row-id={item.rowId}
/>
)
},
{
field: 'time',
name: 'time',
dataType: 'date',
render: (date) => renderTime(date, interval),
textOnly: true,
sortable: true
},
{
field: 'severity',
name: `${(isAggregatedData === true) ? 'max ' : ''}severity`,
render: (score) => (
<EuiHealth color={getSeverityColor(score)} compressed="true">
{score >= 1 ? Math.floor(score) : '< 1'}
</EuiHealth>
),
sortable: true
},
{
field: 'detector',
name: 'detector',
render: (detectorDescription, item) => (
<DetectorCell
detectorDescription={detectorDescription}
numberOfRules={item.rulesLength}
/>
),
textOnly: true,
sortable: true
}
];
if (items.some(item => item.entityValue !== undefined)) {
columns.push({
field: 'entityValue',
name: 'found for',
render: (entityValue, item) => (
<EntityCell
entityName={item.entityName}
entityValue={entityValue}
filter={filter}
/>
),
textOnly: true,
sortable: true
});
}
if (items.some(item => item.influencers !== undefined)) {
columns.push({
field: 'influencers',
name: 'influenced by',
render: (influencers) => (
<InfluencersCell
limit={INFLUENCERS_LIMIT}
influencers={influencers}
/>
),
textOnly: true,
sortable: true
});
}
// Map the additional 'sort' fields to the actual, typical and description
// fields to ensure sorting is done correctly on the underlying metric value
// and not on e.g. the actual values array as a String.
if (items.some(item => item.actual !== undefined)) {
columns.push({
field: 'actualSort',
name: 'actual',
render: (actual, item) => {
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
return formatValue(item.actual, item.source.function, fieldFormat);
},
sortable: true
});
}
if (items.some(item => item.typical !== undefined)) {
columns.push({
field: 'typicalSort',
name: 'typical',
render: (typical, item) => {
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
return formatValue(item.typical, item.source.function, fieldFormat);
},
sortable: true
});
// Assume that if we are showing typical, there will be an actual too,
// so we can add a column to describe how actual compares to typical.
const nonTimeOfDayOrWeek = items.some((item) => {
const summaryRecFunc = item.source.function;
return summaryRecFunc !== 'time_of_day' && summaryRecFunc !== 'time_of_week';
});
if (nonTimeOfDayOrWeek === true) {
columns.push({
field: 'metricDescriptionSort',
name: 'description',
render: (metricDescriptionSort, item) => (
<DescriptionCell
actual={item.actual}
typical={item.typical}
/>
),
textOnly: true,
sortable: true
});
}
}
columns.push({
field: 'jobId',
name: 'job ID',
sortable: true
});
const showLinks = (showViewSeriesLink === true) || items.some(item => showLinksMenuForItem(item));
if (showLinks === true) {
columns.push({
name: 'actions',
render: (item) => {
if (showLinksMenuForItem(item) === true) {
return (
<LinksMenu
anomaly={item}
showViewSeriesLink={showViewSeriesLink}
isAggregatedData={isAggregatedData}
interval={interval}
timefilter={timefilter}
showRuleEditorFlyout={showRuleEditorFlyout}
/>
);
} else {
return null;
}
},
sortable: false
});
}
const showExamples = items.some(item => item.entityName === 'mlcategory');
if (showExamples === true) {
columns.push({
name: 'category examples',
sortable: false,
truncateText: true,
render: (item) => {
const examples = _.get(examplesByJobId, [item.jobId, item.entityValue], []);
return (
<EuiText size="xs">
{examples.map((example, i) => {
return <span key={`example${i}`} className="category-example">{example}</span>;
}
)}
</EuiText>
);
},
textOnly: true,
});
}
return columns;
}
import { RuleEditorFlyout } from '../../components/rule_editor';
import {
INFLUENCERS_LIMIT,
ANOMALIES_TABLE_TABS
} from './anomalies_table_constants';
class AnomaliesTable extends Component {
constructor(props) {
@ -294,7 +71,7 @@ class AnomaliesTable extends Component {
return null;
}
toggleRow = (item) => {
toggleRow = (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => {
const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap };
if (itemIdToExpandedRowMap[item.rowId]) {
delete itemIdToExpandedRowMap[item.rowId];
@ -303,6 +80,7 @@ class AnomaliesTable extends Component {
_.get(this.props.tableData, ['examplesByJobId', item.jobId, item.entityValue]) : undefined;
itemIdToExpandedRowMap[item.rowId] = (
<AnomalyDetails
tabIndex={tab}
anomaly={item}
examples={examples}
isAggregatedData={this.isShowingAggregatedData()}
@ -369,6 +147,7 @@ class AnomaliesTable extends Component {
const columns = getColumns(
tableData.anomalies,
tableData.jobIds,
tableData.examplesByJobId,
this.isShowingAggregatedData(),
tableData.interval,

View file

@ -0,0 +1,249 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import mockAnomaliesTableData from '../../explorer/__mocks__/mock_anomalies_table_data.json';
import { getColumns } from './anomalies_table_columns';
jest.mock('../../privilege/check_privilege', () => ({
checkPermission: () => false
}));
jest.mock('../../license/check_license', () => ({
hasLicenseExpired: () => false
}));
jest.mock('../../privilege/get_privileges', () => ({
getPrivileges: () => { }
}));
jest.mock('../../services/field_format_service', () => ({
getFieldFormat: () => { }
}));
jest.mock('./links_menu', () => () => <div id="mocLinkCom">mocked link component</div>);
jest.mock('./description_cell', () => () => <div id="mockDescriptorCom">mocked description component</div>);
jest.mock('./detector_cell', () => () => <div id="mocDetectorCom">mocked detector component</div>);
jest.mock('./entity_cell', () => () => <div id="mocEntityCom">mocked entity component</div>);
jest.mock('./influencers_cell', () => () => <div id="mocInfluencerCom">mocked influencer component</div>);
const columnData = {
items: mockAnomaliesTableData.default.anomalies,
jobIds: mockAnomaliesTableData.default.jobIds,
examplesByJobId: mockAnomaliesTableData.default.examplesByJobId,
isAggregatedData: true,
interval: mockAnomaliesTableData.default.interval,
timefilter: jest.fn(),
showViewSeriesLink: mockAnomaliesTableData.default.showViewSeriesLink,
showRuleEditorFlyout: false,
itemIdToExpandedRowMap: false,
toggleRow: jest.fn(),
filter: undefined
};
describe('AnomaliesTable', () => {
test('all columns created', () => {
const columns = getColumns(
columnData.items,
columnData.jobIds,
columnData.examplesByJobId,
columnData.examplesByJobId,
columnData.sAggregatedData,
columnData.interval,
columnData.timefilter,
columnData.showViewSeriesLink,
columnData.showRuleEditorFlyout,
columnData.itemIdToExpandedRowMap,
columnData.toggleRow,
columnData.filter
);
expect(columns).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: ''
}),
expect.objectContaining({
name: 'time'
}),
expect.objectContaining({
name: 'severity'
}),
expect.objectContaining({
name: 'detector'
}),
expect.objectContaining({
field: 'entityValue',
name: 'found for'
}),
expect.objectContaining({
name: 'influenced by'
}),
expect.objectContaining({
name: 'actual'
}),
expect.objectContaining({
name: 'typical'
}),
expect.objectContaining({
name: 'description'
}),
expect.objectContaining({
name: 'category examples'
})
])
);
});
test('no "found for" column if entityValue missing from items', () => {
const noEntityValueColumnData = {
...columnData,
items: mockAnomaliesTableData.noEntityValue.anomalies
};
const columns = getColumns(
noEntityValueColumnData.items,
noEntityValueColumnData.jobIds,
noEntityValueColumnData.examplesByJobId,
noEntityValueColumnData.examplesByJobId,
noEntityValueColumnData.sAggregatedData,
noEntityValueColumnData.interval,
noEntityValueColumnData.timefilter,
noEntityValueColumnData.showViewSeriesLink,
noEntityValueColumnData.showRuleEditorFlyout,
noEntityValueColumnData.itemIdToExpandedRowMap,
noEntityValueColumnData.toggleRow,
noEntityValueColumnData.filter
);
expect(columns).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
name: 'found for'
}),
])
);
});
test('no "influenced by" column if influencers missing from items', () => {
const noInfluencersColumnData = {
...columnData,
items: mockAnomaliesTableData.noInfluencers.anomalies
};
const columns = getColumns(
noInfluencersColumnData.items,
noInfluencersColumnData.jobIds,
noInfluencersColumnData.examplesByJobId,
noInfluencersColumnData.examplesByJobId,
noInfluencersColumnData.sAggregatedData,
noInfluencersColumnData.interval,
noInfluencersColumnData.timefilter,
noInfluencersColumnData.showViewSeriesLink,
noInfluencersColumnData.showRuleEditorFlyout,
noInfluencersColumnData.itemIdToExpandedRowMap,
noInfluencersColumnData.toggleRow,
noInfluencersColumnData.filter
);
expect(columns).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
name: 'influenced by'
}),
])
);
});
test('no "actual" column if actual missing from items', () => {
const noActualColumnData = {
...columnData,
items: mockAnomaliesTableData.noActual.anomalies
};
const columns = getColumns(
noActualColumnData.items,
noActualColumnData.jobIds,
noActualColumnData.examplesByJobId,
noActualColumnData.examplesByJobId,
noActualColumnData.sAggregatedData,
noActualColumnData.interval,
noActualColumnData.timefilter,
noActualColumnData.showViewSeriesLink,
noActualColumnData.showRuleEditorFlyout,
noActualColumnData.itemIdToExpandedRowMap,
noActualColumnData.toggleRow,
noActualColumnData.filter
);
expect(columns).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
name: 'actual'
}),
])
);
});
test('no "typical" column if typical missing from items', () => {
const noTypicalColumnData = {
...columnData,
items: mockAnomaliesTableData.noTypical.anomalies
};
const columns = getColumns(
noTypicalColumnData.items,
noTypicalColumnData.jobIds,
noTypicalColumnData.examplesByJobId,
noTypicalColumnData.examplesByJobId,
noTypicalColumnData.sAggregatedData,
noTypicalColumnData.interval,
noTypicalColumnData.timefilter,
noTypicalColumnData.showViewSeriesLink,
noTypicalColumnData.showRuleEditorFlyout,
noTypicalColumnData.itemIdToExpandedRowMap,
noTypicalColumnData.toggleRow,
noTypicalColumnData.filter
);
expect(columns).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
name: 'typical'
}),
])
);
});
test('"job ID" column shown if multiple jobs selected', () => {
const multipleJobIdsData = {
...columnData,
jobIds: mockAnomaliesTableData.multipleJobIds.jobIds
};
const columns = getColumns(
multipleJobIdsData.items,
multipleJobIdsData.jobIds,
multipleJobIdsData.examplesByJobId,
multipleJobIdsData.examplesByJobId,
multipleJobIdsData.sAggregatedData,
multipleJobIdsData.interval,
multipleJobIdsData.timefilter,
multipleJobIdsData.showViewSeriesLink,
multipleJobIdsData.showRuleEditorFlyout,
multipleJobIdsData.itemIdToExpandedRowMap,
multipleJobIdsData.toggleRow,
multipleJobIdsData.filter
);
expect(columns).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'job ID'
}),
])
);
});
});

View file

@ -0,0 +1,252 @@
/*
* 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 {
EuiButtonIcon,
EuiHealth,
EuiLink,
} from '@elastic/eui';
import React from 'react';
import _ from 'lodash';
import {
formatHumanReadableDate,
formatHumanReadableDateTime,
formatHumanReadableDateTimeSeconds
} from '../../util/date_utils';
import { DescriptionCell } from './description_cell';
import { DetectorCell } from './detector_cell';
import { EntityCell } from './entity_cell';
import { InfluencersCell } from './influencers_cell';
import { LinksMenu } from './links_menu';
import { checkPermission } from '../../privilege/check_privilege';
import { mlFieldFormatService } from '../../services/field_format_service';
import { getSeverityColor, isRuleSupported } from '../../../common/util/anomaly_utils';
import { formatValue } from '../../formatters/format_value';
import {
INFLUENCERS_LIMIT,
ANOMALIES_TABLE_TABS
} from './anomalies_table_constants';
function renderTime(date, aggregationInterval) {
if (aggregationInterval === 'hour') {
return formatHumanReadableDateTime(date);
} else if (aggregationInterval === 'second') {
return formatHumanReadableDateTimeSeconds(date);
} else {
return formatHumanReadableDate(date);
}
}
function showLinksMenuForItem(item) {
const canConfigureRules = (isRuleSupported(item) && checkPermission('canUpdateJob'));
return (canConfigureRules ||
item.isTimeSeriesViewDetector ||
item.entityName === 'mlcategory' ||
item.customUrls !== undefined);
}
export function getColumns(
items,
jobIds,
examplesByJobId,
isAggregatedData,
interval,
timefilter,
showViewSeriesLink,
showRuleEditorFlyout,
itemIdToExpandedRowMap,
toggleRow,
filter) {
const columns = [
{
name: '',
render: (item) => (
<EuiButtonIcon
onClick={() => toggleRow(item)}
iconType={itemIdToExpandedRowMap[item.rowId] ? 'arrowDown' : 'arrowRight'}
aria-label={itemIdToExpandedRowMap[item.rowId] ? 'Hide details' : 'Show details'}
data-row-id={item.rowId}
/>
)
},
{
field: 'time',
name: 'time',
dataType: 'date',
render: (date) => renderTime(date, interval),
textOnly: true,
sortable: true
},
{
field: 'severity',
name: `${(isAggregatedData === true) ? 'max ' : ''}severity`,
render: (score) => (
<EuiHealth color={getSeverityColor(score)} compressed="true">
{score >= 1 ? Math.floor(score) : '< 1'}
</EuiHealth>
),
sortable: true
},
{
field: 'detector',
name: 'detector',
render: (detectorDescription, item) => (
<DetectorCell
detectorDescription={detectorDescription}
numberOfRules={item.rulesLength}
/>
),
textOnly: true,
sortable: true
}
];
if (items.some(item => item.entityValue !== undefined)) {
columns.push({
field: 'entityValue',
name: 'found for',
render: (entityValue, item) => (
<EntityCell
entityName={item.entityName}
entityValue={entityValue}
filter={filter}
/>
),
textOnly: true,
sortable: true
});
}
if (items.some(item => item.influencers !== undefined)) {
columns.push({
field: 'influencers',
name: 'influenced by',
render: (influencers) => (
<InfluencersCell
limit={INFLUENCERS_LIMIT}
influencers={influencers}
/>
),
textOnly: true,
sortable: true
});
}
// Map the additional 'sort' fields to the actual, typical and description
// fields to ensure sorting is done correctly on the underlying metric value
// and not on e.g. the actual values array as a String.
if (items.some(item => item.actual !== undefined)) {
columns.push({
field: 'actualSort',
name: 'actual',
render: (actual, item) => {
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
return formatValue(item.actual, item.source.function, fieldFormat);
},
sortable: true
});
}
if (items.some(item => item.typical !== undefined)) {
columns.push({
field: 'typicalSort',
name: 'typical',
render: (typical, item) => {
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
return formatValue(item.typical, item.source.function, fieldFormat);
},
sortable: true
});
// Assume that if we are showing typical, there will be an actual too,
// so we can add a column to describe how actual compares to typical.
const nonTimeOfDayOrWeek = items.some((item) => {
const summaryRecFunc = item.source.function;
return summaryRecFunc !== 'time_of_day' && summaryRecFunc !== 'time_of_week';
});
if (nonTimeOfDayOrWeek === true) {
columns.push({
field: 'metricDescriptionSort',
name: 'description',
render: (metricDescriptionSort, item) => (
<DescriptionCell
actual={item.actual}
typical={item.typical}
/>
),
textOnly: true,
sortable: true
});
}
}
if (jobIds && jobIds.length > 1) {
columns.push({
field: 'jobId',
name: 'job ID',
sortable: true
});
}
const showExamples = items.some(item => item.entityName === 'mlcategory');
if (showExamples === true) {
columns.push({
name: 'category examples',
sortable: false,
truncateText: true,
render: (item) => {
const examples = _.get(examplesByJobId, [item.jobId, item.entityValue], []);
return (
<EuiLink
className="mlAnomalyCategoryExamples__link"
onClick={() => toggleRow(item, ANOMALIES_TABLE_TABS.CATEGORY_EXAMPLES)}
>
{examples.map((example, i) => {
return <span key={`example${i}`} className="category-example">{example}</span>;
}
)}
</EuiLink>
);
},
textOnly: true,
width: '13%'
});
}
const showLinks = (showViewSeriesLink === true) || items.some(item => showLinksMenuForItem(item));
if (showLinks === true) {
columns.push({
name: 'actions',
render: (item) => {
if (showLinksMenuForItem(item) === true) {
return (
<LinksMenu
anomaly={item}
showViewSeriesLink={showViewSeriesLink}
isAggregatedData={isAggregatedData}
interval={interval}
timefilter={timefilter}
showRuleEditorFlyout={showRuleEditorFlyout}
/>
);
} else {
return null;
}
},
sortable: false
});
}
return columns;
}

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
// Maximum number of influencers to display before a 'show more' link is added.
export const INFLUENCERS_LIMIT = 5;
export const ANOMALIES_TABLE_TABS = {
DETAILS: 0,
CATEGORY_EXAMPLES: 1
};

View file

@ -11,14 +11,17 @@
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import _ from 'lodash';
import {
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiSpacer,
EuiTabbedContent,
EuiText
} from '@elastic/eui';
import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils';
@ -115,13 +118,6 @@ function getDetailsItems(anomaly, examples, filter) {
description: timeDesc
});
if (examples !== undefined && examples.length > 0) {
examples.forEach((example, index) => {
const title = (index === 0) ? 'category examples' : '';
items.push({ title, description: example });
});
}
items.push({
title: 'function',
description: (source.function !== 'metric') ? source.function : source.function_description
@ -188,12 +184,58 @@ export class AnomalyDetails extends Component {
this.state = {
showAllInfluencers: false
};
if (this.props.examples !== undefined && this.props.examples.length > 0) {
this.tabs = [{
id: 'Details',
name: 'Details',
content: (
<Fragment>
<div className="ml-anomalies-table-details">
{this.renderDescription()}
<EuiSpacer size="m" />
{this.renderDetails()}
{this.renderInfluencers()}
</div>
</Fragment>
)
},
{
id: 'Category examples',
name: 'Category examples',
content: (
<Fragment>
{this.renderCategoryExamples()}
</Fragment>
),
}
];
}
}
toggleAllInfluencers() {
this.setState({ showAllInfluencers: !this.state.showAllInfluencers });
}
renderCategoryExamples() {
return (
<EuiFlexGroup
direction="column"
justifyContent="center"
gutterSize="m"
className="mlAnomalyCategoryExamples"
>
{this.props.examples.map((example, i) => {
return (
<EuiFlexItem key={`example${i}`}>
<span className="mlAnomalyCategoryExamples__item">{example}</span>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);
}
renderDescription() {
const anomaly = this.props.anomaly;
const source = anomaly.source;
@ -315,15 +357,27 @@ export class AnomalyDetails extends Component {
}
render() {
const { tabIndex } = this.props;
return (
<div className="ml-anomalies-table-details">
{this.renderDescription()}
<EuiSpacer size="m" />
{this.renderDetails()}
{this.renderInfluencers()}
</div>
);
if (this.tabs !== undefined) {
return (
<EuiTabbedContent
tabs={this.tabs}
size="s"
initialSelectedTab={this.tabs[tabIndex]}
onTabClick={() => {}}
/>
);
} else {
return (
<div className="ml-anomalies-table-details">
{this.renderDescription()}
<EuiSpacer size="m" />
{this.renderDetails()}
{this.renderInfluencers()}
</div>
);
}
}
}
@ -332,5 +386,6 @@ AnomalyDetails.propTypes = {
examples: PropTypes.array,
isAggregatedData: PropTypes.bool,
filter: PropTypes.func,
influencersLimit: PropTypes.number
influencersLimit: PropTypes.number,
tabIndex: PropTypes.number.isRequired
};

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { AnomalyDetails } from './anomaly_details';
const props = {
anomaly: {
time: 1486018800000,
source: {
job_id: 'it-ops-count-by-mlcategory-one',
result_type: 'record',
probability: 0.004741615571416013,
multi_bucket_impact: 0,
record_score: 25.662,
initial_record_score: 41.00971774894037,
bucket_span: 900,
detector_index: 0,
is_interim: false,
timestamp: 1486062900000,
by_field_name: 'mlcategory',
by_field_value: '1',
function: 'count',
function_description: 'count',
typical: [
0.012071679592192066
],
actual: [
1
],
mlcategory: [
'1'
]
},
rowId: '1546553208554_0',
jobId: 'it-ops-count-by-mlcategory-one',
detectorIndex: 0,
severity: 25.662,
entityName: 'mlcategory',
entityValue: '1',
actual: [
1
],
actualSort: 1,
typical: [
0.012071679592192066
],
typicalSort: 0.012071679592192066,
metricDescriptionSort: 82.83851409101328,
detector: 'count by mlcategory',
isTimeSeriesViewDetector: false
},
examples: [
'Actual Transaction Already Voided / Reversed;hostname=dbserver.acme.com;physicalhost=esxserver1.acme.com;vmhost=app1.acme.com',
'REC Not INSERTED [DB TRAN] Table;hostname=dbserver.acme.com;physicalhost=esxserver1.acme.com;vmhost=app1.acme.com'
],
influencersLimit: 5,
isAggregatedData: true,
tabIndex: 0
};
describe('AnomalyDetails', () => {
test('Renders with anomaly details tab selected by default', () => {
const wrapper = shallow(
<AnomalyDetails {...props} />
);
expect(wrapper.prop('tabs').length).toBe(2);
expect(wrapper.prop('initialSelectedTab').id).toBe('Details');
});
test('Renders with category tab selected when index set to 1', () => {
const categoryTabProps = {
...props,
tabIndex: 1
};
const wrapper = shallow(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.prop('initialSelectedTab').id).toBe('Category examples');
});
});

View file

@ -15,7 +15,7 @@ import {
EuiText
} from '@elastic/eui';
import { getMetricChangeDescription } from 'plugins/ml/formatters/metric_change_description';
import { getMetricChangeDescription } from '../../formatters/metric_change_description';
/*
* Component for rendering the description cell in the anomalies table, which provides a

View file

@ -21,16 +21,16 @@ import {
import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify';
import { ES_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types';
import { checkPermission } from 'plugins/ml/privilege/check_privilege';
import { isRuleSupported } from 'plugins/ml/../common/util/anomaly_utils';
import { parseInterval } from 'plugins/ml/../common/util/parse_interval';
import { getFieldTypeFromMapping } from 'plugins/ml/services/mapping_service';
import { ml } from 'plugins/ml/services/ml_api_service';
import { mlJobService } from 'plugins/ml/services/job_service';
import { getUrlForRecord } from 'plugins/ml/util/custom_url_utils';
import { getIndexPatterns } from 'plugins/ml/util/index_utils';
import { replaceStringTokens } from 'plugins/ml/util/string_utils';
import { ES_FIELD_TYPES } from '../../../common/constants/field_types';
import { checkPermission } from '../../privilege/check_privilege';
import { isRuleSupported } from '../../../common/util/anomaly_utils';
import { parseInterval } from '../../../common/util/parse_interval';
import { getFieldTypeFromMapping } from '../../services/mapping_service';
import { ml } from '../../services/ml_api_service';
import { mlJobService } from '../../services/job_service';
import { getUrlForRecord } from '../../util/custom_url_utils';
import { getIndexPatterns } from '../../util/index_utils';
import { replaceStringTokens } from '../../util/string_utils';
/*

View file

@ -14,7 +14,7 @@
<ui-select-choices
repeat="item in mlItemSelect.allItems | filter: { id: $select.search }"
>
<div ng-if="item.isTag" class="select-item" ng-bind-html="(item.id | highlight: $select.search) +' <small>'+ (mlItemSelect.taggingText === undefined ? '(new item)' : mlItemSelect.taggingText) +'</small>'"></div>
<div ng-if="item.isTag" class="select-item" ng-bind-html="(item.id | highlight: $select.search) +' <small>'+ (mlItemSelect.taggingText === undefined ? mlItemSelect.newItemLabel : mlItemSelect.taggingText) +'</small>'"></div>
<div ng-if="!item.isTag" class="select-item" >
<div ng-bind-html="item.id | highlight: $select.search"></div>
<small ng-if="item.extra!==undefined">

Some files were not shown because too many files have changed in this diff Show more