diff --git a/x-pack/packages/ml/anomaly_utils/types.ts b/x-pack/packages/ml/anomaly_utils/types.ts index 2d7082848b48f..457330fc4d7e0 100644 --- a/x-pack/packages/ml/anomaly_utils/types.ts +++ b/x-pack/packages/ml/anomaly_utils/types.ts @@ -343,7 +343,7 @@ export interface MlAnomaliesTableRecord { /** * Returns true if the job has the model plot enabled */ - modelPlotEnabled: boolean; + modelPlotEnabled?: boolean; } /** diff --git a/x-pack/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json b/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data.json similarity index 97% rename from x-pack/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json rename to x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data.json index 2827e87a05d1e..12cd80644efcf 100644 --- a/x-pack/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json +++ b/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data.json @@ -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" + } ] } ], diff --git a/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data_multiple_detectors.json b/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data_multiple_detectors.json new file mode 100644 index 0000000000000..bd886f4745af3 --- /dev/null +++ b/x-pack/plugins/ml/common/__mocks__/mock_anomalies_table_data_multiple_detectors.json @@ -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 + } +} diff --git a/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts b/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts new file mode 100644 index 0000000000000..e64c0daf983eb --- /dev/null +++ b/x-pack/plugins/ml/common/util/anomalies_table_utils.test.ts @@ -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"); + }); +}); diff --git a/x-pack/plugins/ml/common/util/anomalies_table_utils.ts b/x-pack/plugins/ml/common/util/anomalies_table_utils.ts new file mode 100644 index 0000000000000..1964ccfd85ca6 --- /dev/null +++ b/x-pack/plugins/ml/common/util/anomalies_table_utils.ts @@ -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( + (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 + ); +} diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index d8f00b92992c6..59b4fcb042186 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -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', () => ({ diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.tsx b/x-pack/plugins/ml/public/application/explorer/explorer.tsx index 2b6971077a086..ab503c11d7955 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer.tsx @@ -622,6 +622,7 @@ export const Explorer: FC = ({ {...{ ...chartsData, severity, + tableData, timefilter, mlLocator, timeBuckets, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx index e769537a19779..6198044753687 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_anomalies_container.tsx @@ -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 = ( severity, setSeverity, mlLocator, + tableData, timeBuckets, timefilter, onSelectEntity, @@ -89,6 +92,7 @@ export const ExplorerAnomaliesContainer: FC = ( ...chartsData, severity: severity.val, mlLocator, + tableData, timeBuckets, timefilter, timeRange, diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 79e8bd449876e..c7fcddbe4c515 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -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 ( -
- {isLoading && } - {!isLoading &&
} -
+ <> + + {this.state.popoverData !== null && ( +
+ this.closePopover()} + panelPaddingSize="none" + anchorPosition="upLeft" + > + this.closePopover()} + sourceIndicesWithGeoFields={this.props.sourceIndicesWithGeoFields} + /> + +
+ )} +
+ {isLoading && } + {!isLoading &&
} +
+ ); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 77118b376e97a..e358c381288d3 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -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 ( -
- {isLoading && } - {!isLoading &&
} -
+ <> + + {this.state.popoverData !== null && ( +
+ this.closePopover()} + panelPaddingSize="none" + anchorPosition="upLeft" + > + this.closePopover()} + sourceIndicesWithGeoFields={this.props.sourceIndicesWithGeoFields} + /> + +
+ )} +
+ {isLoading && } + {!isLoading &&
} +
+ ); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index f6dd68c15a675..e7efbed4f1ff7 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -91,6 +91,7 @@ function ExplorerChartContainer({ tooManyBuckets, wrapLabel, mlLocator, + tableData, timeBuckets, timefilter, timeRange, @@ -331,6 +332,7 @@ function ExplorerChartContainer({ {(tooltipService) => ( ( { const jobIds = getSelectionJobIds(selectedCells, selectedJobs); const influencers = getSelectionInfluencers(selectedCells, fieldName); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 20d94aaabc3bb..51abbf77dbeba 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -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 diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js index 3839c9831c86b..90b0e76167517 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js @@ -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(); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx index d31ee17ef0780..aefdac533f859 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_react_container.tsx @@ -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 = ({ onLoading, api, }) => { + const isMounted = useMountedState(); + + const [tableData, setTableData] = useState({ + anomalies: [], + examplesByJobId: [''], + interval: 0, + jobIds: [], + showViewSeriesLink: false, + }); + const [chartWidth, setChartWidth] = useState(0); const [severity, setSeverity] = useState( optionValueToThreshold( @@ -65,8 +80,14 @@ const AnomalyChartsContainer: FC = ({ ) ); const [selectedEntities, setSelectedEntities] = useState(); - 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 = ({ 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(); @@ -207,6 +277,7 @@ const AnomalyChartsContainer: FC = ({ severity={severity} setSeverity={setSeverity} mlLocator={mlLocator} + tableData={tableData} timeBuckets={timeBuckets} timefilter={timefilter} onSelectEntity={addEntityFieldFilter} diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts index 078db5b83f506..0a3d1b9ea0ca6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/get_anomaly_charts_services_dependencies.ts @@ -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; diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts index 62afd5e251b6a..de6867b213e18 100644 --- a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/get_services.ts @@ -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, diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index b7d7dea505f0b..1bcb0f9ae3579 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -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; diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 8353c023f1955..2f980378de923 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -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",