[8.x] [ML] Anomaly Detection: Adds popover links menu to anomaly explorer charts. (#186587) (#193383)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ML] Anomaly Detection: Adds popover links menu to anomaly explorer
charts. (#186587)](https://github.com/elastic/kibana/pull/186587)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Walter
Rafelsberger","email":"walter.rafelsberger@elastic.co"},"sourceCommit":{"committedDate":"2024-09-19T06:10:38Z","message":"[ML]
Anomaly Detection: Adds popover links menu to anomaly explorer charts.
(#186587)\n\n## Summary\r\n\r\nAdds support for clicking on Anomaly
Explorer charts to trigger the\r\nactions popover menu.\r\n\r\n- [x]
ExplorerChartSingleMetric\r\n- [x] ExplorerChartDistribution\r\n- [x]
Support for embedded charts\r\n\r\nAnomaly
Explorer\r\n\r\n[ml-anomaly-charts-actions-0001.webm](ee519b47-e924-4947-b127-4f3ecf62616e)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"854cb15725f059aa80abea6d7155190ef88622ef","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement",":ml","Feature:Anomaly
Detection","v9.0.0","v8.16.0"],"title":"[ML] Anomaly Detection: Adds
popover links menu to anomaly explorer
charts.","number":186587,"url":"https://github.com/elastic/kibana/pull/186587","mergeCommit":{"message":"[ML]
Anomaly Detection: Adds popover links menu to anomaly explorer charts.
(#186587)\n\n## Summary\r\n\r\nAdds support for clicking on Anomaly
Explorer charts to trigger the\r\nactions popover menu.\r\n\r\n- [x]
ExplorerChartSingleMetric\r\n- [x] ExplorerChartDistribution\r\n- [x]
Support for embedded charts\r\n\r\nAnomaly
Explorer\r\n\r\n[ml-anomaly-charts-actions-0001.webm](ee519b47-e924-4947-b127-4f3ecf62616e)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"854cb15725f059aa80abea6d7155190ef88622ef"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/186587","number":186587,"mergeCommit":{"message":"[ML]
Anomaly Detection: Adds popover links menu to anomaly explorer charts.
(#186587)\n\n## Summary\r\n\r\nAdds support for clicking on Anomaly
Explorer charts to trigger the\r\nactions popover menu.\r\n\r\n- [x]
ExplorerChartSingleMetric\r\n- [x] ExplorerChartDistribution\r\n- [x]
Support for embedded charts\r\n\r\nAnomaly
Explorer\r\n\r\n[ml-anomaly-charts-actions-0001.webm](ee519b47-e924-4947-b127-4f3ecf62616e)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"854cb15725f059aa80abea6d7155190ef88622ef"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Walter Rafelsberger <walter.rafelsberger@elastic.co>
This commit is contained in:
Kibana Machine 2024-09-21 00:30:29 +10:00 committed by GitHub
parent a4fb640635
commit e0e48ccfb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1465 additions and 40 deletions

View file

@ -343,7 +343,7 @@ export interface MlAnomaliesTableRecord {
/**
* Returns true if the job has the model plot enabled
*/
modelPlotEnabled: boolean;
modelPlotEnabled?: boolean;
}
/**

View file

@ -1,4 +1,5 @@
{ "default": {
{
"default": {
"anomalies": [
{
"time": 1486018800000,
@ -44,9 +45,11 @@
"metricDescriptionSort": 82.83851409101328,
"detector": "count by mlcategory",
"isTimeSeriesViewDetector": false,
"influencers": [
"mockInfluencer"
]
"influencers": [
{
"mockInfluencerField": "mockInfluencerValue"
}
]
},
{
"time": 1486018800000,
@ -92,12 +95,16 @@
"metricDescriptionSort": 38.82201810127708,
"detector": "count by mlcategory",
"isTimeSeriesViewDetector": false,
"influencers": [
"mockInfluencer"
]
"influencers": [
{
"mockInfluencerField": "mockInfluencerValue"
}
]
}
],
"jobIds": ["it-ops-count-by-mlcategory-one"],
"jobIds": [
"it-ops-count-by-mlcategory-one"
],
"interval": "day",
"examplesByJobId": {
"it-ops-count-by-mlcategory-one": {
@ -161,7 +168,9 @@
"detector": "count by mlcategory",
"isTimeSeriesViewDetector": false,
"influencers": [
"mockInfluencer"
{
"mockInfluencerField": "mockInfluencerValue"
}
]
},
{
@ -208,7 +217,9 @@
"detector": "count by mlcategory",
"isTimeSeriesViewDetector": false,
"influencers": [
"mockInfluencer"
{
"mockInfluencerField": "mockInfluencerValue"
}
]
}
],
@ -496,7 +507,9 @@
"detector": "count by mlcategory",
"isTimeSeriesViewDetector": false,
"influencers": [
"mockInfluencer"
{
"mockInfluencerField": "mockInfluencerValue"
}
]
},
{
@ -541,7 +554,9 @@
"detector": "count by mlcategory",
"isTimeSeriesViewDetector": false,
"influencers": [
"mockInfluencer"
{
"mockInfluencerField": "mockInfluencerValue"
}
]
}
],

View file

@ -0,0 +1,975 @@
{
"default": {
"anomalies": [
{
"time": 1725840000000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.00011921600143273021,
"multi_bucket_impact": -5,
"record_score": 94.31236,
"initial_record_score": 91.59429607036628,
"bucket_span": 900,
"detector_index": 0,
"is_interim": false,
"timestamp": 1725862500000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Clothing",
"function": "mean",
"function_description": "mean",
"typical": [
15.851794819088305
],
"actual": [
350
],
"field_name": "products.price",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"SA"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 8,
"lower_confidence_bound": 5.733288153284621,
"typical_value": 15.851794819088305,
"upper_confidence_bound": 41.175974376816086
},
"category.keyword": [
"Men's Clothing"
],
"geoip.country_iso_code": [
"SA"
]
},
"rowId": "1726503845974_0",
"jobId": "ecom_dect_01",
"detectorIndex": 0,
"severity": 94.31236,
"entityName": "category.keyword",
"entityValue": "Men's Clothing",
"influencers": [
{
"category.keyword": "Men's Clothing"
},
{
"geoip.country_iso_code": "SA"
}
],
"actual": [
350
],
"actualSort": 350,
"typical": [
15.851794819088305
],
"typicalSort": 15.851794819088305,
"metricDescriptionSort": 22.079518691381207,
"detector": "mean(\"products.price\") partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1725840000000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.03461785745665291,
"multi_bucket_impact": -5,
"record_score": 10.620156126608986,
"initial_record_score": 10.620156126608986,
"bucket_span": 900,
"detector_index": 0,
"is_interim": false,
"timestamp": 1725862500000,
"partition_field_name": "category.keyword",
"partition_field_value": "Women's Clothing",
"function": "mean",
"function_description": "mean",
"typical": [
17.553578210957593
],
"actual": [
45
],
"field_name": "products.price",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Women's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"SA"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 2,
"lower_confidence_bound": 7.606026510878394,
"typical_value": 17.553578210957593,
"upper_confidence_bound": 37.852824407923066
},
"category.keyword": [
"Women's Clothing"
],
"geoip.country_iso_code": [
"SA"
]
},
"rowId": "1726503845974_1",
"jobId": "ecom_dect_01",
"detectorIndex": 0,
"severity": 10.620156126608986,
"entityName": "category.keyword",
"entityValue": "Women's Clothing",
"influencers": [
{
"category.keyword": "Women's Clothing"
},
{
"geoip.country_iso_code": "SA"
}
],
"actual": [
45
],
"actualSort": 45,
"typical": [
17.553578210957593
],
"typicalSort": 17.553578210957593,
"metricDescriptionSort": 2.5635798843514044,
"detector": "mean(\"products.price\") partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1725926400000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.01787543314280476,
"multi_bucket_impact": -5,
"record_score": 0.6916846762237938,
"initial_record_score": 0.6916846762237938,
"bucket_span": 900,
"detector_index": 0,
"is_interim": false,
"timestamp": 1725990300000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Clothing",
"function": "mean",
"function_description": "mean",
"typical": [
15.779865757388727
],
"actual": [
65
],
"field_name": "products.price",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"US"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 3,
"lower_confidence_bound": 5.889827882876367,
"typical_value": 15.779865757388727,
"upper_confidence_bound": 39.8666079359938
},
"category.keyword": [
"Men's Clothing"
],
"geoip.country_iso_code": [
"US"
]
},
"rowId": "1726503845974_2",
"jobId": "ecom_dect_01",
"detectorIndex": 0,
"severity": 0.6916846762237938,
"entityName": "category.keyword",
"entityValue": "Men's Clothing",
"influencers": [
{
"category.keyword": "Men's Clothing"
},
{
"geoip.country_iso_code": "US"
}
],
"actual": [
65
],
"actualSort": 65,
"typical": [
15.779865757388727
],
"typicalSort": 15.779865757388727,
"metricDescriptionSort": 4.1191731919243075,
"detector": "mean(\"products.price\") partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726012800000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.013914839080385263,
"multi_bucket_impact": -5,
"record_score": 24.552541318692445,
"initial_record_score": 24.552541318692445,
"bucket_span": 900,
"detector_index": 0,
"is_interim": false,
"timestamp": 1726056000000,
"partition_field_name": "category.keyword",
"partition_field_value": "Women's Accessories,Women's Clothing",
"function": "mean",
"function_description": "mean",
"typical": [
16.072466356993818
],
"actual": [
42
],
"field_name": "products.price",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Women's Accessories,Women's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"AE"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 2,
"lower_confidence_bound": 7.901062666669044,
"typical_value": 16.072466356993818,
"upper_confidence_bound": 31.621998523165693
},
"category.keyword": [
"Women's Accessories,Women's Clothing"
],
"geoip.country_iso_code": [
"AE"
]
},
"rowId": "1726503845974_3",
"jobId": "ecom_dect_01",
"detectorIndex": 0,
"severity": 24.552541318692445,
"entityName": "category.keyword",
"entityValue": "Women's Accessories,Women's Clothing",
"influencers": [
{
"category.keyword": "Women's Accessories,Women's Clothing"
},
{
"geoip.country_iso_code": "AE"
}
],
"actual": [
42
],
"actualSort": 42,
"typical": [
16.072466356993818
],
"typicalSort": 16.072466356993818,
"metricDescriptionSort": 2.613164592609273,
"detector": "mean(\"products.price\") partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726012800000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.013569993829022374,
"multi_bucket_impact": -5,
"record_score": 1.08840456688412,
"initial_record_score": 1.08840456688412,
"bucket_span": 900,
"detector_index": 0,
"is_interim": false,
"timestamp": 1726083900000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Clothing",
"function": "mean",
"function_description": "mean",
"typical": [
15.91745643577788
],
"actual": [
75
],
"field_name": "products.price",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"AE"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 3,
"lower_confidence_bound": 5.831067931970601,
"typical_value": 15.91745643577788,
"upper_confidence_bound": 40.89686519101566
},
"category.keyword": [
"Men's Clothing"
],
"geoip.country_iso_code": [
"AE"
]
},
"rowId": "1726503845974_4",
"jobId": "ecom_dect_01",
"detectorIndex": 0,
"severity": 1.08840456688412,
"entityName": "category.keyword",
"entityValue": "Men's Clothing",
"influencers": [
{
"category.keyword": "Men's Clothing"
},
{
"geoip.country_iso_code": "AE"
}
],
"actual": [
75
],
"actualSort": 75,
"typical": [
15.91745643577788
],
"typicalSort": 15.91745643577788,
"metricDescriptionSort": 4.711808089602902,
"detector": "mean(\"products.price\") partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726012800000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.012483410285350998,
"multi_bucket_impact": -5,
"record_score": 26.21197793819939,
"initial_record_score": 26.21197793819939,
"bucket_span": 900,
"detector_index": 1,
"is_interim": false,
"timestamp": 1726065000000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Clothing",
"function": "distinct_count",
"function_description": "distinct_count",
"typical": [
1
],
"actual": [
2
],
"field_name": "total_unique_products",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"FR",
"MA",
"TR"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 3,
"lower_confidence_bound": 0,
"typical_value": 0,
"upper_confidence_bound": 0
},
"category.keyword": [
"Men's Clothing"
],
"geoip.country_iso_code": [
"FR",
"MA",
"TR"
]
},
"rowId": "1726503845974_5",
"jobId": "ecom_dect_01",
"detectorIndex": 1,
"severity": 26.21197793819939,
"entityName": "category.keyword",
"entityValue": "Men's Clothing",
"influencers": [
{
"category.keyword": "Men's Clothing"
},
{
"geoip.country_iso_code": "FR"
},
{
"geoip.country_iso_code": "MA"
},
{
"geoip.country_iso_code": "TR"
}
],
"actual": [
2
],
"actualSort": 2,
"typical": [
1
],
"typicalSort": 1,
"metricDescriptionSort": 2,
"detector": "distinct_count(total_unique_products) partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726012800000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.029221215176833293,
"multi_bucket_impact": -5,
"record_score": 13.210841360998488,
"initial_record_score": 13.210841360998488,
"bucket_span": 900,
"detector_index": 1,
"is_interim": false,
"timestamp": 1726065000000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Clothing,Men's Shoes",
"function": "distinct_count",
"function_description": "distinct_count",
"typical": [
1
],
"actual": [
2
],
"field_name": "total_unique_products",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Clothing,Men's Shoes"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"EG",
"US"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 2,
"lower_confidence_bound": 0,
"typical_value": 0,
"upper_confidence_bound": 0
},
"category.keyword": [
"Men's Clothing,Men's Shoes"
],
"geoip.country_iso_code": [
"EG",
"US"
]
},
"rowId": "1726503845974_6",
"jobId": "ecom_dect_01",
"detectorIndex": 1,
"severity": 13.210841360998488,
"entityName": "category.keyword",
"entityValue": "Men's Clothing,Men's Shoes",
"influencers": [
{
"category.keyword": "Men's Clothing,Men's Shoes"
},
{
"geoip.country_iso_code": "EG"
},
{
"geoip.country_iso_code": "US"
}
],
"actual": [
2
],
"actualSort": 2,
"typical": [
1
],
"typicalSort": 1,
"metricDescriptionSort": 2,
"detector": "distinct_count(total_unique_products) partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726099200000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.01614665640555698,
"multi_bucket_impact": -5,
"record_score": 22.27855551555865,
"initial_record_score": 22.27855551555865,
"bucket_span": 900,
"detector_index": 0,
"is_interim": false,
"timestamp": 1726133400000,
"partition_field_name": "category.keyword",
"partition_field_value": "Women's Accessories,Women's Shoes",
"function": "mean",
"function_description": "mean",
"typical": [
17.70351589303024
],
"actual": [
65
],
"field_name": "products.price",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Women's Accessories,Women's Shoes"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"US"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 2,
"lower_confidence_bound": 6.522434202795708,
"typical_value": 17.70351589303024,
"upper_confidence_bound": 44.79010478599095
},
"category.keyword": [
"Women's Accessories,Women's Shoes"
],
"geoip.country_iso_code": [
"US"
]
},
"rowId": "1726503845974_7",
"jobId": "ecom_dect_01",
"detectorIndex": 0,
"severity": 22.27855551555865,
"entityName": "category.keyword",
"entityValue": "Women's Accessories,Women's Shoes",
"influencers": [
{
"category.keyword": "Women's Accessories,Women's Shoes"
},
{
"geoip.country_iso_code": "US"
}
],
"actual": [
65
],
"actualSort": 65,
"typical": [
17.70351589303024
],
"typicalSort": 17.70351589303024,
"metricDescriptionSort": 3.6715870673796545,
"detector": "mean(\"products.price\") partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726099200000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.01894227723384407,
"multi_bucket_impact": -5,
"record_score": 19.837546336871547,
"initial_record_score": 19.837546336871547,
"bucket_span": 900,
"detector_index": 1,
"is_interim": false,
"timestamp": 1726121700000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Clothing",
"function": "distinct_count",
"function_description": "distinct_count",
"typical": [
1
],
"actual": [
2
],
"field_name": "total_unique_products",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"AE",
"US"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 2,
"lower_confidence_bound": 0,
"typical_value": 0,
"upper_confidence_bound": 0
},
"category.keyword": [
"Men's Clothing"
],
"geoip.country_iso_code": [
"AE",
"US"
]
},
"rowId": "1726503845974_8",
"jobId": "ecom_dect_01",
"detectorIndex": 1,
"severity": 19.837546336871547,
"entityName": "category.keyword",
"entityValue": "Men's Clothing",
"influencers": [
{
"category.keyword": "Men's Clothing"
},
{
"geoip.country_iso_code": "AE"
},
{
"geoip.country_iso_code": "US"
}
],
"actual": [
2
],
"actualSort": 2,
"typical": [
1
],
"typicalSort": 1,
"metricDescriptionSort": 2,
"detector": "distinct_count(total_unique_products) partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726185600000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.005452151333957955,
"multi_bucket_impact": -5,
"record_score": 38.875218910067446,
"initial_record_score": 38.875218910067446,
"bucket_span": 900,
"detector_index": 0,
"is_interim": false,
"timestamp": 1726186500000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Accessories,Men's Clothing",
"function": "mean",
"function_description": "mean",
"typical": [
15.943152908258694
],
"actual": [
60
],
"field_name": "products.price",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Accessories,Men's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"AE"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 3,
"lower_confidence_bound": 6.39588429144841,
"typical_value": 15.943152908258694,
"upper_confidence_bound": 37.04143942410928
},
"category.keyword": [
"Men's Accessories,Men's Clothing"
],
"geoip.country_iso_code": [
"AE"
]
},
"rowId": "1726503845974_9",
"jobId": "ecom_dect_01",
"detectorIndex": 0,
"severity": 38.875218910067446,
"entityName": "category.keyword",
"entityValue": "Men's Accessories,Men's Clothing",
"influencers": [
{
"category.keyword": "Men's Accessories,Men's Clothing"
},
{
"geoip.country_iso_code": "AE"
}
],
"actual": [
60
],
"actualSort": 60,
"typical": [
15.943152908258694
],
"typicalSort": 15.943152908258694,
"metricDescriptionSort": 3.763371043686062,
"detector": "mean(\"products.price\") partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726185600000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.030245620963532376,
"multi_bucket_impact": -5,
"record_score": 12.684121118422576,
"initial_record_score": 12.684121118422576,
"bucket_span": 900,
"detector_index": 0,
"is_interim": false,
"timestamp": 1726225200000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Clothing,Men's Shoes",
"function": "mean",
"function_description": "mean",
"typical": [
19.693271090135916
],
"actual": [
75
],
"field_name": "products.price",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Clothing,Men's Shoes"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"AE"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 2,
"lower_confidence_bound": 6.850458655008051,
"typical_value": 19.693271090135916,
"upper_confidence_bound": 53.49007700314821
},
"category.keyword": [
"Men's Clothing,Men's Shoes"
],
"geoip.country_iso_code": [
"AE"
]
},
"rowId": "1726503845974_10",
"jobId": "ecom_dect_01",
"detectorIndex": 0,
"severity": 12.684121118422576,
"entityName": "category.keyword",
"entityValue": "Men's Clothing,Men's Shoes",
"influencers": [
{
"category.keyword": "Men's Clothing,Men's Shoes"
},
{
"geoip.country_iso_code": "AE"
}
],
"actual": [
75
],
"actualSort": 75,
"typical": [
19.693271090135916
],
"typicalSort": 19.693271090135916,
"metricDescriptionSort": 3.808407433012307,
"detector": "mean(\"products.price\") partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
},
{
"time": 1726185600000,
"source": {
"job_id": "ecom_dect_01",
"result_type": "record",
"probability": 0.024450443154858347,
"multi_bucket_impact": -5,
"record_score": 15.93562008623777,
"initial_record_score": 15.93562008623777,
"bucket_span": 900,
"detector_index": 1,
"is_interim": false,
"timestamp": 1726194600000,
"partition_field_name": "category.keyword",
"partition_field_value": "Men's Clothing",
"function": "distinct_count",
"function_description": "distinct_count",
"typical": [
1
],
"actual": [
2
],
"field_name": "total_unique_products",
"influencers": [
{
"influencer_field_name": "category.keyword",
"influencer_field_values": [
"Men's Clothing"
]
},
{
"influencer_field_name": "geoip.country_iso_code",
"influencer_field_values": [
"TR",
"US"
]
}
],
"anomaly_score_explanation": {
"single_bucket_impact": 2,
"lower_confidence_bound": 0,
"typical_value": 0,
"upper_confidence_bound": 0
},
"category.keyword": [
"Men's Clothing"
],
"geoip.country_iso_code": [
"TR",
"US"
]
},
"rowId": "1726503845974_11",
"jobId": "ecom_dect_01",
"detectorIndex": 1,
"severity": 15.93562008623777,
"entityName": "category.keyword",
"entityValue": "Men's Clothing",
"influencers": [
{
"category.keyword": "Men's Clothing"
},
{
"geoip.country_iso_code": "TR"
},
{
"geoip.country_iso_code": "US"
}
],
"actual": [
2
],
"actualSort": 2,
"typical": [
1
],
"typicalSort": 1,
"metricDescriptionSort": 2,
"detector": "distinct_count(total_unique_products) partitionfield=\"category.keyword\"",
"isTimeSeriesViewRecord": true,
"isGeoRecord": false
}
],
"jobIds": [
"ecommerce-multiple-detectors"
],
"interval": "day",
"examplesByJobId": {
"ecommerce-multiple-detectors": {}
},
"showViewSeriesLink": true
}
}

View file

@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { cloneDeep } from 'lodash';
import type { MlAnomaliesTableRecord } from '@kbn/ml-anomaly-utils';
import { getTableItemClosestToTimestamp } from './anomalies_table_utils';
import mockAnomaliesTableData from '../__mocks__/mock_anomalies_table_data.json';
import mockAnomaliesTableDataMultipleDetectors from '../__mocks__/mock_anomalies_table_data_multiple_detectors.json';
describe('getTableItemClosestToTimestamp without entities filter', () => {
const anomalies: MlAnomaliesTableRecord[] = mockAnomaliesTableData.default.anomalies;
anomalies.push(cloneDeep(anomalies[0]));
anomalies[0].source.timestamp = 1000;
anomalies[1].source.timestamp = 2000;
anomalies[2].source.timestamp = 3000;
it('should return the first item if it is the closest', () => {
const anomalyTime = 1400;
const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime);
expect(closestItem && closestItem.source.timestamp).toBe(1000);
});
it('should return the last item if it is the closest', () => {
const anomalyTime = 5000;
const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime);
expect(closestItem && closestItem.source.timestamp).toBe(3000);
});
it('should return the second item if it is the closest', () => {
const anomalyTime = 2600;
const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime);
expect(closestItem && closestItem.source.timestamp).toBe(3000);
});
it('should handle an empty anomalies array', () => {
const anomalyTime = 2000;
const closestItem = getTableItemClosestToTimestamp([], anomalyTime);
expect(closestItem).toBeUndefined();
});
});
// These tests test for the case when there's multiple anomalies with the same
// timestamp but different entity values.
describe('getTableItemClosestToTimestamp with entities filter', () => {
const anomalies: MlAnomaliesTableRecord[] =
mockAnomaliesTableDataMultipleDetectors.default.anomalies;
it("should return the closest item matching the filter for Men's Clothing", () => {
const anomalyTime = 1725862500000;
const entityFields = [{ fieldName: 'category.keyword', fieldValue: "Men's Clothing" }];
const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime, entityFields);
expect(closestItem).toBeDefined();
// This is just to satisfy TypeScript.
if (!closestItem) throw new Error('closestItem is undefined');
expect(closestItem.source.timestamp).toBe(1725862500000);
expect(closestItem.entityName).toBe('category.keyword');
expect(closestItem.entityValue).toBe("Men's Clothing");
});
it("should return the closest item matching the filter for Women's Clothing", () => {
const anomalyTime = 1725862500000;
const entityFields = [{ fieldName: 'category.keyword', fieldValue: "Women's Clothing" }];
const closestItem = getTableItemClosestToTimestamp(anomalies, anomalyTime, entityFields);
expect(closestItem).toBeDefined();
// This is just to satisfy TypeScript.
if (!closestItem) throw new Error('closestItem is undefined');
expect(closestItem.source.timestamp).toBe(1725862500000);
expect(closestItem.entityName).toBe('category.keyword');
expect(closestItem.entityValue).toBe("Women's Clothing");
});
});

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { MlAnomaliesTableRecord, MlEntityField } from '@kbn/ml-anomaly-utils';
// The table items could be aggregated, so we have to find the item
// that has the closest timestamp to the selected anomaly from the chart.
export function getTableItemClosestToTimestamp(
anomalies: MlAnomaliesTableRecord[],
anomalyTime: number,
entityFields?: MlEntityField[]
) {
const filteredAnomalies = entityFields
? anomalies.filter((anomaly) => {
const currentEntity = {
entityName: anomaly.entityName,
entityValue: anomaly.entityValue,
};
return entityFields.some(
(field) =>
field.fieldName === currentEntity.entityName &&
field.fieldValue === currentEntity.entityValue
);
})
: anomalies;
return filteredAnomalies.reduce<MlAnomaliesTableRecord | undefined>(
(closestItem, currentItem) => {
// If the closest item is not defined, return the current item.
// This is the case when we start the reducer. For the case of an empty
// array the reducer will not be called and the value will stay undefined.
if (!closestItem) return currentItem;
const closestItemDelta = Math.abs(anomalyTime - closestItem.source.timestamp);
const currentItemDelta = Math.abs(anomalyTime - currentItem.source.timestamp);
return currentItemDelta < closestItemDelta ? currentItem : closestItem;
},
undefined
);
}

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import mockAnomaliesTableData from '../../explorer/__mocks__/mock_anomalies_table_data.json';
import mockAnomaliesTableData from '../../../../common/__mocks__/mock_anomalies_table_data.json';
import { getColumns } from './anomalies_table_columns';
jest.mock('../../capabilities/check_capabilities', () => ({

View file

@ -622,6 +622,7 @@ export const Explorer: FC<ExplorerUIProps> = ({
{...{
...chartsData,
severity,
tableData,
timefilter,
mlLocator,
timeBuckets,

View file

@ -19,6 +19,7 @@ import type { TableSeverity } from '../../components/controls/select_severity/se
import { SelectSeverityUI } from '../../components/controls/select_severity/select_severity';
import type { ExplorerChartsData } from './explorer_charts_container_service';
import type { MlLocator } from '../../../../common/types/locator';
import type { AnomaliesTableData } from '../explorer_utils';
interface ExplorerAnomaliesContainerProps {
id: string;
@ -27,6 +28,7 @@ interface ExplorerAnomaliesContainerProps {
severity: TableSeverity;
setSeverity: (severity: TableSeverity) => void;
mlLocator: MlLocator;
tableData: AnomaliesTableData;
timeBuckets: TimeBuckets;
timefilter: TimefilterContract;
onSelectEntity: (
@ -54,6 +56,7 @@ export const ExplorerAnomaliesContainer: FC<ExplorerAnomaliesContainerProps> = (
severity,
setSeverity,
mlLocator,
tableData,
timeBuckets,
timefilter,
onSelectEntity,
@ -89,6 +92,7 @@ export const ExplorerAnomaliesContainer: FC<ExplorerAnomaliesContainerProps> = (
...chartsData,
severity: severity.val,
mlLocator,
tableData,
timeBuckets,
timefilter,
timeRange,

View file

@ -16,6 +16,8 @@ import React from 'react';
import d3 from 'd3';
import moment from 'moment';
import { EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
getFormattedSeverityScore,
@ -25,6 +27,10 @@ import {
import { formatHumanReadableDateTime } from '@kbn/ml-date-utils';
import { context } from '@kbn/kibana-react-plugin/public';
import { getTableItemClosestToTimestamp } from '../../../../common/util/anomalies_table_utils';
import { LinksMenuUI } from '../../components/anomalies_table/links_menu';
import { RuleEditorFlyout } from '../../components/rule_editor';
import { formatValue } from '../../formatters/format_value';
import {
getChartType,
@ -42,6 +48,7 @@ import { filter } from 'rxjs';
import { drawCursor } from './utils/draw_anomaly_explorer_charts_cursor';
import { SCHEDULE_EVENT_MARKER_ENTITY } from '../../../../common/constants/charts';
const popoverMenuOffset = 0;
const CONTENT_WRAPPER_HEIGHT = 215;
const SCHEDULED_EVENT_MARKER_HEIGHT = 5;
@ -58,6 +65,7 @@ export class ExplorerChartDistribution extends React.Component {
static propTypes = {
seriesConfig: PropTypes.object,
severity: PropTypes.number,
tableData: PropTypes.object,
tooltipService: PropTypes.object.isRequired,
cursor$: PropTypes.object,
};
@ -66,7 +74,9 @@ export class ExplorerChartDistribution extends React.Component {
super(props);
this.chartScales = undefined;
this.cursorStateSubscription = undefined;
this.state = { popoverData: null, popoverCoords: [0, 0], showRuleEditorFlyout: () => {} };
}
componentDidMount() {
this.renderChart();
this.cursorStateSubscription = this.props.cursor$
@ -447,6 +457,8 @@ export class ExplorerChartDistribution extends React.Component {
dots.exit().remove();
}
const that = this;
function drawRareChartHighlightedSpan() {
if (showSelectedInterval === false) return;
// Draws a rectangle which highlights the time span that has been selected for view.
@ -484,6 +496,11 @@ export class ExplorerChartDistribution extends React.Component {
.enter()
.append('circle')
.attr('r', LINE_CHART_ANOMALY_RADIUS)
.on('click', function (d) {
d3.event.preventDefault();
if (d.anomalyScore === undefined) return;
showAnomalyPopover(d, this);
})
// Don't use an arrow function since we need access to `this`.
.on('mouseover', function (d) {
showLineChartTooltip(d, this);
@ -530,6 +547,35 @@ export class ExplorerChartDistribution extends React.Component {
);
}
function showAnomalyPopover(marker, circle) {
const anomalyTime = marker.date;
const tableItem = getTableItemClosestToTimestamp(
that.props.tableData.anomalies,
anomalyTime,
that.props.seriesConfig.entityFields
);
if (tableItem) {
// Overwrite the timestamp of the possibly aggregated table item with the
// timestamp of the anomaly clicked in the chart so we're able to pick
// the right baseline and deviation time ranges for Log Rate Analysis.
tableItem.source.timestamp = anomalyTime;
// Calculate the relative coordinates of the clicked anomaly marker
// so we're able to position the popover actions menu above it.
const dotRect = circle.getBoundingClientRect();
const rootRect = that.rootNode.getBoundingClientRect();
const x = Math.round(dotRect.x + dotRect.width / 2 - rootRect.x);
const y = Math.round(dotRect.y + dotRect.height / 2 - rootRect.y) - popoverMenuOffset;
// Hide any active tooltip
that.props.tooltipService.hide();
// Set the popover state to enable the actions menu
that.setState({ popoverData: tableItem, popoverCoords: [x, y] });
}
}
function showLineChartTooltip(marker, circle) {
// Show the time and metric values in the tooltip.
// Uses date, value, upper, lower and anomalyScore (optional) marker properties.
@ -666,6 +712,22 @@ export class ExplorerChartDistribution extends React.Component {
this.rootNode = componentNode;
}
closePopover() {
this.setState({ popoverData: null, popoverCoords: [0, 0] });
}
setShowRuleEditorFlyoutFunction = (func) => {
this.setState({
showRuleEditorFlyout: func,
});
};
unsetShowRuleEditorFlyoutFunction = () => {
this.setState({
showRuleEditorFlyout: () => {},
});
};
render() {
const { seriesConfig } = this.props;
@ -678,10 +740,47 @@ export class ExplorerChartDistribution extends React.Component {
const isLoading = seriesConfig.loading;
return (
<div className="ml-explorer-chart" ref={this.setRef.bind(this)}>
{isLoading && <LoadingIndicator height={CONTENT_WRAPPER_HEIGHT} />}
{!isLoading && <div className="content-wrapper" />}
</div>
<>
<RuleEditorFlyout
setShowFunction={this.setShowRuleEditorFlyoutFunction}
unsetShowFunction={this.unsetShowRuleEditorFlyoutFunction}
/>
{this.state.popoverData !== null && (
<div
style={{
position: 'absolute',
marginLeft: this.state.popoverCoords[0],
marginTop: this.state.popoverCoords[1],
}}
>
<EuiPopover
isOpen={true}
closePopover={() => this.closePopover()}
panelPaddingSize="none"
anchorPosition="upLeft"
>
<LinksMenuUI
anomaly={this.state.popoverData}
bounds={{
min: moment(seriesConfig.plotEarliest),
max: moment(seriesConfig.plotLatest),
}}
showMapsLink={false}
showViewSeriesLink={true}
isAggregatedData={this.props.tableData.interval !== 'second'}
interval={this.props.tableData.interval}
showRuleEditorFlyout={this.state.showRuleEditorFlyout}
onItemClick={() => this.closePopover()}
sourceIndicesWithGeoFields={this.props.sourceIndicesWithGeoFields}
/>
</EuiPopover>
</div>
)}
<div className="ml-explorer-chart" ref={this.setRef.bind(this)}>
{isLoading && <LoadingIndicator height={CONTENT_WRAPPER_HEIGHT} />}
{!isLoading && <div className="content-wrapper" />}
</div>
</>
);
}
}

View file

@ -16,6 +16,8 @@ import React from 'react';
import d3 from 'd3';
import moment from 'moment';
import { EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
getFormattedSeverityScore,
@ -25,6 +27,10 @@ import {
import { formatHumanReadableDateTime } from '@kbn/ml-date-utils';
import { context } from '@kbn/kibana-react-plugin/public';
import { getTableItemClosestToTimestamp } from '../../../../common/util/anomalies_table_utils';
import { LinksMenuUI } from '../../components/anomalies_table/links_menu';
import { RuleEditorFlyout } from '../../components/rule_editor';
import { formatValue } from '../../formatters/format_value';
import {
LINE_CHART_ANOMALY_RADIUS,
@ -43,6 +49,7 @@ import { CHART_HEIGHT, TRANSPARENT_BACKGROUND } from './constants';
import { filter } from 'rxjs';
import { drawCursor } from './utils/draw_anomaly_explorer_charts_cursor';
const popoverMenuOffset = 0;
const CONTENT_WRAPPER_HEIGHT = 215;
const CONTENT_WRAPPER_CLASS = 'ml-explorer-chart-content-wrapper';
@ -52,6 +59,7 @@ export class ExplorerChartSingleMetric extends React.Component {
tooManyBuckets: PropTypes.bool,
seriesConfig: PropTypes.object,
severity: PropTypes.number.isRequired,
tableData: PropTypes.object,
tooltipService: PropTypes.object.isRequired,
timeBuckets: PropTypes.object.isRequired,
onPointerUpdate: PropTypes.func.isRequired,
@ -63,7 +71,9 @@ export class ExplorerChartSingleMetric extends React.Component {
constructor(props) {
super(props);
this.chartScales = undefined;
this.state = { popoverData: null, popoverCoords: [0, 0], showRuleEditorFlyout: () => {} };
}
componentDidMount() {
this.renderChart();
@ -351,6 +361,8 @@ export class ExplorerChartSingleMetric extends React.Component {
.attr('d', lineChartValuesLine(data));
}
const that = this;
function drawLineChartMarkers(data) {
// Render circle markers for the points.
// These are used for displaying tooltips on mouseover.
@ -375,9 +387,17 @@ export class ExplorerChartSingleMetric extends React.Component {
.enter()
.append('circle')
.attr('r', LINE_CHART_ANOMALY_RADIUS)
.on('click', function (d) {
d3.event.preventDefault();
if (d.anomalyScore === undefined) return;
showAnomalyPopover(d, this);
})
// Don't use an arrow function since we need access to `this`.
.on('mouseover', function (d) {
showLineChartTooltip(d, this);
// Show the tooltip only if the actions menu isn't active
if (that.state.popoverData === null) {
showLineChartTooltip(d, this);
}
})
.on('mouseout', () => tooltipService.hide());
@ -418,6 +438,11 @@ export class ExplorerChartSingleMetric extends React.Component {
'class',
(d) => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`
)
.on('click', function (d) {
d3.event.preventDefault();
if (d.anomalyScore === undefined) return;
showAnomalyPopover(d, this);
})
// Don't use an arrow function since we need access to `this`.
.on('mouseover', function (d) {
showLineChartTooltip(d, this);
@ -448,6 +473,35 @@ export class ExplorerChartSingleMetric extends React.Component {
.attr('y', (d) => lineChartYScale(d.value) - SCHEDULED_EVENT_SYMBOL_HEIGHT / 2);
}
function showAnomalyPopover(marker, circle) {
const anomalyTime = marker.date;
const tableItem = getTableItemClosestToTimestamp(
that.props.tableData.anomalies,
anomalyTime,
that.props.seriesConfig.entityFields
);
if (tableItem) {
// Overwrite the timestamp of the possibly aggregated table item with the
// timestamp of the anomaly clicked in the chart so we're able to pick
// the right baseline and deviation time ranges for Log Rate Analysis.
tableItem.source.timestamp = anomalyTime;
// Calculate the relative coordinates of the clicked anomaly marker
// so we're able to position the popover actions menu above it.
const dotRect = circle.getBoundingClientRect();
const rootRect = that.rootNode.getBoundingClientRect();
const x = Math.round(dotRect.x + dotRect.width / 2 - rootRect.x);
const y = Math.round(dotRect.y + dotRect.height / 2 - rootRect.y) - popoverMenuOffset;
// Hide any active tooltip
that.props.tooltipService.hide();
// Set the popover state to enable the actions menu
that.setState({ popoverData: tableItem, popoverCoords: [x, y] });
}
}
function showLineChartTooltip(marker, circle) {
// Show the time and metric values in the tooltip.
// Uses date, value, upper, lower and anomalyScore (optional) marker properties.
@ -589,6 +643,22 @@ export class ExplorerChartSingleMetric extends React.Component {
this.rootNode = componentNode;
}
closePopover() {
this.setState({ popoverData: null, popoverCoords: [0, 0] });
}
setShowRuleEditorFlyoutFunction = (func) => {
this.setState({
showRuleEditorFlyout: func,
});
};
unsetShowRuleEditorFlyoutFunction = () => {
this.setState({
showRuleEditorFlyout: () => {},
});
};
render() {
const { seriesConfig } = this.props;
@ -601,10 +671,47 @@ export class ExplorerChartSingleMetric extends React.Component {
const isLoading = seriesConfig.loading;
return (
<div className="ml-explorer-chart" ref={this.setRef.bind(this)}>
{isLoading && <LoadingIndicator height={CONTENT_WRAPPER_HEIGHT} />}
{!isLoading && <div className={CONTENT_WRAPPER_CLASS} />}
</div>
<>
<RuleEditorFlyout
setShowFunction={this.setShowRuleEditorFlyoutFunction}
unsetShowFunction={this.unsetShowRuleEditorFlyoutFunction}
/>
{this.state.popoverData !== null && (
<div
style={{
position: 'absolute',
marginLeft: this.state.popoverCoords[0],
marginTop: this.state.popoverCoords[1],
}}
>
<EuiPopover
isOpen={true}
closePopover={() => this.closePopover()}
panelPaddingSize="none"
anchorPosition="upLeft"
>
<LinksMenuUI
anomaly={this.state.popoverData}
bounds={{
min: moment(seriesConfig.plotEarliest),
max: moment(seriesConfig.plotLatest),
}}
showMapsLink={false}
showViewSeriesLink={true}
isAggregatedData={this.props.tableData.interval !== 'second'}
interval={this.props.tableData.interval}
showRuleEditorFlyout={this.state.showRuleEditorFlyout}
onItemClick={() => this.closePopover()}
sourceIndicesWithGeoFields={this.props.sourceIndicesWithGeoFields}
/>
</EuiPopover>
</div>
)}
<div className="ml-explorer-chart" ref={this.setRef.bind(this)}>
{isLoading && <LoadingIndicator height={CONTENT_WRAPPER_HEIGHT} />}
{!isLoading && <div className={CONTENT_WRAPPER_CLASS} />}
</div>
</>
);
}
}

View file

@ -91,6 +91,7 @@ function ExplorerChartContainer({
tooManyBuckets,
wrapLabel,
mlLocator,
tableData,
timeBuckets,
timefilter,
timeRange,
@ -331,6 +332,7 @@ function ExplorerChartContainer({
{(tooltipService) => (
<ExplorerChartDistribution
id={id}
tableData={tableData}
timeBuckets={timeBuckets}
tooManyBuckets={tooManyBuckets}
seriesConfig={series}
@ -351,6 +353,7 @@ function ExplorerChartContainer({
{(tooltipService) => (
<ExplorerChartSingleMetric
id={id}
tableData={tableData}
timeBuckets={timeBuckets}
tooManyBuckets={tooManyBuckets}
seriesConfig={series}
@ -380,6 +383,7 @@ export const ExplorerChartsContainerUI = ({
kibana,
errorMessages,
mlLocator,
tableData,
timeBuckets,
timefilter,
timeRange,
@ -444,6 +448,7 @@ export const ExplorerChartsContainerUI = ({
tooManyBuckets={tooManyBuckets}
wrapLabel={wrapLabel}
mlLocator={mlLocator}
tableData={tableData}
timeBuckets={timeBuckets}
timefilter={timefilter}
timeRange={timeRange}

View file

@ -52,6 +52,7 @@ import {
import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import type { MlResultsService } from '../services/results_service';
import type { Annotations, AnnotationsTable } from '../../../common/types/annotations';
import { useMlKibana } from '../contexts/kibana';
import type { MlApi } from '../services/ml_api_service';
export interface ExplorerJob {
@ -250,6 +251,15 @@ export function getInfluencers(mlJobService: MlJobService, selectedJobs: any[]):
return influencers;
}
export function useDateFormatTz(): string {
const { services } = useMlKibana();
const { uiSettings } = services;
// Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table.
const tzConfig = uiSettings.get('dateFormat:tz');
const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess();
return dateFormatTz;
}
export function getDateFormatTz(uiSettings: IUiSettingsClient): string {
// Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table.
const tzConfig = uiSettings.get('dateFormat:tz');
@ -475,7 +485,7 @@ export async function loadAnomaliesTableData(
fieldName: string,
tableInterval: string,
tableSeverity: number,
influencersFilterQuery: InfluencersFilterQuery
influencersFilterQuery?: InfluencersFilterQuery
): Promise<AnomaliesTableData> {
const jobIds = getSelectionJobIds(selectedCells, selectedJobs);
const influencers = getSelectionInfluencers(selectedCells, fieldName);

View file

@ -24,6 +24,8 @@ import { getFormattedSeverityScore, getSeverityWithLow } from '@kbn/ml-anomaly-u
import { formatHumanReadableDateTimeSeconds } from '@kbn/ml-date-utils';
import { context } from '@kbn/kibana-react-plugin/public';
import { getTableItemClosestToTimestamp } from '../../../../../common/util/anomalies_table_utils';
import { formatValue } from '../../../formatters/format_value';
import {
LINE_CHART_ANOMALY_RADIUS,
@ -1582,13 +1584,7 @@ class TimeseriesChartIntl extends Component {
showAnomalyPopover(marker, circle) {
const anomalyTime = marker.date.getTime();
// The table items could be aggregated, so we have to find the item
// that has the closest timestamp to the selected anomaly from the chart.
const tableItem = this.props.tableData.anomalies.reduce((closestItem, currentItem) => {
const closestItemDelta = Math.abs(anomalyTime - closestItem.source.timestamp);
const currentItemDelta = Math.abs(anomalyTime - currentItem.source.timestamp);
return currentItemDelta < closestItemDelta ? currentItem : closestItem;
}, this.props.tableData.anomalies[0]);
const tableItem = getTableItemClosestToTimestamp(this.props.tableData.anomalies, anomalyTime);
if (tableItem) {
// Overwrite the timestamp of the possibly aggregated table item with the

View file

@ -65,6 +65,7 @@ import { TimeseriesExplorerCheckbox } from './timeseriesexplorer_checkbox';
import { timeBucketsServiceFactory } from '../../util/time_buckets_service';
import { timeSeriesExplorerServiceFactory } from '../../util/time_series_explorer_service';
import { getTimeseriesexplorerDefaultState } from '../timeseriesexplorer_utils';
import { mlJobServiceFactory } from '../../services/job_service';
import { forecastServiceFactory } from '../../services/forecast_service';
// Used to indicate the chart is being plotted across
@ -736,6 +737,10 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component {
]);
}
// Populate mlJobService to work with LinksMenuUI.
this.mlJobService = mlJobServiceFactory(undefined, this.context.services.mlServices.mlApi);
await this.mlJobService.loadJobsWrapper();
this.componentDidUpdate();
}

View file

@ -7,6 +7,8 @@
import type { FC } from 'react';
import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react';
import moment from 'moment-timezone';
import useMountedState from 'react-use/lib/useMountedState';
import { EuiCallOut, EuiLoadingChart, EuiResizeObserver, EuiText } from '@elastic/eui';
import type { Observable } from 'rxjs';
import { FormattedMessage } from '@kbn/i18n-react';
@ -27,12 +29,15 @@ import type {
AnomalyChartsAttachmentApi,
} from '..';
import type { AnomaliesTableData, ExplorerJob } from '../../application/explorer/explorer_utils';
import { ExplorerAnomaliesContainer } from '../../application/explorer/explorer_charts/explorer_anomalies_container';
import { ML_APP_LOCATOR } from '../../../common/constants/locator';
import { optionValueToThreshold } from '../../application/components/controls/select_severity/select_severity';
import { EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER } from '../../ui_actions/triggers';
import type { MlLocatorParams } from '../../../common/types/locator';
import { useAnomalyChartsData } from './use_anomaly_charts_data';
import { useDateFormatTz, loadAnomaliesTableData } from '../../application/explorer/explorer_utils';
import { useMlJobService } from '../../application/services/job_service';
const RESIZE_THROTTLE_TIME_MS = 500;
@ -58,6 +63,16 @@ const AnomalyChartsContainer: FC<AnomalyChartsContainerProps> = ({
onLoading,
api,
}) => {
const isMounted = useMountedState();
const [tableData, setTableData] = useState<AnomaliesTableData>({
anomalies: [],
examplesByJobId: [''],
interval: 0,
jobIds: [],
showViewSeriesLink: false,
});
const [chartWidth, setChartWidth] = useState<number>(0);
const [severity, setSeverity] = useState(
optionValueToThreshold(
@ -65,8 +80,14 @@ const AnomalyChartsContainer: FC<AnomalyChartsContainerProps> = ({
)
);
const [selectedEntities, setSelectedEntities] = useState<MlEntityField[] | undefined>();
const [{ uiSettings }, { data: dataServices, share, uiActions, charts: chartsService }] =
services;
const [
{ uiSettings },
{ data: dataServices, share, uiActions, charts: chartsService },
{ mlApi },
] = services;
const mlJobService = useMlJobService();
const { timefilter } = dataServices.query.timefilter;
const timeRange = useObservable(timeRange$);
@ -108,6 +129,55 @@ const AnomalyChartsContainer: FC<AnomalyChartsContainerProps> = ({
error,
} = useAnomalyChartsData(api, services, chartWidth, severity.val, renderCallbacks);
const dateFormatTz = useDateFormatTz();
useEffect(() => {
// async IFEE
(async () => {
if (chartsData === undefined) {
return;
}
try {
await mlJobService.loadJobsWrapper();
const explorerJobs: ExplorerJob[] =
chartsData.seriesToPlot.map(({ jobId, bucketSpanSeconds }) => {
return {
id: jobId,
selected: true,
bucketSpanSeconds,
modelPlotEnabled: false,
};
}) ?? [];
const timeRangeBounds = {
min: moment(chartsData.seriesToPlot[0].plotEarliest),
max: moment(chartsData.seriesToPlot[0].plotLatest),
};
const newTableData = await loadAnomaliesTableData(
mlApi,
mlJobService,
undefined,
explorerJobs,
dateFormatTz,
timeRangeBounds,
'job ID',
'auto',
0
);
if (isMounted()) {
setTableData(newTableData);
}
} catch (err) {
console.log(err); // eslint-disable-line no-console
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chartsData]);
// Holds the container height for previously fetched data
const containerHeightRef = useRef<number>();
@ -207,6 +277,7 @@ const AnomalyChartsContainer: FC<AnomalyChartsContainerProps> = ({
severity={severity}
setSeverity={setSeverity}
mlLocator={mlLocator}
tableData={tableData}
timeBuckets={timeBuckets}
timefilter={timefilter}
onSelectEntity={addEntityFieldFilter}

View file

@ -22,6 +22,7 @@ export const getAnomalyChartsServiceDependencies = async (
{ mlApiProvider },
{ mlJobServiceFactory },
{ mlResultsServiceProvider },
{ MlCapabilitiesService },
] = await Promise.all([
await import('../../application/services/anomaly_detector_service'),
await import('../../application/services/field_format_service_factory'),
@ -29,12 +30,14 @@ export const getAnomalyChartsServiceDependencies = async (
await import('../../application/services/ml_api_service'),
await import('../../application/services/job_service'),
await import('../../application/services/results_service'),
await import('../../application/capabilities/check_capabilities'),
]);
const httpService = new HttpService(coreStart.http);
const anomalyDetectorService = new AnomalyDetectorService(httpService);
const mlApi = mlApiProvider(httpService);
const mlJobService = mlJobServiceFactory(mlApi);
const mlResultsService = mlResultsServiceProvider(mlApi);
const mlCapabilities = new MlCapabilitiesService(mlApi);
const anomalyExplorerService = new AnomalyExplorerChartsService(
pluginsStart.data.query.timefilter.timefilter,
mlApi,
@ -59,8 +62,10 @@ export const getAnomalyChartsServiceDependencies = async (
{
anomalyDetectorService,
anomalyExplorerService,
mlCapabilities,
mlFieldFormatService,
mlResultsService,
mlApi,
},
];
return anomalyChartsEmbeddableServices;

View file

@ -26,24 +26,24 @@ export const getMlServices = async (
{ indexServiceFactory },
{ timeSeriesExplorerServiceFactory },
{ mlApiProvider },
{ mlJobServiceFactory },
{ mlResultsServiceProvider },
{ MlCapabilitiesService },
{ timeSeriesSearchServiceFactory },
{ toastNotificationServiceProvider },
{ mlJobServiceFactory },
] = await Promise.all([
await import('../../application/services/anomaly_detector_service'),
await import('../../application/services/field_format_service_factory'),
await import('../../application/util/index_service'),
await import('../../application/util/time_series_explorer_service'),
await import('../../application/services/ml_api_service'),
await import('../../application/services/job_service'),
await import('../../application/services/results_service'),
await import('../../application/capabilities/check_capabilities'),
await import(
'../../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'
),
await import('../../application/services/toast_notification_service'),
await import('../../application/services/job_service'),
]);
const httpService = new HttpService(coreStart.http);
@ -81,7 +81,6 @@ export const getMlServices = async (
mlApi,
mlCapabilities,
mlFieldFormatService,
mlJobService,
mlResultsService,
mlTimeSeriesSearchService,
mlTimeSeriesExplorerService,

View file

@ -32,7 +32,6 @@ import type { AnomalyDetectorService } from '../application/services/anomaly_det
import type { AnomalyExplorerChartsService } from '../application/services/anomaly_explorer_charts_service';
import type { AnomalyTimelineService } from '../application/services/anomaly_timeline_service';
import type { MlFieldFormatService } from '../application/services/field_format_service';
import type { MlJobService } from '../application/services/job_service';
import type { MlApi } from '../application/services/ml_api_service';
import type { MlResultsService } from '../application/services/results_service';
import type { MlTimeSeriesSearchService } from '../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service';
@ -233,9 +232,10 @@ export interface SingleMetricViewerComponentApi {
export interface AnomalyChartsServices {
anomalyDetectorService: AnomalyDetectorService;
anomalyExplorerService: AnomalyExplorerChartsService;
mlCapabilities: MlCapabilitiesService;
mlFieldFormatService: MlFieldFormatService;
mlResultsService: MlResultsService;
mlApi?: MlApi;
mlApi: MlApi;
}
export interface SingleMetricViewerServices {
@ -244,7 +244,6 @@ export interface SingleMetricViewerServices {
mlApi: MlApi;
mlCapabilities: MlCapabilitiesService;
mlFieldFormatService: MlFieldFormatService;
mlJobService: MlJobService;
mlResultsService: MlResultsService;
mlTimeSeriesSearchService?: MlTimeSeriesSearchService;
mlTimeSeriesExplorerService?: TimeSeriesExplorerService;

View file

@ -11,6 +11,7 @@
"__mocks__/**/*",
"../../../typings/**/*",
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
"common/**/*.json",
"public/**/*.json",
"server/**/*.json"
],
@ -19,7 +20,9 @@
],
"kbn_references": [
"@kbn/core",
{ "path": "../../../src/setup_node_env/tsconfig.json" },
{
"path": "../../../src/setup_node_env/tsconfig.json"
},
// add references to other TypeScript projects the plugin depends on
"@kbn/ace",
"@kbn/actions-plugin",