mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Merge remote-tracking branch 'origin/master' into feature/merge-code
This commit is contained in:
commit
78af65ebad
183 changed files with 6936 additions and 1509 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"]
|
||||
+
|
||||
|
|
|
@ -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"]
|
||||
|
||||
--
|
||||
|
||||
|
|
|
@ -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[]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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.
|
||||
|
||||
--
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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].
|
||||
--
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]
|
||||
============================================================================
|
||||
|
|
|
@ -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}>>.
|
||||
|
|
17
docs/settings/general-infra-logs-ui-settings.asciidoc
Normal file
17
docs/settings/general-infra-logs-ui-settings.asciidoc
Normal 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`.
|
14
docs/settings/infrastructure-ui-settings.asciidoc
Normal file
14
docs/settings/infrastructure-ui-settings.asciidoc
Normal 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[]
|
14
docs/settings/logs-ui-settings.asciidoc
Normal file
14
docs/settings/logs-ui-settings.asciidoc
Normal 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[]
|
|
@ -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[]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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",
|
||||
|
|
22
src/dev/failed_tests/cli.js
Normal file
22
src/dev/failed_tests/cli.js
Normal 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();
|
|
@ -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.`));
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -72,6 +72,7 @@ export function visWithSplits(WrappedComponent) {
|
|||
onBrush={props.onBrush}
|
||||
additionalLabel={label}
|
||||
backgroundColor={props.backgroundColor}
|
||||
getConfig={props.getConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)');
|
||||
});
|
||||
});
|
|
@ -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(),
|
||||
};
|
||||
};
|
|
@ -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' },
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -439,7 +439,9 @@ export class KibanaMap extends EventEmitter {
|
|||
}
|
||||
this._baseLayerIsDesaturated = isDesaturated;
|
||||
this._updateDesaturation();
|
||||
this._leafletBaseLayer.redraw();
|
||||
if (this._leafletBaseLayer) {
|
||||
this._leafletBaseLayer.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
addDrawControl() {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
xvfb-run "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:report;
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
];
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: []
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
} = {
|
||||
|
|
|
@ -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`;
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -32,5 +32,6 @@ export function checkLicense(xPackInfo) {
|
|||
|
||||
return {
|
||||
gis: true,
|
||||
uid: xPackInfo.license.getUid(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
"type": "integer"
|
||||
},
|
||||
"bounds": {
|
||||
"type": "geo_shape",
|
||||
"tree": "quadtree"
|
||||
"type": "geo_shape"
|
||||
},
|
||||
"mapStateJSON": {
|
||||
"type": "text"
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
/*
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue