mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
a4fb640635
commit
e0e48ccfb1
19 changed files with 1465 additions and 40 deletions
|
@ -343,7 +343,7 @@ export interface MlAnomaliesTableRecord {
|
|||
/**
|
||||
* Returns true if the job has the model plot enabled
|
||||
*/
|
||||
modelPlotEnabled: boolean;
|
||||
modelPlotEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
|
@ -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
|
||||
}
|
||||
}
|
86
x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts
Normal file
86
x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts
Normal 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");
|
||||
});
|
||||
});
|
45
x-pack/plugins/ml/common/util/anomalies_table_utils.ts
Normal file
45
x-pack/plugins/ml/common/util/anomalies_table_utils.ts
Normal 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
|
||||
);
|
||||
}
|
|
@ -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', () => ({
|
||||
|
|
|
@ -622,6 +622,7 @@ export const Explorer: FC<ExplorerUIProps> = ({
|
|||
{...{
|
||||
...chartsData,
|
||||
severity,
|
||||
tableData,
|
||||
timefilter,
|
||||
mlLocator,
|
||||
timeBuckets,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue