From dc9775b04985b478b716b0cf17ba07a79d9fb37d Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jul 2019 13:59:12 +0100 Subject: [PATCH 01/15] [ML] New job wizards UI --- .../plugins/ml/common/constants/anomalies.ts | 22 ++ .../legacy/plugins/ml/common/types/fields.ts | 21 +- .../plugins/ml/common/util/anomaly_utils.d.ts | 11 + .../plugins/ml/common/util/anomaly_utils.js | 18 + .../ml/common/util/group_color_utils.d.ts | 6 + .../plugins/ml/common/util/job_utils.d.ts | 13 + .../plugins/ml/common/util/string_utils.d.ts | 8 + ....tsx => full_time_range_selector.test.tsx} | 0 .../full_time_range_selector.tsx | 35 ++ .../full_time_range_selector_service.ts | 27 ++ .../full_time_range_selector/index.tsx | 32 +- x-pack/legacy/plugins/ml/public/jobs/index.js | 1 + .../jobs/new_job/utils/new_job_utils.d.ts | 17 + .../wizard/steps/job_type/job_type.html | 96 +++++ .../common/chart_loader/chart_loader.ts | 137 +++++++ .../new_job_new/common/chart_loader/index.ts | 7 + .../common/chart_loader/searches.ts | 50 +++ .../jobs/new_job_new/common/chart_settings.ts | 4 +- .../public/jobs/new_job_new/common/index.ts | 1 + .../common/index_pattern_context.ts | 12 + .../new_job_new/common/job_creator/index.ts | 6 + .../common/job_creator/job_creator.ts | 65 +++- .../common/job_creator/job_creator_factory.ts | 36 ++ .../job_creator/multi_metric_job_creator.ts | 6 +- .../job_creator/population_job_creator.ts | 6 +- .../job_creator/single_metric_job_creator.ts | 24 +- .../common/job_creator/type_guards.ts | 28 ++ .../common/job_creator/util/constants.ts | 11 + .../common/job_runner/job_runner.ts | 45 +-- .../common/results_loader/index.ts | 7 + .../common/results_loader/results_loader.ts | 257 ++++++++++++++ .../common/results_loader/searches.ts | 152 ++++++++ .../ml/public/jobs/new_job_new/index.ts | 8 + .../charts/anomaly_chart/anomalies.tsx | 102 ++++++ .../charts/anomaly_chart/anomaly_chart.tsx | 62 ++++ .../components/charts/anomaly_chart/index.ts | 6 + .../components/charts/anomaly_chart/line.tsx | 37 ++ .../charts/anomaly_chart/model_bounds.tsx | 50 +++ .../charts/anomaly_chart/scatter.tsx | 45 +++ .../pages/components/charts/common/axes.tsx | 42 +++ .../components/charts/common/settings.ts | 48 +++ .../pages/components/charts/common/utils.ts | 40 +++ .../event_rate_chart/event_rate_chart.tsx | 42 +++ .../charts/event_rate_chart/index.ts | 7 + .../pages/components/job_creator_context.ts | 45 +++ .../job_details_step/groups_input.tsx | 93 +++++ .../components/job_details_step/index.ts | 7 + .../job_description_input.tsx | 36 ++ .../job_details_step/job_details.tsx | 76 ++++ .../job_details_step/job_id_input.tsx | 32 ++ .../components/agg_select/agg_select.tsx | 71 ++++ .../components/agg_select/index.ts | 7 + .../components/bucket_span/bucket_span.tsx | 26 ++ .../bucket_span/bucket_span_input.tsx | 36 ++ .../components/bucket_span/index.ts | 6 + .../detector_title/detector_title.tsx | 55 +++ .../components/detector_title/index.ts | 7 + .../components/influencers/index.ts | 6 + .../components/influencers/influencers.tsx | 56 +++ .../influencers/influencers_select.tsx | 60 ++++ .../components/job_progress/index.ts | 6 + .../components/job_progress/job_progress.tsx | 20 ++ .../components/multi_metric_view/index.ts | 7 + .../multi_metric_view/metric_selection.tsx | 258 ++++++++++++++ .../multi_metric_view/metric_selector.tsx | 41 +++ .../multi_metric_view/multi_metric_view.tsx | 37 ++ .../components/multi_metric_view/settings.tsx | 56 +++ .../components/population_view/index.ts | 7 + .../population_view/metric_selection.tsx | 336 ++++++++++++++++++ .../population_view/metric_selector.tsx | 41 +++ .../population_view/population_view.tsx | 37 ++ .../components/population_view/settings.tsx | 49 +++ .../components/single_metric_view/index.ts | 7 + .../single_metric_view/metric_selection.tsx | 160 +++++++++ .../single_metric_view/settings.tsx | 31 ++ .../single_metric_view/single_metric_view.tsx | 37 ++ .../components/split_cards/index.ts | 7 + .../components/split_cards/split_cards.tsx | 99 ++++++ .../components/split_field/by_field.tsx | 46 +++ .../components/split_field/description.tsx | 55 +++ .../components/split_field/index.ts | 7 + .../components/split_field/split_field.tsx | 48 +++ .../split_field/split_field_select.tsx | 55 +++ .../components/pick_fields_step/index.ts | 7 + .../pick_fields_step/pick_fields.tsx | 48 +++ .../pages/components/step_types.ts | 17 + .../components/summary_step}/index.ts | 2 +- .../pages/components/summary_step/summary.tsx | 88 +++++ .../pages/components/time_range_step/index.ts | 7 + .../components/time_range_step/time_range.tsx | 119 +++++++ .../new_job_new/pages/new_job/directive.tsx | 77 ++++ .../jobs/new_job_new/pages/new_job/page.tsx | 84 +++++ .../jobs/new_job_new/pages/new_job/route.ts | 64 ++++ .../jobs/new_job_new/pages/new_job/wizard.tsx | 131 +++++++ .../ml/public/services/job_service.d.ts | 2 + .../plugins/ml/public/services/job_service.js | 14 + .../public/services/ml_api_service/index.d.ts | 28 ++ .../ml/public/services/ml_api_service/jobs.js | 73 ++++ .../services/new_job_capabilities_service.ts | 7 + .../ml/public/services/results_service.d.ts | 50 +++ .../ml/public/util/ml_time_buckets.d.ts | 23 ++ .../plugins/ml/public/util/string_utils.d.ts | 7 + .../ml/server/models/job_service/index.js | 2 + .../ml/server/models/job_service/jobs.js | 47 +++ .../models/job_service/new_job/charts.ts | 19 + .../models/job_service/new_job/index.ts | 7 + .../models/job_service/new_job/line_chart.ts | 218 ++++++++++++ .../job_service/new_job/population_chart.ts | 275 ++++++++++++++ .../plugins/ml/server/routes/job_service.js | 95 +++++ 109 files changed, 5144 insertions(+), 83 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/common/constants/anomalies.ts create mode 100644 x-pack/legacy/plugins/ml/common/util/anomaly_utils.d.ts create mode 100644 x-pack/legacy/plugins/ml/common/util/group_color_utils.d.ts create mode 100644 x-pack/legacy/plugins/ml/common/util/job_utils.d.ts create mode 100644 x-pack/legacy/plugins/ml/common/util/string_utils.d.ts rename x-pack/legacy/plugins/ml/public/components/full_time_range_selector/{index.test.tsx => full_time_range_selector.test.tsx} (100%) create mode 100644 x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/searches.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/index_pattern_context.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator_factory.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/type_guards.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/constants.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/searches.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/anomalies.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/anomaly_chart.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/line.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/model_bounds.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/scatter.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/axes.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/utils.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/event_rate_chart.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_description_input.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_details.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_id_input.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/agg_select.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/detector_title.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/job_progress/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/job_progress/job_progress.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selector.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/population_view.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/settings.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/settings.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field_select.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/pick_fields.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/step_types.ts rename x-pack/legacy/plugins/ml/public/jobs/new_job_new/{common/job_creator/util => pages/components/summary_step}/index.ts (85%) create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/summary.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/page.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx create mode 100644 x-pack/legacy/plugins/ml/public/services/results_service.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/util/string_utils.d.ts create mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts create mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts create mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts create mode 100644 x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts diff --git a/x-pack/legacy/plugins/ml/common/constants/anomalies.ts b/x-pack/legacy/plugins/ml/common/constants/anomalies.ts new file mode 100644 index 0000000000000..bbf3616c05880 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/constants/anomalies.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum ANOMALY_SEVERITY { + CRITICAL = 'critical', + MAJOR = 'major', + MINOR = 'minor', + WARNING = 'warning', + LOW = 'low', + UNKNOWN = 'unknown', +} + +export enum ANOMALY_THRESHOLD { + CRITICAL = 75, + MAJOR = 50, + MINOR = 25, + WARNING = 3, + LOW = 0, +} diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts index 43781a49c5cce..d9a4446588bfc 100644 --- a/x-pack/legacy/plugins/ml/common/types/fields.ts +++ b/x-pack/legacy/plugins/ml/common/types/fields.ts @@ -10,6 +10,7 @@ import { ML_JOB_AGGREGATION } from '../../common/constants/aggregation_types'; export type FieldId = string; export type AggId = ML_JOB_AGGREGATION; export type SplitField = Field | null; +export type DslName = string; export interface Field { id: FieldId; @@ -24,7 +25,7 @@ export interface Aggregation { id: AggId; title: string; kibanaName: string; - dslName: string; + dslName: DslName; type: string; mlModelPlotAgg: { min: string; @@ -38,3 +39,21 @@ export interface NewJobCaps { fields: Field[]; aggs: Aggregation[]; } + +export interface AggFieldPair { + agg: Aggregation; + field: Field; + by?: { + field: SplitField; + value: string | null; + }; +} + +export interface AggFieldNamePair { + agg: string; + field: string; + by?: { + field: string | null; + value: string | null; + }; +} diff --git a/x-pack/legacy/plugins/ml/common/util/anomaly_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.d.ts new file mode 100644 index 0000000000000..adeb6dc7dd5b9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.d.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ANOMALY_SEVERITY } from '../constants/anomalies'; + +export function getSeverity(normalizedScore: number): string; +export function getSeverityType(normalizedScore: number): ANOMALY_SEVERITY; +export function getSeverityColor(normalizedScore: number): string; diff --git a/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js index 371d1d176394a..b4ce49e5639d8 100644 --- a/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js @@ -15,6 +15,8 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule'; import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact'; +import { ANOMALY_SEVERITY } from '../constants/anomalies'; + // List of function descriptions for which actual values from record level results should be displayed. const DISPLAY_ACTUAL_FUNCTIONS = ['count', 'distinct_count', 'lat_long', 'mean', 'max', 'min', 'sum', @@ -71,6 +73,22 @@ export function getSeverity(normalizedScore) { } } +export function getSeverityType(normalizedScore) { + if (normalizedScore >= 75) { + return ANOMALY_SEVERITY.CRITICAL; + } else if (normalizedScore >= 50) { + return ANOMALY_SEVERITY.MAJOR; + } else if (normalizedScore >= 25) { + return ANOMALY_SEVERITY.MINOR; + } else if (normalizedScore >= 3) { + return ANOMALY_SEVERITY.WARNING; + } else if (normalizedScore >= 0) { + return ANOMALY_SEVERITY.LOW; + } else { + return ANOMALY_SEVERITY.UNKNOWN; + } +} + // Returns a severity label (one of critical, major, minor, warning, low or unknown) // for the supplied normalized anomaly score (a value between 0 and 100), where scores // less than 3 are assigned a severity of 'low'. diff --git a/x-pack/legacy/plugins/ml/common/util/group_color_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/group_color_utils.d.ts new file mode 100644 index 0000000000000..4a1a6ebb8fdf3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/group_color_utils.d.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export function tabColor(name: string): string; diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts new file mode 100644 index 0000000000000..832db922cad5f --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: number): number; + +export function isTimeSeriesViewJob(job: any): boolean; + +export const ML_MEDIAN_PERCENTS: number; + +export const ML_DATA_PREVIEW_COUNT: number; diff --git a/x-pack/legacy/plugins/ml/common/util/string_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/string_utils.d.ts new file mode 100644 index 0000000000000..f8dbc00643d07 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/string_utils.d.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function renderTemplate(str: string, data: string): string; +export function stringHash(str: string): string; diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.test.tsx b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.test.tsx rename to x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx new file mode 100644 index 0000000000000..0e39a0fbc4521 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { Query } from 'ui/embeddable'; +import { IndexPattern } from 'ui/index_patterns'; +import { EuiButton } from '@elastic/eui'; +import { setFullTimeRange } from './full_time_range_selector_service'; + +interface Props { + indexPattern: IndexPattern; + query: Query; + disabled: boolean; +} + +// Component for rendering a button which automatically sets the range of the time filter +// to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query. +export const FullTimeRangeSelector: React.SFC = ({ indexPattern, query, disabled }) => { + return ( + setFullTimeRange(indexPattern, query)}> + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts index 32606d2db425e..08a0fdd3ce784 100644 --- a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts @@ -11,8 +11,14 @@ import { IndexPattern } from 'ui/index_patterns'; import { toastNotifications } from 'ui/notify'; import { timefilter } from 'ui/timefilter'; import { Query } from 'src/legacy/core_plugins/data/public'; +import dateMath from '@elastic/datemath'; import { ml } from '../../services/ml_api_service'; +export interface TimeRange { + from: number; + to: number; +} + export function setFullTimeRange(indexPattern: IndexPattern, query: Query) { return ml .getTimeFieldRange({ @@ -34,3 +40,24 @@ export function setFullTimeRange(indexPattern: IndexPattern, query: Query) { ); }); } + +export function getTimeFilterRange(): TimeRange { + let from = 0; + let to = 0; + const fromString = timefilter.getTime().from; + const toString = timefilter.getTime().to; + if (typeof fromString === 'string' && typeof toString === 'string') { + const fromMoment = dateMath.parse(fromString); + const toMoment = dateMath.parse(toString); + if (typeof fromMoment !== 'undefined' && typeof toMoment !== 'undefined') { + const fromMs = fromMoment.valueOf(); + const toMs = toMoment.valueOf(); + from = fromMs; + to = toMs; + } + } + return { + to, + from, + }; +} diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.tsx b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.tsx index 9066fe0a0e8b9..59c185c76471f 100644 --- a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.tsx +++ b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.tsx @@ -3,33 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import React from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexPattern } from 'ui/index_patterns'; -import { EuiButton } from '@elastic/eui'; -import { Query } from 'src/legacy/core_plugins/data/public'; -import { setFullTimeRange } from './full_time_range_selector_service'; - -interface Props { - indexPattern: IndexPattern; - query: Query; - disabled: boolean; -} - -// Component for rendering a button which automatically sets the range of the time filter -// to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query. -export const FullTimeRangeSelector: React.SFC = ({ indexPattern, query, disabled }) => { - return ( - setFullTimeRange(indexPattern, query)}> - - - ); -}; +export { FullTimeRangeSelector } from './full_time_range_selector'; +export { getTimeFilterRange } from './full_time_range_selector_service'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/index.js b/x-pack/legacy/plugins/ml/public/jobs/index.js index cc1aeb85b8b59..9b0246240da68 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/index.js +++ b/x-pack/legacy/plugins/ml/public/jobs/index.js @@ -14,3 +14,4 @@ import './new_job/simple/population'; import './new_job/simple/recognize'; import './new_job/wizard'; import 'plugins/ml/components/validate_job'; +import './new_job_new'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.d.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.d.ts new file mode 100644 index 0000000000000..187a2645d3958 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.d.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; +import { IndexPatternWithType, IndexPatternTitle } from '../../../../common/types/kibana'; + +export interface SearchItems { + indexPattern: IndexPatternWithType; + savedSearch: SavedSearch; + query: any; + combinedQuery: any; +} + +export function SearchItemsProvider($route: Record, config: any): () => SearchItems; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html index da0bf3d38d4d5..a105dc6175f57 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html @@ -213,6 +213,102 @@ + + +
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts new file mode 100644 index 0000000000000..fbd9f40541970 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; +import { IndexPatternWithType, IndexPatternTitle } from '../../../../../common/types/kibana'; +import { Field, SplitField, AggFieldPair } from '../../../../../common/types/fields'; +import { ml } from '../../../../services/ml_api_service'; +import { mlResultsService } from '../../../../services/results_service'; +import { getCategoryFields } from './searches'; + +type DetectorIndex = number; +export interface LineChartPoint { + time: number | string; + value: number; +} +type SplitFieldValue = string | null; +export type LineChartData = Record; + +export class ChartLoader { + protected _indexPattern: IndexPatternWithType; + protected _savedSearch: SavedSearch; + protected _indexPatternTitle: IndexPatternTitle = ''; + protected _timeFieldName: string = ''; + protected _query: object = {}; + + constructor(indexPattern: IndexPatternWithType, savedSearch: SavedSearch, query: object) { + this._indexPattern = indexPattern; + this._savedSearch = savedSearch; + this._indexPatternTitle = indexPattern.title; + this._query = query; + + if (typeof indexPattern.timeFieldName === 'string') { + this._timeFieldName = indexPattern.timeFieldName; + } + } + + async loadLineCharts( + start: number, + end: number, + aggFieldPairs: AggFieldPair[], + splitField: SplitField, + splitFieldValue: SplitFieldValue, + intervalMs: number + ): Promise { + if (this._timeFieldName !== '') { + const splitFieldName = splitField !== null ? splitField.name : null; + + const resp = await ml.jobs.newJobLineChart( + this._indexPatternTitle, + this._timeFieldName, + start, + end, + intervalMs, + this._query, + aggFieldPairs.map(af => ({ + agg: af.agg.dslName, + field: af.field.name, + })), + splitFieldName, + splitFieldValue + ); + return resp.results; + } + return {}; + } + + async loadPopulationCharts( + start: number, + end: number, + aggFieldPairs: AggFieldPair[], + splitField: SplitField, + intervalMs: number + ): Promise { + if (this._timeFieldName !== '') { + const splitFieldName = splitField !== null ? splitField.name : ''; + + const resp = await ml.jobs.newJobPopulationsChart( + this._indexPatternTitle, + this._timeFieldName, + start, + end, + intervalMs, + this._query, + aggFieldPairs.map(af => { + const by = + af.by !== undefined && af.by.field !== null && af.by.value !== null + ? { field: af.by.field.name, value: af.by.value } + : { field: null, value: null }; + + return { + agg: af.agg.dslName, + field: af.field.name, + by, + }; + }), + splitFieldName + ); + return resp.results; + } + return {}; + } + + async loadEventRateChart( + start: number, + end: number, + intervalMs: number + ): Promise { + if (this._timeFieldName !== '') { + const resp = await mlResultsService.getEventRateData( + this._indexPatternTitle, + this._query, + this._timeFieldName, + start, + end, + intervalMs * 3 + ); + return Object.entries(resp.results).map(([time, value]) => ({ + time: +time, + value: value as number, + })); + } + return []; + } + + async loadFieldExampleValues(field: Field): Promise { + const { results } = await getCategoryFields( + this._indexPatternTitle, + field.name, + 10, + this._query + ); + return results; + } +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/index.ts new file mode 100644 index 0000000000000..73e8f88df479b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ChartLoader, LineChartData, LineChartPoint } from './chart_loader'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/searches.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/searches.ts new file mode 100644 index 0000000000000..f40b93fd8de89 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/searches.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; + +import { ml } from '../../../../services/ml_api_service'; + +interface CategoryResults { + success: boolean; + results: string[]; +} + +export function getCategoryFields( + indexPatternName: string, + fieldName: string, + size: number, + query: any +): Promise { + return new Promise((resolve, reject) => { + ml.esSearch({ + index: indexPatternName, + size: 0, + body: { + query, + aggs: { + catFields: { + terms: { + field: fieldName, + size, + }, + }, + }, + }, + }) + .then((resp: any) => { + const catFields = get(resp, ['aggregations', 'catFields', 'buckets'], []); + + resolve({ + success: true, + results: catFields.map((f: any) => f.key), + }); + }) + .catch((resp: any) => { + reject(resp); + }); + }); +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_settings.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_settings.ts index bb86cb4a6136a..fbb5efb718e2c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_settings.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_settings.ts @@ -25,8 +25,8 @@ export class ChartSettings { this._interval.setMaxBars(this.MAX_BARS); this._interval.setInterval('auto'); - // @ts-ignore: incomplete kibana types - const bounds = timefilter.getActiveBounds(); + const tf = timefilter as any; + const bounds = tf.getActiveBounds(); this._interval.setBounds(bounds); } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/index.ts index cbb6d5a598ee5..81cc00155a64c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/index.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './index_pattern_context'; export * from './job_creator'; export * from './job_runner'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/index_pattern_context.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/index_pattern_context.ts new file mode 100644 index 0000000000000..aa92536da8d1d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/index_pattern_context.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { StaticIndexPattern } from 'ui/index_patterns'; + +export type IndexPatternContextValue = StaticIndexPattern | null; +export const IndexPatternContext = React.createContext(null); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/index.ts index 0cfad2cb0c6d6..eca52c064ce67 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/index.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/index.ts @@ -8,3 +8,9 @@ export { JobCreator } from './job_creator'; export { SingleMetricJobCreator } from './single_metric_job_creator'; export { MultiMetricJobCreator } from './multi_metric_job_creator'; export { PopulationJobCreator } from './population_job_creator'; +export { + isSingleMetricJobCreator, + isMultiMetricJobCreator, + isPopulationJobCreator, +} from './type_guards'; +export { jobCreatorFactory } from './job_creator_factory'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts index d4fbb11a59d4a..a33984d9e8492 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator.ts @@ -7,11 +7,14 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPatternWithType, IndexPatternTitle } from '../../../../../common/types/kibana'; import { Job, Datafeed, Detector, JobId, DatafeedId, BucketSpan } from './configs'; -import { createEmptyJob, createEmptyDatafeed } from './util'; +import { Aggregation } from '../../../../../common/types/fields'; +import { createEmptyJob, createEmptyDatafeed } from './util/default_configs'; import { mlJobService } from '../../../../services/job_service'; import { JobRunner, ProgressSubscriber } from '../job_runner'; +import { JOB_TYPE } from './util/constants'; export class JobCreator { + protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; protected _indexPattern: IndexPatternWithType; protected _savedSearch: SavedSearch; protected _indexPatternTitle: IndexPatternTitle = ''; @@ -23,6 +26,10 @@ export class JobCreator { protected _start: number = 0; protected _end: number = 0; protected _subscribers: ProgressSubscriber[]; + protected _aggs: Aggregation[] = []; + private _stopAllRefreshPolls: { + stop: boolean; + }; constructor(indexPattern: IndexPatternWithType, savedSearch: SavedSearch, query: object) { this._indexPattern = indexPattern; @@ -40,26 +47,47 @@ export class JobCreator { this._datafeed_config.query = query; this._subscribers = []; + this._stopAllRefreshPolls = { stop: false }; } - protected _addDetector(detector: Detector) { + public get type(): JOB_TYPE { + return this._type; + } + + protected _addDetector(detector: Detector, agg: Aggregation) { this._detectors.push(detector); + this._aggs.push(agg); } - protected _editDetector(detector: Detector, index: number) { + protected _editDetector(detector: Detector, agg: Aggregation, index: number) { if (this._detectors[index] !== undefined) { this._detectors[index] = detector; + this._aggs[index] = agg; } } protected _removeDetector(index: number) { this._detectors.splice(index, 1); + this._aggs.splice(index, 1); + } + + public removeAllDetectors() { + this._detectors.length = 0; } public get detectors(): Detector[] { return this._detectors; } + public get aggregationsInDetectors(): Aggregation[] { + return this._aggs; + } + + public getAggregation(index: number): Aggregation | null { + const agg = this._aggs[index]; + return agg !== undefined ? agg : null; + } + public set bucketSpan(bucketSpan: BucketSpan) { this._job_config.analysis_config.bucket_span = bucketSpan; } @@ -74,6 +102,17 @@ export class JobCreator { } } + public removeInfluencer(influencer: string) { + const idx = this._influencers.indexOf(influencer); + if (idx !== -1) { + this._influencers.splice(idx, 1); + } + } + + public removeAllInfluencers() { + this._influencers.length = 0; + } + public get influencers(): string[] { return this._influencers; } @@ -114,6 +153,10 @@ export class JobCreator { return this._job_config.groups; } + public set groups(groups: string[]) { + this._job_config.groups = groups; + } + public set modelPlot(enable: boolean) { if (enable) { this._job_config.model_plot_config = { @@ -223,4 +266,20 @@ export class JobCreator { public subscribeToProgress(func: ProgressSubscriber) { this._subscribers.push(func); } + + public get jobConfig(): Job { + return this._job_config; + } + + public get datafeedConfig(): Datafeed { + return this._datafeed_config; + } + + public get stopAllRefreshPolls(): { stop: boolean } { + return this._stopAllRefreshPolls; + } + + public forceStopRefreshPolls() { + this._stopAllRefreshPolls.stop = true; + } } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator_factory.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator_factory.ts new file mode 100644 index 0000000000000..c62ce5b64d5c5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/job_creator_factory.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; +import { SingleMetricJobCreator } from './single_metric_job_creator'; +import { MultiMetricJobCreator } from './multi_metric_job_creator'; +import { PopulationJobCreator } from './population_job_creator'; +import { IndexPatternWithType } from '../../../../../common/types/kibana'; + +import { JOB_TYPE } from './util/constants'; + +export const jobCreatorFactory = (jobType: JOB_TYPE) => ( + indexPattern: IndexPatternWithType, + savedSearch: SavedSearch, + query: object +) => { + let jc; + switch (jobType) { + case JOB_TYPE.SINGLE_METRIC: + jc = SingleMetricJobCreator; + break; + case JOB_TYPE.MULTI_METRIC: + jc = MultiMetricJobCreator; + break; + case JOB_TYPE.POPULATION: + jc = PopulationJobCreator; + break; + default: + jc = SingleMetricJobCreator; + break; + } + return new jc(indexPattern, savedSearch, query); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts index 0ef97f2333ac4..a8efd2e1e8ee7 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/multi_metric_job_creator.ts @@ -8,11 +8,13 @@ import { JobCreator } from './job_creator'; import { Field, Aggregation, SplitField } from '../../../../../common/types/fields'; import { Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; +import { JOB_TYPE } from './util/constants'; export class MultiMetricJobCreator extends JobCreator { // a multi metric job has one optional overall partition field // which is the same for all detectors. private _splitField: SplitField = null; + protected _type: JOB_TYPE = JOB_TYPE.MULTI_METRIC; // set the split field, applying it to each detector public setSplitField(field: SplitField) { @@ -39,12 +41,12 @@ export class MultiMetricJobCreator extends JobCreator { public addDetector(agg: Aggregation, field: Field | null) { const dtr: Detector = this._createDetector(agg, field); - this._addDetector(dtr); + this._addDetector(dtr, agg); } public editDetector(agg: Aggregation, field: Field | null, index: number) { const dtr: Detector = this._createDetector(agg, field); - this._editDetector(dtr, index); + this._editDetector(dtr, agg, index); } // create a new detector object, applying the overall split field diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts index 2b8eeb8744122..b34586457ee74 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/population_job_creator.ts @@ -8,12 +8,14 @@ import { JobCreator } from './job_creator'; import { Field, Aggregation, SplitField } from '../../../../../common/types/fields'; import { Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; +import { JOB_TYPE } from './util/constants'; export class PopulationJobCreator extends JobCreator { // a population job has one overall over (split) field, which is the same for all detectors // each detector has an optional by field private _splitField: SplitField = null; private _byFields: SplitField[] = []; + protected _type: JOB_TYPE = JOB_TYPE.POPULATION; // add a by field to a specific detector public setByField(field: SplitField, index: number) { @@ -70,7 +72,7 @@ export class PopulationJobCreator extends JobCreator { public addDetector(agg: Aggregation, field: Field | null) { const dtr: Detector = this._createDetector(agg, field); - this._addDetector(dtr); + this._addDetector(dtr, agg); this._byFields.push(null); } @@ -84,7 +86,7 @@ export class PopulationJobCreator extends JobCreator { dtr.by_field_name = sp.id; } - this._editDetector(dtr, index); + this._editDetector(dtr, agg, index); } // create a detector object, adding the current over field diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts index a71820fbbb7ba..c1c2d2aa50036 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/single_metric_job_creator.ts @@ -10,10 +10,11 @@ import { Field, Aggregation } from '../../../../../common/types/fields'; import { Detector, BucketSpan } from './configs'; import { createBasicDetector } from './util/default_configs'; import { KIBANA_AGGREGATION } from '../../../../../common/constants/aggregation_types'; +import { JOB_TYPE } from './util/constants'; export class SingleMetricJobCreator extends JobCreator { private _field: Field | null = null; - private _agg: Aggregation | null = null; + protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; // only a single detector exists for this job type // therefore _addDetector and _editDetector merge into this @@ -22,33 +23,38 @@ export class SingleMetricJobCreator extends JobCreator { const dtr: Detector = createBasicDetector(agg, field); if (this._detectors.length === 0) { - this._addDetector(dtr); + this._addDetector(dtr, agg); } else { - this._editDetector(dtr, 0); + this._editDetector(dtr, agg, 0); } this._field = field; - this._agg = agg; - this._createAggregations(); + this._createDatafeedAggregations(); } public set bucketSpan(bucketSpan: BucketSpan) { this._job_config.analysis_config.bucket_span = bucketSpan; - this._createAggregations(); + this._createDatafeedAggregations(); + } + + // overriding set means we need to override get too + // JS doesn't do inheritance very well + public get bucketSpan(): BucketSpan { + return this._job_config.analysis_config.bucket_span; } // aggregations need to be recreated whenever the detector or bucket_span change - private _createAggregations() { + private _createDatafeedAggregations() { if ( this._detectors.length && typeof this._job_config.analysis_config.bucket_span === 'string' && - this._agg !== null + this._aggs.length > 0 ) { delete this._job_config.analysis_config.summary_count_field_name; delete this._datafeed_config.aggregations; - const functionName = this._agg.dslName; + const functionName = this._aggs[0].dslName; const timeField = this._job_config.data_description.time_field; const duration = parseInterval(this._job_config.analysis_config.bucket_span); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/type_guards.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/type_guards.ts new file mode 100644 index 0000000000000..9bba4981ea078 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/type_guards.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SingleMetricJobCreator } from './single_metric_job_creator'; +import { MultiMetricJobCreator } from './multi_metric_job_creator'; +import { PopulationJobCreator } from './population_job_creator'; +import { JOB_TYPE } from './util/constants'; + +export function isSingleMetricJobCreator( + jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator +): jobCreator is SingleMetricJobCreator { + return jobCreator.type === JOB_TYPE.SINGLE_METRIC; +} + +export function isMultiMetricJobCreator( + jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator +): jobCreator is MultiMetricJobCreator { + return jobCreator.type === JOB_TYPE.MULTI_METRIC; +} + +export function isPopulationJobCreator( + jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator +): jobCreator is PopulationJobCreator { + return jobCreator.type === JOB_TYPE.POPULATION; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/constants.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/constants.ts new file mode 100644 index 0000000000000..5bbc1c78bb5d1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum JOB_TYPE { + SINGLE_METRIC = 'single_metric', + MULTI_METRIC = 'multi_metric', + POPULATION = 'population', +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts index 80c619e0221b7..81ac531323e4c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts @@ -21,10 +21,13 @@ export class JobRunner { private _start: number = 0; private _end: number = 0; private _datafeedState: DATAFEED_STATE = DATAFEED_STATE.STOPPED; - private _refreshInterval = REFRESH_INTERVAL_MS; + private _refreshInterval: number = REFRESH_INTERVAL_MS; private _progress$: BehaviorSubject; private _percentageComplete: Progress = 0; + private _stopRefreshPoll: { + stop: boolean; + }; constructor(jobCreator: JobCreator) { this._jobId = jobCreator.jobId; @@ -32,6 +35,7 @@ export class JobRunner { this._start = jobCreator.start; this._end = jobCreator.end; this._percentageComplete = 0; + this._stopRefreshPoll = jobCreator.stopAllRefreshPolls; this._progress$ = new BehaviorSubject(this._percentageComplete); // link the _subscribers list from the JobCreator @@ -74,12 +78,12 @@ export class JobRunner { this._percentageComplete = 0; const check = async () => { - const isRunning = await this.isRunning(); + const { isRunning, progress } = await this.getProgress(); - this._percentageComplete = await this.getProgress(); + this._percentageComplete = progress; this._progress$.next(this._percentageComplete); - if (isRunning) { + if (isRunning === true && this._stopRefreshPoll.stop === false) { setTimeout(async () => { await check(); }, this._refreshInterval); @@ -97,10 +101,8 @@ export class JobRunner { } } - public async getProgress(): Promise { - const lrts = await this.getLatestRecordTimeStamp(); - const progress = (lrts - this._start) / (this._end - this._start); - return Math.round(progress * 100); + public async getProgress(): Promise<{ progress: Progress; isRunning: boolean }> { + return await ml.jobs.getLookBackProgress(this._jobId, this._start, this._end); } public subscribeToProgress(func: ProgressSubscriber) { @@ -108,30 +110,7 @@ export class JobRunner { } public async isRunning(): Promise { - const state = await this.getDatafeedState(); - this._datafeedState = state; - return ( - state === DATAFEED_STATE.STARTED || - state === DATAFEED_STATE.STARTING || - state === DATAFEED_STATE.STOPPING - ); - } - - public async getDatafeedState(): Promise { - const stats = await ml.getDatafeedStats({ datafeedId: this._datafeedId }); - if (stats.datafeeds.length) { - return stats.datafeeds[0].state; - } - return DATAFEED_STATE.STOPPED; - } - - public async getLatestRecordTimeStamp(): Promise { - const stats = await ml.getJobStats({ jobId: this._jobId }); - - if (stats.jobs.length) { - const time = stats.jobs[0].data_counts.latest_record_timestamp; - return time === undefined ? 0 : time; - } - return 0; + const { isRunning } = await this.getProgress(); + return isRunning; } } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/index.ts new file mode 100644 index 0000000000000..ef0b05f73fa31 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ResultsLoader, Results, ModelItem, Anomaly } from './results_loader'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts new file mode 100644 index 0000000000000..cddf97fdcf41a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BehaviorSubject } from 'rxjs'; +import { + SingleMetricJobCreator, + MultiMetricJobCreator, + isMultiMetricJobCreator, + PopulationJobCreator, + isPopulationJobCreator, +} from '../job_creator'; +import { mlResultsService } from '../../../../services/results_service'; +import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; +import { getSeverityType } from '../../../../../common/util/anomaly_utils'; +import { ANOMALY_SEVERITY } from '../../../../../common/constants/anomalies'; +import { getScoresByRecord } from './searches'; +import { JOB_TYPE } from '../job_creator/util/constants'; +import { ChartLoader } from '../chart_loader'; + +export interface Results { + progress: number; + model: Record; + anomalies: Record; +} + +export interface ModelItem { + time: number; + actual: number; + modelUpper: number; + modelLower: number; +} + +export interface Anomaly { + time: number; + value: number; + severity: ANOMALY_SEVERITY; +} + +const emptyModelItem = { + time: 0, + actual: 0, + modelUpper: 0, + modelLower: 0, +}; + +interface SplitFieldWithValue { + name: string; + value: string; +} + +const LAST_UPDATE_DELAY_MS = 500; + +export type ResultsSubscriber = (results: Results) => void; + +type AnyJobCreator = SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator; + +export class ResultsLoader { + private _results$: BehaviorSubject; + private _resultsSearchRunning = false; + private _jobCreator: AnyJobCreator; + private _chartInterval: MlTimeBuckets; + private _lastModelTimeStamp: number = 0; + private _lastResultsTimeout: any = null; + private _chartLoader: ChartLoader; + // private _detectorSplitFieldWithValue: DetectorSplitFieldFilter; + + private _results: Results = { + progress: 0, + model: [], + anomalies: [], + }; + + private _detectorSplitFieldFilters: SplitFieldWithValue | null = null; + private _splitFieldFiltersLoaded: boolean = false; + + constructor(jobCreator: AnyJobCreator, chartInterval: MlTimeBuckets, chartLoader: ChartLoader) { + this._jobCreator = jobCreator; + this._chartInterval = chartInterval; + this._results$ = new BehaviorSubject(this._results); + this._chartLoader = chartLoader; + + jobCreator.subscribeToProgress(this.progressSubscriber); + } + + progressSubscriber = async (progress: number) => { + if (this._resultsSearchRunning === false) { + if (progress - this._results.progress > 5 || progress === 100) { + if (this._splitFieldFiltersLoaded === false) { + this._splitFieldFiltersLoaded = true; + // load detector field filters if this is the first run. + await this._populateDetectorSplitFieldFilters(); + } + + this._updateData(progress, false); + + if (progress === 100) { + // after the job has finished, do one final update + // a while after the last 100% has been received. + // note, there may be multiple 100% progresses sent as they will only stop once the + // datafeed has stopped. + clearTimeout(this._lastResultsTimeout); + this._lastResultsTimeout = setTimeout(() => { + this._updateData(progress, true); + }, LAST_UPDATE_DELAY_MS); + } + } + } + }; + + private async _updateData(progress: number, fullRefresh: boolean) { + this._resultsSearchRunning = true; + + if (fullRefresh === true) { + this._clearResults(); + } + this._results.progress = progress; + + const getAnomalyData = + this._jobCreator.type === JOB_TYPE.SINGLE_METRIC + ? () => this._loadJobAnomalyData(0) + : () => this._loadDetectorsAnomalyData(); + + // TODO - load more that one model + const [model, anomalies] = await Promise.all([this._loadModelData(0), getAnomalyData()]); + this._results.model = model; + this._results.anomalies = anomalies; + + this._resultsSearchRunning = false; + this._results$.next(this._results); + } + + public subscribeToResults(func: ResultsSubscriber) { + this._results$.subscribe(func); + } + + public get progress() { + return this._results.progress; + } + + private _clearResults() { + this._results.model = {}; + this._results.anomalies = {}; + this._results.progress = 0; + this._lastModelTimeStamp = 0; + } + + private async _loadModelData(dtrIndex: number): Promise> { + if (this._jobCreator.modelPlot === false) { + return []; + } + + const agg = this._jobCreator.getAggregation(dtrIndex); + if (agg === null) { + return { [dtrIndex]: [emptyModelItem] }; + } + const resp = await mlResultsService.getModelPlotOutput( + this._jobCreator.jobId, + dtrIndex, + [], + this._lastModelTimeStamp, + this._jobCreator.end, + `${this._chartInterval.getInterval().asMilliseconds()}ms`, + agg.mlModelPlotAgg + ); + + return this._createModel(resp, dtrIndex); + } + + private _createModel(resp: any, dtrIndex: number): Record { + if (this._results.model[dtrIndex] === undefined) { + this._results.model[dtrIndex] = []; + } + + // create ModelItem list from search results + const model = Object.entries(resp.results).map( + ([time, modelItems]) => + ({ + time: +time, + ...modelItems, + } as ModelItem) + ); + + if (model.length > 10) { + // discard the last 5 buckets in the previously loaded model to avoid partial results + // set the _lastModelTimeStamp to be 5 buckets behind so we load the correct + // section of results next time. + this._lastModelTimeStamp = model[model.length - 5].time; + for (let i = 0; i < 5; i++) { + this._results.model[dtrIndex].pop(); + } + } + + // return a new array from the old and new model + return { [dtrIndex]: this._results.model[dtrIndex].concat(model) }; + } + + private async _loadJobAnomalyData(dtrIndex: number): Promise> { + const resp = await mlResultsService.getScoresByBucket( + [this._jobCreator.jobId], + this._jobCreator.start, + this._jobCreator.end, + `${this._chartInterval.getInterval().asMilliseconds()}ms`, + 1 + ); + + const results = resp.results[this._jobCreator.jobId]; + if (results === undefined) { + return []; + } + + const anomalies: Record = {}; + anomalies[0] = Object.entries(results).map( + ([time, value]) => + ({ time: +time, value, severity: getSeverityType(value as number) } as Anomaly) + ); + return anomalies; + } + + private async _loadDetectorsAnomalyData(): Promise> { + const resp = await getScoresByRecord( + this._jobCreator.jobId, + this._jobCreator.start, + this._jobCreator.end, + `${this._chartInterval.getInterval().asMilliseconds()}ms`, + this._detectorSplitFieldFilters + ); + + const anomalies: Record = {}; + Object.entries(resp.results).forEach(([dtrIdx, results]) => { + anomalies[+dtrIdx] = results.map( + r => ({ ...r, severity: getSeverityType(r.value as number) } as Anomaly) + ); + }); + return anomalies; + } + + private async _populateDetectorSplitFieldFilters() { + if (isMultiMetricJobCreator(this._jobCreator) || isPopulationJobCreator(this._jobCreator)) { + if (this._jobCreator.splitField !== null) { + const fieldValues = await this._chartLoader.loadFieldExampleValues( + this._jobCreator.splitField + ); + if (fieldValues.length > 0) { + this._detectorSplitFieldFilters = { + name: this._jobCreator.splitField.name, + value: fieldValues[0], + }; + } + return; + } + } + this._detectorSplitFieldFilters = null; + } +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/searches.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/searches.ts new file mode 100644 index 0000000000000..bb47af0c4b27a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/searches.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; + +import { ML_RESULTS_INDEX_PATTERN } from './../../../../../common/constants/index_patterns'; +import { escapeForElasticsearchQuery } from '../../../../util/string_utils'; +import { ml } from '../../../../services/ml_api_service'; + +interface SplitFieldWithValue { + name: string; + value: string; +} + +type TimeStamp = number; + +interface Result { + time: TimeStamp; + value: Value; +} + +interface ProcessedResults { + success: boolean; + results: Record; + totalResults: number; +} + +// detector swimlane search +export function getScoresByRecord( + jobId: string, + earliestMs: number, + latestMs: number, + interval: string, + firstSplitField: SplitFieldWithValue | null +): Promise { + return new Promise((resolve, reject) => { + const obj: ProcessedResults = { + success: true, + results: {}, + totalResults: 0, + }; + + let jobIdFilterStr = 'job_id: ' + jobId; + if (firstSplitField && firstSplitField.value !== undefined) { + // Escape any reserved characters for the query_string query, + // wrapping the value in quotes to do a phrase match. + // Backslash is a special character in JSON strings, so doubly escape + // any backslash characters which exist in the field value. + jobIdFilterStr += ` AND ${escapeForElasticsearchQuery(firstSplitField.name)}:`; + jobIdFilterStr += `"${String(firstSplitField.value).replace(/\\/g, '\\\\')}"`; + } + + ml.esSearch({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + }, + }, + { + bool: { + must: [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + query_string: { + query: jobIdFilterStr, + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + detector_index: { + terms: { + field: 'detector_index', + order: { + recordScore: 'desc', + }, + }, + aggs: { + recordScore: { + max: { + field: 'record_score', + }, + }, + byTime: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 1, + extended_bounds: { + min: earliestMs, + max: latestMs, + }, + }, + aggs: { + recordScore: { + max: { + field: 'record_score', + }, + }, + }, + }, + }, + }, + }, + }, + }) + .then((resp: any) => { + const detectorsByIndex = get(resp, ['aggregations', 'detector_index', 'buckets'], []); + detectorsByIndex.forEach((dtr: any) => { + const dtrResults: Result[] = []; + const dtrIndex = +dtr.key; + + const buckets = get(dtr, ['byTime', 'buckets'], []); + for (let j = 0; j < buckets.length; j++) { + const bkt: any = buckets[j]; + const time = bkt.key; + dtrResults.push({ + time, + value: get(bkt, ['recordScore', 'value']), + }); + } + obj.results[dtrIndex] = dtrResults; + }); + + resolve(obj); + }) + .catch((resp: any) => { + reject(resp); + }); + }); +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts new file mode 100644 index 0000000000000..d3feaf087524c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './pages/new_job/route'; +import './pages/new_job/directive'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/anomalies.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/anomalies.tsx new file mode 100644 index 0000000000000..1fef1d804e6f2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/anomalies.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { AnnotationDomainTypes, getAnnotationId, LineAnnotation } from '@elastic/charts'; +import { Anomaly } from '../../../../common/results_loader'; +import { getSeverityColor } from '../../../../../../../common/util/anomaly_utils'; +import { ANOMALY_THRESHOLD } from '../../../../../../../common/constants/anomalies'; + +interface Props { + anomalyData?: Anomaly[]; +} + +interface Severities { + critical: any[]; + major: any[]; + minor: any[]; + warning: any[]; + unknown: any[]; + low: any[]; +} + +function getAnomalyStyle(threshold: number) { + return { + line: { + stroke: getSeverityColor(threshold), + strokeWidth: 3, + opacity: 1, + }, + }; +} + +function splitAnomalySeverities(anomalies: Anomaly[]) { + const severities: Severities = { + critical: [], + major: [], + minor: [], + warning: [], + unknown: [], + low: [], + }; + anomalies.forEach(a => { + if (a.value !== 0) { + severities[a.severity].push({ dataValue: a.time }); + } + }); + return severities; +} + +export const Anomalies: FC = ({ anomalyData }) => { + const anomalies = anomalyData === undefined ? [] : anomalyData; + const severities: Severities = splitAnomalySeverities(anomalies); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/anomaly_chart.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/anomaly_chart.tsx new file mode 100644 index 0000000000000..0d3b436caee77 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/anomaly_chart.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { Chart, Settings, TooltipType } from '@elastic/charts'; +import { ModelItem, Anomaly } from '../../../../common/results_loader'; +import { Anomalies } from './anomalies'; +import { ModelBounds } from './model_bounds'; +import { Line } from './line'; +import { Scatter } from './scatter'; +import { Axes } from '../common/axes'; +import { getXRange } from '../common/utils'; +import { LineChartPoint } from '../../../../common/chart_loader'; + +export enum CHART_TYPE { + LINE, + SCATTER, +} + +interface Props { + chartType: CHART_TYPE; + chartData: LineChartPoint[]; + modelData: ModelItem[]; + anomalyData: Anomaly[]; + height: string; + width: string; +} + +export const AnomalyChart: FC = ({ + chartType, + chartData, + modelData, + anomalyData, + height, + width, +}) => { + const data = chartType === CHART_TYPE.SCATTER ? flattenData(chartData) : chartData; + const xDomain = getXRange(data); + return ( +
+ + + + + + {chartType === CHART_TYPE.LINE && } + {chartType === CHART_TYPE.SCATTER && } + +
+ ); +}; + +function flattenData(data: any): LineChartPoint[] { + const chartData = data.reduce((p: any[], c: any) => { + p.push(...c.values.map((v: any) => ({ time: c.time, value: v.value }))); + return p; + }, []); + return chartData; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/index.ts new file mode 100644 index 0000000000000..46880334f3e9a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { AnomalyChart, CHART_TYPE } from './anomaly_chart'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/line.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/line.tsx new file mode 100644 index 0000000000000..3da84da900a2d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/line.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { LineSeries, getSpecId, ScaleType, CurveType } from '@elastic/charts'; +import { getCustomColor } from '../common/utils'; +import { seriesStyle, LINE_COLOR } from '../common/settings'; + +interface Props { + chartData: any[]; +} + +const SPEC_ID = 'line'; + +const lineSeriesStyle = { + ...seriesStyle, +}; + +export const Line: FC = ({ chartData }) => { + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/model_bounds.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/model_bounds.tsx new file mode 100644 index 0000000000000..0d76b50b80b97 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/model_bounds.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { getSpecId, ScaleType, AreaSeries, CurveType } from '@elastic/charts'; +import { ModelItem } from '../../../../common/results_loader'; +import { getCustomColor } from '../common/utils'; +import { seriesStyle, MODEL_COLOR } from '../common/settings'; + +interface Props { + modelData?: ModelItem[]; +} + +const SPEC_ID = 'model'; + +const areaSeriesStyle = { + ...seriesStyle, + area: { + ...seriesStyle.area, + visible: true, + }, + line: { + ...seriesStyle.line, + strokeWidth: 1, + opacity: 0.4, + }, +}; + +export const ModelBounds: FC = ({ modelData }) => { + const model = modelData === undefined ? [] : modelData; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/scatter.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/scatter.tsx new file mode 100644 index 0000000000000..3a8fb9dbfb4d7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/anomaly_chart/scatter.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { LineSeries, getSpecId, ScaleType, CurveType } from '@elastic/charts'; +import { getCustomColor } from '../common/utils'; +import { seriesStyle, LINE_COLOR } from '../common/settings'; + +interface Props { + chartData: any[]; +} + +const SPEC_ID = 'scatter'; + +const scatterSeriesStyle = { + ...seriesStyle, + line: { + ...seriesStyle.line, + visible: false, + }, + point: { + ...seriesStyle.point, + visible: true, + }, +}; + +export const Scatter: FC = ({ chartData }) => { + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/axes.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/axes.tsx new file mode 100644 index 0000000000000..d1b8f1fe284b8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/axes.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment } from 'react'; +import { Axis, getAxisId, Position, timeFormatter, niceTimeFormatByDay } from '@elastic/charts'; +import { getYRange } from './utils'; +import { LineChartPoint } from '../../../../common/chart_loader'; + +const dateFormatter = timeFormatter(niceTimeFormatByDay(3)); + +interface Props { + chartData?: LineChartPoint[]; +} + +// round to 2dp +function tickFormatter(d: number): string { + return Math.round((d * 100) / 100).toString(); +} + +export const Axes: FC = ({ chartData }) => { + const yDomain = chartData !== undefined ? getYRange(chartData) : undefined; + + return ( + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts new file mode 100644 index 0000000000000..288600d52d29e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const LINE_COLOR = '#006BB4'; +export const MODEL_COLOR = '#006BB4'; + +export interface ChartSettings { + width: string; + height: string; + cols: number; + intervalMs: number; +} + +export const defaultChartSettings: ChartSettings = { + width: '100%', + height: '300px', + cols: 1, + intervalMs: 0, +}; + +export const seriesStyle = { + line: { + stroke: '', + strokeWidth: 2, + visible: true, + opacity: 1, + }, + border: { + visible: false, + strokeWidth: 0, + stroke: '', + }, + point: { + visible: false, + radius: 2, + stroke: '', + strokeWidth: 4, + opacity: 0.5, + }, + area: { + fill: '', + opacity: 0.25, + visible: false, + }, +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/utils.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/utils.ts new file mode 100644 index 0000000000000..74d01b00f9254 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/utils.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSpecId, CustomSeriesColorsMap, DataSeriesColorsValues } from '@elastic/charts'; + +export function getCustomColor(specId: string, color: string): CustomSeriesColorsMap { + const lineDataSeriesColorValues: DataSeriesColorsValues = { + colorValues: [], + specId: getSpecId(specId), + }; + return new Map([[lineDataSeriesColorValues, color]]); +} + +export function getYRange(chartData: any) { + let max: number = Number.MIN_VALUE; + let min: number = Number.MAX_VALUE; + chartData.forEach((r: any) => { + max = Math.max(r.value, max); + min = Math.min(r.value, min); + }); + + const padding = (max - min) * 0.1; + max += padding; + min -= padding; + + return { + min, + max, + }; +} + +export function getXRange(lineChartData: any) { + return { + min: lineChartData[0].time, + max: lineChartData[lineChartData.length - 1].time, + }; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/event_rate_chart.tsx new file mode 100644 index 0000000000000..53df4772f2e1c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { BarSeries, Chart, getSpecId, ScaleType, Settings, TooltipType } from '@elastic/charts'; +import { Axes } from '../common/axes'; +import { getCustomColor } from '../common/utils'; +import { LineChartPoint } from '../../../../common/chart_loader'; + +interface Props { + eventRateChartData: LineChartPoint[]; + height: string; + width: string; + showAxis?: boolean; +} + +const SPEC_ID = 'event_rate'; +const COLOR = '#006BB4'; + +export const EventRateChart: FC = ({ eventRateChartData, height, width, showAxis }) => { + return ( +
+ + {showAxis === true && } + + + + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/index.ts new file mode 100644 index 0000000000000..589e735f23519 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EventRateChart } from './event_rate_chart'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts new file mode 100644 index 0000000000000..3974c73bfcb2c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createContext, Dispatch } from 'react'; +import { Field, Aggregation } from '../../../../../common/types/fields'; +import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; +import { + SingleMetricJobCreator, + MultiMetricJobCreator, + PopulationJobCreator, +} from '../../common/job_creator'; +import { ChartLoader } from '../../common/chart_loader'; +import { ResultsLoader } from '../../common/results_loader'; + +export interface ExistingJobsAndGroups { + jobs: string[]; + groups: string[]; +} + +export interface JobCreatorContextValue { + jobCreatorUpdated: number; + jobCreatorUpdate: Dispatch; + jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator; + chartLoader: ChartLoader; + resultsLoader: ResultsLoader; + chartInterval: MlTimeBuckets; + fields: Field[]; + aggs: Aggregation[]; + existingJobsAndGroups: ExistingJobsAndGroups; +} + +export const JobCreatorContext = createContext({ + jobCreatorUpdated: 0, + jobCreatorUpdate: () => {}, + jobCreator: {} as SingleMetricJobCreator, + chartLoader: {} as ChartLoader, + resultsLoader: {} as ResultsLoader, + chartInterval: {} as MlTimeBuckets, + fields: [], + aggs: [], + existingJobsAndGroups: {} as ExistingJobsAndGroups, +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx new file mode 100644 index 0000000000000..06fed41923d86 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext } from 'react'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiComboBox, + EuiComboBoxOptionProps, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { JobCreatorContext } from '../job_creator_context'; +import { tabColor } from '../../../../../../common/util/group_color_utils'; + +interface Props { + selectedGroupNames: string[]; + setSelectedGroupNames: (groupNames: string[]) => void; +} + +export const GroupsInput: FC = ({ selectedGroupNames, setSelectedGroupNames }) => { + const { existingJobsAndGroups } = useContext(JobCreatorContext); + + const groups: EuiComboBoxOptionProps[] = existingJobsAndGroups.groups.map((g: string) => ({ + label: g, + color: tabColor(g), + })); + + const selectedGroups: EuiComboBoxOptionProps[] = selectedGroupNames.map((g: string) => ({ + label: g, + color: tabColor(g), + })); + + function setSelectedGroups(options: EuiComboBoxOptionProps[]) { + setSelectedGroupNames(options.map(g => g.label)); + } + + function onCreateGroup(input: string, flattenedOptions: EuiComboBoxOptionProps[]) { + const normalizedSearchValue = input.trim().toLowerCase(); + + if (!normalizedSearchValue) { + return; + } + + const newGroup: EuiComboBoxOptionProps = { + label: input, + color: tabColor(input), + }; + + if ( + flattenedOptions.findIndex( + option => option.label.trim().toLowerCase() === normalizedSearchValue + ) === -1 + ) { + groups.push(newGroup); + } + + setSelectedGroupNames([...selectedGroups, newGroup].map(g => g.label)); + } + + return ( + Groups} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/index.ts new file mode 100644 index 0000000000000..766ccdc7cc0d1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { JobDetailsStep } from './job_details'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_description_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_description_input.tsx new file mode 100644 index 0000000000000..5d610759e509a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_description_input.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow, EuiTextArea } from '@elastic/eui'; + +interface Props { + jobDescription: string; + setJobDescription: (id: string) => void; +} + +export const JobDescriptionInput: FC = ({ jobDescription, setJobDescription }) => { + return ( + Job description} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + setJobDescription(e.target.value)} + /> + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_details.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_details.tsx new file mode 100644 index 0000000000000..50ea9d17f5086 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_details.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { WizardNav } from '../../../../../data_frame/components/wizard_nav'; +import { JobIdInput } from './job_id_input'; +import { JobDescriptionInput } from './job_description_input'; +import { GroupsInput } from './groups_input'; +import { WIZARD_STEPS, StepProps } from '../step_types'; +import { JobCreatorContext } from '../job_creator_context'; +import { KibanaContext, isKibanaContext } from '../../../../../data_frame/common/kibana_context'; + +export const JobDetailsStep: FC = ({ setCurrentStep, isCurrentStep }) => { + const kibanaContext = useContext(KibanaContext); + if (!isKibanaContext(kibanaContext)) { + return null; + } + + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); + + const [jobId, setJobId] = useState(jobCreator.jobId); + const [jobDescription, setJobDescription] = useState(jobCreator.description); + const [selectedGroups, setSelectedGroups] = useState(jobCreator.groups); + + useEffect(() => { + jobCreator.jobId = jobId; + jobCreatorUpdate(); + }, [jobId]); + + useEffect(() => { + jobCreator.description = jobDescription; + jobCreatorUpdate(); + }, [jobDescription]); + + useEffect(() => { + jobCreator.groups = selectedGroups; + jobCreatorUpdate(); + }, [selectedGroups.join()]); + + function nextActive(): boolean { + return jobId !== ''; + } + + return ( + + {isCurrentStep && ( + + + + + + + + + + + setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} + next={() => setCurrentStep(WIZARD_STEPS.SUMMARY)} + nextActive={nextActive()} + /> + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_id_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_id_input.tsx new file mode 100644 index 0000000000000..bea348097b260 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_id_input.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow, EuiFieldText } from '@elastic/eui'; + +interface Props { + jobId: string; + setJobId: (id: string) => void; +} + +export const JobIdInput: FC = ({ jobId, setJobId }) => { + return ( + Job Id} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + setJobId(e.target.value)} /> + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/agg_select.tsx new file mode 100644 index 0000000000000..e406436d2aab0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; + +import { Field, Aggregation, AggFieldPair } from '../../../../../../../../common/types/fields'; + +// The display label used for an aggregation e.g. sum(bytes). +export type Label = string; + +// Label object structured for EUI's ComboBox. +export interface DropDownLabel { + label: Label; + agg: Aggregation; + field: Field; +} + +// Label object structure for EUI's ComboBox with support for nesting. +export interface DropDownOption { + label: Label; + options: DropDownLabel[]; +} + +export type DropDownProps = DropDownLabel[] | EuiComboBoxOptionProps[]; + +interface Props { + fields: Field[]; + changeHandler(d: EuiComboBoxOptionProps[]): void; + selectedOptions: EuiComboBoxOptionProps[]; + removeOptions: AggFieldPair[]; +} + +export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, removeOptions }) => { + // create list of labels based on already selected detectors + // so they can be removed from the dropdown list + const removeLabels = removeOptions.map(o => `${o.agg.title}(${o.field.name})`); + + const options: EuiComboBoxOptionProps[] = fields.map(f => { + const aggOption: DropDownOption = { label: f.name, options: [] }; + if (typeof f.aggs !== 'undefined') { + aggOption.options = f.aggs + .map( + a => + ({ + label: `${a.title}(${f.name})`, + agg: a, + field: f, + } as DropDownLabel) + ) + .filter(o => removeLabels.includes(o.label) === false); + } + return aggOption; + }); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/index.ts new file mode 100644 index 0000000000000..43b1c86e76a3f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AggSelect, DropDownLabel, DropDownOption, DropDownProps } from './agg_select'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx new file mode 100644 index 0000000000000..2095d3bd839b9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext, useEffect, useState } from 'react'; + +import { BucketSpanInput } from './bucket_span_input'; +import { JobCreatorContext } from '../../../job_creator_context'; + +export const BucketSpan: FC = () => { + const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const [bucketSpan, setBucketSpan] = useState(jobCreator.bucketSpan); + + useEffect(() => { + jobCreator.bucketSpan = bucketSpan; + jobCreatorUpdate(); + }, [bucketSpan]); + + useEffect(() => { + setBucketSpan(jobCreator.bucketSpan); + }, [jobCreatorUpdated]); + + return ; +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx new file mode 100644 index 0000000000000..14f47a66ed9ba --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow, EuiFieldText } from '@elastic/eui'; + +interface Props { + bucketSpan: string; + setBucketSpan: (bs: string) => void; +} + +export const BucketSpanInput: FC = ({ bucketSpan, setBucketSpan }) => { + return ( + Bucket span} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + setBucketSpan(e.target.value)} + /> + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/index.ts new file mode 100644 index 0000000000000..14baca0da4599 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { BucketSpan } from './bucket_span'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/detector_title.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/detector_title.tsx new file mode 100644 index 0000000000000..aca883b552aab --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/detector_title.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; + +import { Field, Aggregation, SplitField } from '../../../../../../../../common/types/fields'; + +interface DetectorTitleProps { + index: number; + agg: Aggregation; + field: Field; + splitField: SplitField; + deleteDetector?: (dtrIds: number) => void; +} + +export const DetectorTitle: FC = ({ + index, + agg, + field, + splitField, + deleteDetector, +}) => { + return ( + + + {getTitle(agg, field, splitField)} + + + + {deleteDetector !== undefined && ( + deleteDetector(index)} + iconType="cross" + size="s" + aria-label="Next" + /> + )} + + + ); +}; + +function getTitle(agg: Aggregation, field: Field, splitField: SplitField): string { + // let title = ${agg.title}(${field.name})`; + // if (splitField !== null) { + // title += ` split by ${splitField.name}`; + // } + // return title; + return `${agg.title}(${field.name})`; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/index.ts new file mode 100644 index 0000000000000..1eb9cac39ed93 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DetectorTitle } from './detector_title'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/index.ts new file mode 100644 index 0000000000000..10dd3978d79d3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { Influencers } from './influencers'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx new file mode 100644 index 0000000000000..43a68b0ab7df5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; + +import { InfluencersSelect } from './influencers_select'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { + MultiMetricJobCreator, + isMultiMetricJobCreator, + PopulationJobCreator, + isPopulationJobCreator, +} from '../../../../../common/job_creator'; + +export const Influencers: FC = () => { + const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + if (isMultiMetricJobCreator(jc) === false && isPopulationJobCreator(jc) === false) { + return ; + } + + const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator; + const { fields } = newJobCapsService; + const [influencers, setInfluencers] = useState([...jobCreator.influencers]); + const [splitField, setSplitField] = useState(jobCreator.splitField); + + useEffect(() => { + jobCreator.removeAllInfluencers(); + influencers.forEach(i => jobCreator.addInfluencer(i)); + jobCreatorUpdate(); + }, [influencers.join()]); + + useEffect(() => { + // if the split field has changed auto add it to the influencers + if (splitField !== null && influencers.includes(splitField.name) === false) { + setInfluencers([...influencers, splitField.name]); + } + }, [splitField]); + + useEffect(() => { + setSplitField(jobCreator.splitField); + setInfluencers([...jobCreator.influencers]); + }, [jobCreatorUpdated]); + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx new file mode 100644 index 0000000000000..ea2f45c3702d9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { + EuiComboBox, + EuiComboBoxOptionProps, + EuiDescribedFormGroup, + EuiFormRow, +} from '@elastic/eui'; + +import { Field, SplitField } from '../../../../../../../../common/types/fields'; + +interface Props { + fields: Field[]; + changeHandler(i: string[]): void; + selectedInfluencers: string[]; + splitField: SplitField; +} + +export const InfluencersSelect: FC = ({ + fields, + changeHandler, + selectedInfluencers, + splitField, +}) => { + const options: EuiComboBoxOptionProps[] = fields.map(f => ({ + label: f.name, + })); + const selection: EuiComboBoxOptionProps[] = selectedInfluencers.map(i => ({ label: i })); + + function onChange(selectedOptions: EuiComboBoxOptionProps[]) { + changeHandler(selectedOptions.map(o => o.label)); + } + + return ( + Influencers} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/job_progress/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/job_progress/index.ts new file mode 100644 index 0000000000000..648e1da5ba1f1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/job_progress/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { JobProgress } from './job_progress'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/job_progress/job_progress.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/job_progress/job_progress.tsx new file mode 100644 index 0000000000000..d270f37ab48f1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/job_progress/job_progress.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiProgress } from '@elastic/eui'; + +interface Props { + progress: number; +} + +export const JobProgress: FC = ({ progress }) => { + if (progress > 0 && progress < 100) { + return ; + } else { + return null; + } +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/index.ts new file mode 100644 index 0000000000000..42af66e54af0d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MultiMetricView } from './multi_metric_view'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx new file mode 100644 index 0000000000000..fd054eb8ac106 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -0,0 +1,258 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { MultiMetricJobCreator, isMultiMetricJobCreator } from '../../../../../common/job_creator'; +import { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { DropDownLabel, DropDownProps } from '../agg_select'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; +import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; +import { defaultChartSettings, ChartSettings } from '../../../charts/common/settings'; +import { MetricSelector } from './metric_selector'; +import { DetectorTitle } from '../detector_title'; +import { JobProgress } from '../job_progress'; +import { SplitCards } from '../split_cards'; +import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; + +interface Props { + isActive: boolean; + setIsValid: (na: boolean) => void; +} + +export const MultiMetricDetectors: FC = ({ isActive, setIsValid }) => { + const { + jobCreator: jc, + jobCreatorUpdate, + jobCreatorUpdated, + chartLoader, + chartInterval, + resultsLoader, + } = useContext(JobCreatorContext); + + if (isMultiMetricJobCreator(jc) === false) { + return ; + } + const jobCreator = jc as MultiMetricJobCreator; + + const { fields } = newJobCapsService; + const [selectedOptions, setSelectedOptions] = useState([{ label: '' }]); + const [aggFieldPairList, setAggFieldPairList] = useState([]); + const [lineChartsData, setLineChartsData] = useState({}); + const [modelData, setModelData] = useState>([]); + const [anomalyData, setAnomalyData] = useState>([]); + const [start, setStart] = useState(jobCreator.start); + const [end, setEnd] = useState(jobCreator.end); + const [progress, setProgress] = useState(resultsLoader.progress); + const [chartSettings, setChartSettings] = useState(defaultChartSettings); + const [splitField, setSplitField] = useState(jobCreator.splitField); + const [fieldValues, setFieldValues] = useState([]); + + function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { + addDetector(selectedOptionsIn); + } + + function addDetector(selectedOptionsIn: DropDownLabel[]) { + if (selectedOptionsIn !== null && selectedOptionsIn.length) { + const option = selectedOptionsIn[0] as DropDownLabel; + if (typeof option !== 'undefined') { + const newPair = { agg: option.agg, field: option.field }; + setAggFieldPairList([...aggFieldPairList, newPair]); + setSelectedOptions([{ label: '' }]); + } else { + setAggFieldPairList([]); + } + } + } + + function deleteDetector(index: number) { + aggFieldPairList.splice(index, 1); + setAggFieldPairList([...aggFieldPairList]); + } + + function setResultsWrapper(results: Results) { + setModelData(results.model); + setAnomalyData(results.anomalies); + } + + // subscribe to progress + useEffect(() => { + jobCreator.subscribeToProgress(setProgress); + }, []); + + // subscribe to results + useEffect(() => { + resultsLoader.subscribeToResults(setResultsWrapper); + }, []); + + // watch for changes in detector list length + useEffect(() => { + jobCreator.removeAllDetectors(); + aggFieldPairList.forEach(pair => { + jobCreator.addDetector(pair.agg, pair.field); + }); + jobCreatorUpdate(); + loadCharts(); + setIsValid(aggFieldPairList.length > 0); + }, [aggFieldPairList.length]); + + // watch for change in jobCreator + useEffect(() => { + if (jobCreator.start !== start || jobCreator.end !== end) { + setStart(jobCreator.start); + setEnd(jobCreator.end); + loadCharts(); + } + setSplitField(jobCreator.splitField); + }, [jobCreatorUpdated]); + + // watch for changes in split field. + // load example field values + // changes to fieldValues here will trigger the card effect + useEffect(() => { + if (splitField !== null) { + chartLoader + .loadFieldExampleValues(splitField) + .then(setFieldValues) + .catch(() => {}); + } else { + setFieldValues([]); + } + }, [splitField]); + + // watch for changes in the split field values + // reload the charts + useEffect(() => { + loadCharts(); + }, [fieldValues]); + + function getChartSettings(): ChartSettings { + const cs = { + ...defaultChartSettings, + intervalMs: chartInterval.getInterval().asMilliseconds(), + }; + if (aggFieldPairList.length > 2) { + cs.cols = 3; + cs.height = '150px'; + cs.intervalMs = cs.intervalMs * 3; + } else if (aggFieldPairList.length > 1) { + cs.cols = 2; + cs.height = '200px'; + cs.intervalMs = cs.intervalMs * 2; + } + return cs; + } + + async function loadCharts() { + const cs = getChartSettings(); + setChartSettings(cs); + + if (aggFieldPairList.length > 0) { + const resp: LineChartData = await chartLoader.loadLineCharts( + jobCreator.start, + jobCreator.end, + aggFieldPairList, + jobCreator.splitField, + fieldValues.length > 0 ? fieldValues[0] : null, + cs.intervalMs + ); + + setLineChartsData(resp); + } + } + + return ( + + {lineChartsData && ( + + )} + {isActive && ( + + )} + {isActive === false && ( + {lineChartsData && } + )} + + ); +}; + +interface ChartGridProps { + aggFieldPairList: AggFieldPair[]; + chartSettings: ChartSettings; + splitField: SplitField; + fieldValues: string[]; + lineChartsData: LineChartData; + modelData: Record; + anomalyData: Record; + deleteDetector?: (index: number) => void; + jobType: JOB_TYPE; +} + +const ChartGrid: FC = ({ + aggFieldPairList, + chartSettings, + splitField, + fieldValues, + lineChartsData, + modelData, + anomalyData, + deleteDetector, + jobType, +}) => { + return ( + + + {aggFieldPairList.map((af, i) => ( + + {lineChartsData[i] !== undefined && ( + + + + + )} + + ))} + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx new file mode 100644 index 0000000000000..6b56172e4100a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; + +interface Props { + fields: Field[]; + detectorChangeHandler: (options: DropDownLabel[]) => void; + selectedOptions: DropDownProps; + maxWidth: number; + removeOptions: AggFieldPair[]; +} + +export const MetricSelector: FC = ({ + fields, + detectorChangeHandler, + selectedOptions, + maxWidth, + removeOptions, +}) => { + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx new file mode 100644 index 0000000000000..a33d986782ae1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useEffect, useState } from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; + +import { MultiMetricDetectors } from './metric_selection'; +import { MultiMetricSettings } from './settings'; + +interface Props { + isActive: boolean; + setCanProceed: (proceed: boolean) => void; +} + +export const MultiMetricView: FC = ({ isActive, setCanProceed }) => { + const [metricsValid, setMetricValid] = useState(false); + const [settingsValid, setSettingsValid] = useState(false); + + useEffect(() => { + setCanProceed(metricsValid && settingsValid); + }, [metricsValid, settingsValid]); + + return ( + + + {metricsValid && isActive && ( + + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx new file mode 100644 index 0000000000000..03cd9b2dee232 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { BucketSpan } from '../bucket_span'; +import { SplitFieldSelector } from '../split_field'; +import { Influencers } from '../influencers'; + +interface Props { + isActive: boolean; + setIsValid: (proceed: boolean) => void; +} + +export const MultiMetricSettings: FC = ({ isActive, setIsValid }) => { + const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const [bucketSpan, setBucketSpan] = useState(jobCreator.bucketSpan); + + useEffect(() => { + jobCreator.bucketSpan = bucketSpan; + jobCreatorUpdate(); + setIsValid(bucketSpan !== ''); + }, [bucketSpan]); + + useEffect(() => { + setBucketSpan(jobCreator.bucketSpan); + }, [jobCreatorUpdated]); + + return ( + + {isActive && ( + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/index.ts new file mode 100644 index 0000000000000..2397767fe4650 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PopulationView } from './population_view'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx new file mode 100644 index 0000000000000..41eaec397f5bd --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -0,0 +1,336 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useEffect, useState, useReducer } from 'react'; +import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { PopulationJobCreator, isPopulationJobCreator } from '../../../../../common/job_creator'; +import { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { DropDownLabel, DropDownProps } from '../agg_select'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { Field, AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; +import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; +import { defaultChartSettings, ChartSettings } from '../../../charts/common/settings'; +import { MetricSelector } from './metric_selector'; +import { DetectorTitle } from '../detector_title'; +import { JobProgress } from '../job_progress'; +import { SplitCards } from '../split_cards'; +import { SplitFieldSelector, ByFieldSelector } from '../split_field'; +import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { MlTimeBuckets } from '../../../../../../../util/ml_time_buckets'; + +interface Props { + isActive: boolean; + setIsValid: (na: boolean) => void; +} + +type DetectorFieldValues = Record; + +export const PopulationDetectors: FC = ({ isActive, setIsValid }) => { + const { + jobCreator: jc, + jobCreatorUpdate, + jobCreatorUpdated, + chartLoader, + chartInterval, + resultsLoader, + } = useContext(JobCreatorContext); + + if (isPopulationJobCreator(jc) === false) { + return ; + } + const jobCreator = jc as PopulationJobCreator; + + const { fields } = newJobCapsService; + const [selectedOptions, setSelectedOptions] = useState([{ label: '' }]); + const [aggFieldPairList, setAggFieldPairList] = useState([]); + const [lineChartsData, setLineChartsData] = useState({}); + const [modelData, setModelData] = useState>([]); + const [anomalyData, setAnomalyData] = useState>([]); + const [start, setStart] = useState(jobCreator.start); + const [end, setEnd] = useState(jobCreator.end); + const [progress, setProgress] = useState(resultsLoader.progress); + const [chartSettings, setChartSettings] = useState(defaultChartSettings); + const [splitField, setSplitField] = useState(jobCreator.splitField); + const [fieldValuesPerDetector, setFieldValuesPerDetector] = useState({}); + const [byFieldsUpdated, setByFieldsUpdated] = useReducer<(s: number) => number>(s => s + 1, 0); + + function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { + addDetector(selectedOptionsIn); + } + + function addDetector(selectedOptionsIn: DropDownLabel[]) { + if (selectedOptionsIn !== null && selectedOptionsIn.length) { + const option = selectedOptionsIn[0] as DropDownLabel; + if (typeof option !== 'undefined') { + const newPair = { agg: option.agg, field: option.field, by: { field: null, value: null } }; + setAggFieldPairList([...aggFieldPairList, newPair]); + setSelectedOptions([{ label: '' }]); + } else { + setAggFieldPairList([]); + } + } + } + + function deleteDetector(index: number) { + aggFieldPairList.splice(index, 1); + setAggFieldPairList([...aggFieldPairList]); + } + + function setResultsWrapper(results: Results) { + setModelData(results.model); + setAnomalyData(results.anomalies); + } + + // subscribe to progress + useEffect(() => { + jobCreator.subscribeToProgress(setProgress); + }, []); + + // subscribe to results + useEffect(() => { + resultsLoader.subscribeToResults(setResultsWrapper); + }, []); + + // watch for changes in detector list length + useEffect(() => { + jobCreator.removeAllDetectors(); + aggFieldPairList.forEach((pair, i) => { + jobCreator.addDetector(pair.agg, pair.field); + if (pair.by !== undefined) { + // re-add by fields + jobCreator.setByField(pair.by.field, i); + } + }); + jobCreatorUpdate(); + loadCharts(); + setIsValid(aggFieldPairList.length > 0); + }, [aggFieldPairList.length]); + + // watch for changes in by field values + // redraw the charts if they change. + // triggered when example fields have been loaded + // if the split field or by fields have changed + useEffect(() => { + loadCharts(); + }, [JSON.stringify(fieldValuesPerDetector)]); + + // watch for change in jobCreator + useEffect(() => { + if (jobCreator.start !== start || jobCreator.end !== end) { + setStart(jobCreator.start); + setEnd(jobCreator.end); + loadCharts(); + } + setSplitField(jobCreator.splitField); + + // update by fields and their by fields + let update = false; + const newList = [...aggFieldPairList]; + newList.forEach((pair, i) => { + const bf = jobCreator.getByField(i); + if (pair.by !== undefined && pair.by.field !== bf) { + pair.by.field = bf; + update = true; + } + }); + if (update) { + setAggFieldPairList(newList); + setByFieldsUpdated(0); + } + }, [jobCreatorUpdated]); + + // watch for changes in split field or by fields. + // load example field values + // changes to fieldValues here will trigger the card effect via setFieldValuesPerDetector + useEffect(() => { + loadFieldExamples(); + }, [splitField, byFieldsUpdated]); + + function getChartSettings(): ChartSettings { + const interval = new MlTimeBuckets(); + interval.setInterval('auto'); + interval.setBounds(chartInterval.getBounds()); + + const cs = { + ...defaultChartSettings, + intervalMs: interval.getInterval().asMilliseconds(), + }; + if (aggFieldPairList.length > 2) { + cs.cols = 3; + cs.height = '150px'; + cs.intervalMs = cs.intervalMs * 3; + } else if (aggFieldPairList.length > 1) { + cs.cols = 2; + cs.height = '200px'; + cs.intervalMs = cs.intervalMs * 2; + } + return cs; + } + + async function loadCharts() { + const cs = getChartSettings(); + setChartSettings(cs); + + if (aggFieldPairList.length > 0) { + const resp: LineChartData = await chartLoader.loadPopulationCharts( + jobCreator.start, + jobCreator.end, + aggFieldPairList, + jobCreator.splitField, + cs.intervalMs + ); + + setLineChartsData(resp); + } + } + + async function loadFieldExamples() { + const promises: any[] = []; + aggFieldPairList.forEach((af, i) => { + if (af.by !== undefined && af.by.field !== null) { + promises.push( + (async (index: number, field: Field) => { + return { + index, + fields: await chartLoader.loadFieldExampleValues(field), + }; + })(i, af.by.field) + ); + } + }); + const results = await Promise.all(promises); + const fieldValues = results.reduce((p, c) => { + p[c.index] = c.fields; + return p; + }, {}) as DetectorFieldValues; + + const newPairs = aggFieldPairList.map((pair, i) => ({ + ...pair, + ...(pair.by === undefined || pair.by.field === null + ? {} + : { + by: { + ...pair.by, + value: fieldValues[i][0], + }, + }), + })); + setAggFieldPairList([...newPairs]); + setFieldValuesPerDetector(fieldValues); + } + + return ( + + {isActive === true && ( + + + {splitField !== null && } + + )} + + {isActive === false && splitField === null && ( + + Population label TODO + {splitField !== null && } + + )} + + {lineChartsData && splitField !== null && ( + + )} + {isActive === true && splitField !== null && ( + + )} + {isActive === false && ( + {lineChartsData && } + )} + + ); +}; + +interface ChartGridProps { + aggFieldPairList: AggFieldPair[]; + chartSettings: ChartSettings; + splitField: SplitField; + lineChartsData: LineChartData; + modelData: Record; + anomalyData: Record; + deleteDetector?: (index: number) => void; + jobType: JOB_TYPE; + fieldValuesPerDetector: DetectorFieldValues; +} + +const ChartGrid: FC = ({ + aggFieldPairList, + chartSettings, + splitField, + lineChartsData, + modelData, + anomalyData, + deleteDetector, + jobType, + fieldValuesPerDetector, +}) => { + return ( + + {aggFieldPairList.map((af, i) => ( + + {lineChartsData[i] !== undefined && ( + + + + + + + {deleteDetector !== undefined && } + + + + + + + )} + + ))} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selector.tsx new file mode 100644 index 0000000000000..6b56172e4100a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selector.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; + +interface Props { + fields: Field[]; + detectorChangeHandler: (options: DropDownLabel[]) => void; + selectedOptions: DropDownProps; + maxWidth: number; + removeOptions: AggFieldPair[]; +} + +export const MetricSelector: FC = ({ + fields, + detectorChangeHandler, + selectedOptions, + maxWidth, + removeOptions, +}) => { + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/population_view.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/population_view.tsx new file mode 100644 index 0000000000000..a6dacc7abda17 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/population_view.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useEffect, useState } from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; + +import { PopulationDetectors } from './metric_selection'; +import { PopulationSettings } from './settings'; + +interface Props { + isActive: boolean; + setCanProceed: (proceed: boolean) => void; +} + +export const PopulationView: FC = ({ isActive, setCanProceed }) => { + const [metricsValid, setMetricValid] = useState(false); + const [settingsValid, setSettingsValid] = useState(false); + + useEffect(() => { + setCanProceed(metricsValid && settingsValid); + }, [metricsValid, settingsValid]); + + return ( + + + {metricsValid && isActive && ( + + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/settings.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/settings.tsx new file mode 100644 index 0000000000000..ee010f89c94a2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/settings.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { BucketSpan } from '../bucket_span'; +import { Influencers } from '../influencers'; + +interface Props { + isActive: boolean; + setIsValid: (proceed: boolean) => void; +} + +export const PopulationSettings: FC = ({ isActive, setIsValid }) => { + const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const [bucketSpan, setBucketSpan] = useState(jobCreator.bucketSpan); + + useEffect(() => { + jobCreator.bucketSpan = bucketSpan; + jobCreatorUpdate(); + setIsValid(bucketSpan !== ''); + }, [bucketSpan]); + + useEffect(() => { + setBucketSpan(jobCreator.bucketSpan); + }, [jobCreatorUpdated]); + + return ( + + {isActive && ( + + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/index.ts new file mode 100644 index 0000000000000..3d45f053cd6e9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SingleMetricView } from './single_metric_view'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx new file mode 100644 index 0000000000000..e3f68450efc7d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { + SingleMetricJobCreator, + isSingleMetricJobCreator, +} from '../../../../../common/job_creator'; +import { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { AggFieldPair } from '../../../../../../../../common/types/fields'; +import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; +import { JobProgress } from '../job_progress'; + +interface Props { + isActive: boolean; + setIsValid: (na: boolean) => void; +} + +const DTR_IDX = 0; + +export const SingleMetricDetectors: FC = ({ isActive, setIsValid }) => { + const { + jobCreator: jc, + jobCreatorUpdate, + jobCreatorUpdated, + chartLoader, + chartInterval, + resultsLoader, + } = useContext(JobCreatorContext); + + if (isSingleMetricJobCreator(jc) === false) { + return ; + } + const jobCreator = jc as SingleMetricJobCreator; + + const { fields } = newJobCapsService; + const [selectedOptions, setSelectedOptions] = useState([{ label: '' }]); + const [aggFieldPair, setAggFieldPair] = useState(null); + const [lineChartsData, setLineChartData] = useState([]); + const [modelData, setModelData] = useState([]); + const [anomalyData, setAnomalyData] = useState([]); + const [start, setStart] = useState(jobCreator.start); + const [end, setEnd] = useState(jobCreator.end); + const [progress, setProgress] = useState(resultsLoader.progress); + + function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { + setSelectedOptions(selectedOptionsIn); + if (selectedOptionsIn.length) { + const option = selectedOptionsIn[0]; + if (typeof option !== 'undefined') { + setAggFieldPair({ agg: option.agg, field: option.field }); + } else { + setAggFieldPair(null); + } + } + } + + function setResultsWrapper(results: Results) { + const model = results.model[DTR_IDX]; + if (model !== undefined) { + setModelData(model); + } + const anomalies = results.anomalies[DTR_IDX]; + if (anomalies !== undefined) { + setAnomalyData(anomalies); + } + } + + // subscribe to progress + useEffect(() => { + jobCreator.subscribeToProgress(setProgress); + }, []); + + // subscribe to results + useEffect(() => { + resultsLoader.subscribeToResults(setResultsWrapper); + }, []); + + useEffect(() => { + if (aggFieldPair !== null) { + jobCreator.setDetector(aggFieldPair.agg, aggFieldPair.field); + jobCreatorUpdate(); + loadChart(); + setIsValid(aggFieldPair !== null); + } + }, [aggFieldPair]); + + useEffect(() => { + if (jobCreator.start !== start || jobCreator.end !== end) { + setStart(jobCreator.start); + setEnd(jobCreator.end); + loadChart(); + } + }, [jobCreatorUpdated]); + + async function loadChart() { + if (aggFieldPair !== null) { + const resp: LineChartData = await chartLoader.loadLineCharts( + jobCreator.start, + jobCreator.end, + [aggFieldPair], + null, + null, + chartInterval.getInterval().asMilliseconds() + ); + if (resp[DTR_IDX] !== undefined) { + setLineChartData(resp); + } + } + } + + return ( + + {isActive && ( + + + {lineChartsData[DTR_IDX] !== undefined && ( + + )} + + )} + {isActive === false && ( + + {lineChartsData[DTR_IDX] !== undefined && ( + + + + + )} + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/settings.tsx new file mode 100644 index 0000000000000..145d492018503 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/settings.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { BucketSpan } from '../bucket_span'; + +interface Props { + isActive: boolean; + setIsValid: (proceed: boolean) => void; +} + +export const SingleMetricSettings: FC = ({ isActive, setIsValid }) => { + const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const [bucketSpan, setBucketSpan] = useState(jobCreator.bucketSpan); + + useEffect(() => { + jobCreator.bucketSpan = bucketSpan; + jobCreatorUpdate(); + setIsValid(bucketSpan !== ''); + }, [bucketSpan]); + + useEffect(() => { + setBucketSpan(jobCreator.bucketSpan); + }, [jobCreatorUpdated]); + + return {isActive && }; +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx new file mode 100644 index 0000000000000..0057928c656eb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useEffect, useState } from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; + +import { SingleMetricDetectors } from './metric_selection'; +import { SingleMetricSettings } from './settings'; + +interface Props { + isActive: boolean; + setCanProceed: (proceed: boolean) => void; +} + +export const SingleMetricView: FC = ({ isActive, setCanProceed }) => { + const [metricsValid, setMetricValid] = useState(false); + const [settingsValid, setSettingsValid] = useState(false); + + useEffect(() => { + setCanProceed(metricsValid && settingsValid); + }, [metricsValid, settingsValid]); + + return ( + + + {metricsValid && isActive && ( + + + + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/index.ts new file mode 100644 index 0000000000000..11c60fd18ff07 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SplitCards } from './split_cards'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx new file mode 100644 index 0000000000000..7456f595b31cb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, ReactElement, memo, Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; + +import { SplitField } from '../../../../../../../../common/types/fields'; +import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; + +interface Props { + fieldValues: string[]; + splitField: SplitField; + numberOfDetectors: number; + children: ReactElement; + jobType: JOB_TYPE; +} + +interface Panel { + panel: HTMLDivElement; + marginBottom: number; +} + +export const SplitCards: FC = memo( + ({ fieldValues, splitField, children, numberOfDetectors, jobType }) => { + const panels: Panel[] = []; + + function storePanels(panel: HTMLDivElement | null, marginBottom: number) { + if (panel !== null) { + panels.push({ panel, marginBottom }); + } + } + + function getBackPanels() { + panels.length = 0; + + const fieldValuesCopy = [...fieldValues]; + fieldValuesCopy.shift(); + + let margin = 5; + const sideMargins = fieldValuesCopy.map((f, i) => (margin += 10 - i)).reverse(); + + setTimeout(() => { + panels.forEach(p => (p.panel.style.marginBottom = `${p.marginBottom}px`)); + }, 100); + + const SPACING = 100; + const SPLIT_HEIGHT_MULTIPLIER = 1.6; + return fieldValuesCopy.map((fieldName, i) => { + const diff = (i + 1) * (SPLIT_HEIGHT_MULTIPLIER * (10 / fieldValuesCopy.length)); + const marginBottom = -SPACING + diff; + + const sideMargin = sideMargins[i]; + + const style = { + height: `${SPACING}px`, + marginBottom: `-${SPACING}px`, + marginLeft: `${sideMargin}px`, + marginRight: `${sideMargin}px`, + transition: 'margin 0.2s', + }; + return ( +
storePanels(ref, marginBottom)} style={style}> + +
{fieldName}
+
+
+ ); + }); + } + + return ( + + + {(fieldValues.length === 0 || numberOfDetectors === 0) && {children}} + {fieldValues.length > 0 && numberOfDetectors > 0 && splitField !== null && ( + + {jobType === JOB_TYPE.MULTI_METRIC && ( + +
Data split by {splitField.name}
+ +
+ )} + + {getBackPanels()} + +
{fieldValues[0]}
+ + {children} +
+
+ )} +
+
+ ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx new file mode 100644 index 0000000000000..1fd714f986719 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; + +import { SplitFieldSelect } from './split_field_select'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { + isMultiMetricJobCreator, + PopulationJobCreator, + isPopulationJobCreator, +} from '../../../../../common/job_creator'; + +interface Props { + detectorIndex: number; +} + +export const ByFieldSelector: FC = ({ detectorIndex }) => { + const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + if (isMultiMetricJobCreator(jc) === false && isPopulationJobCreator(jc) === false) { + return ; + } + const jobCreator = jc as PopulationJobCreator; + + const { categoryFields } = newJobCapsService; + + const [byField, setByField] = useState(jobCreator.getByField(detectorIndex)); + + useEffect(() => { + jobCreator.setByField(byField, detectorIndex); + jobCreatorUpdate(); + }, [byField]); + + useEffect(() => { + const bf = jobCreator.getByField(detectorIndex); + setByField(bf); + }, [jobCreatorUpdated]); + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx new file mode 100644 index 0000000000000..4d87099e8ce3d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, ReactElement, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; + +interface Props { + children: ReactElement; + jobType: JOB_TYPE; +} + +export const Description: FC = ({ children, jobType }) => { + if (jobType === JOB_TYPE.MULTI_METRIC) { + return ( + Split field} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + {children} + + + ); + } else if (jobType === JOB_TYPE.POPULATION) { + return ( + Population} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + {children} + + + ); + } else { + return ; + } +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/index.ts new file mode 100644 index 0000000000000..e5a1f2aebdaa0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { ByFieldSelector } from './by_field'; +export { SplitFieldSelector } from './split_field'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx new file mode 100644 index 0000000000000..8b43714460651 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; + +import { SplitFieldSelect } from './split_field_select'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; +import { Description } from './description'; +import { + MultiMetricJobCreator, + isMultiMetricJobCreator, + PopulationJobCreator, + isPopulationJobCreator, +} from '../../../../../common/job_creator'; + +export const SplitFieldSelector: FC = () => { + const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + if (isMultiMetricJobCreator(jc) === false && isPopulationJobCreator(jc) === false) { + return ; + } + const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator; + + const { categoryFields } = newJobCapsService; + const [splitField, setSplitField] = useState(jobCreator.splitField); + + useEffect(() => { + jobCreator.setSplitField(splitField); + jobCreatorUpdate(); + }, [splitField]); + + useEffect(() => { + setSplitField(jobCreator.splitField); + }, [jobCreatorUpdated]); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field_select.tsx new file mode 100644 index 0000000000000..069edb7eaeafd --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field_select.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; + +import { Field, SplitField } from '../../../../../../../../common/types/fields'; + +interface DropDownLabel { + label: string; + field: Field; +} + +interface Props { + fields: Field[]; + changeHandler(f: SplitField): void; + selectedField: SplitField; +} + +export const SplitFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { + const options: EuiComboBoxOptionProps[] = fields.map( + f => + ({ + label: f.name, + field: f, + } as DropDownLabel) + ); + + const selection: EuiComboBoxOptionProps[] = []; + if (selectedField !== null) { + selection.push({ label: selectedField.name, field: selectedField } as DropDownLabel); + } + + function onChange(selectedOptions: EuiComboBoxOptionProps[]) { + const option = selectedOptions[0] as DropDownLabel; + if (typeof option !== 'undefined') { + changeHandler(option.field); + } else { + changeHandler(null); + } + } + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/index.ts new file mode 100644 index 0000000000000..3cd2e15c10bd7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PickFieldsStep } from './pick_fields'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/pick_fields.tsx new file mode 100644 index 0000000000000..1578a4e51e736 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/pick_fields.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; + +import { JobCreatorContext } from '../job_creator_context'; +import { WizardNav } from '../../../../../data_frame/components/wizard_nav'; +import { WIZARD_STEPS, StepProps } from '../step_types'; +import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { SingleMetricView } from './components/single_metric_view'; +import { MultiMetricView } from './components/multi_metric_view'; +import { PopulationView } from './components/population_view'; + +export const PickFieldsStep: FC = ({ setCurrentStep, isCurrentStep }) => { + const { jobCreator, jobCreatorUpdated } = useContext(JobCreatorContext); + const [nextActive, setNextActive] = useState(false); + const [jobType, setJobType] = useState(jobCreator.type); + + // this shouldn't really change, but just in case we need to... + useEffect(() => { + setJobType(jobCreator.type); + }, [jobCreatorUpdated]); + + return ( + + {jobType === JOB_TYPE.SINGLE_METRIC && ( + + )} + {jobType === JOB_TYPE.MULTI_METRIC && ( + + )} + {jobType === JOB_TYPE.POPULATION && ( + + )} + + {isCurrentStep && ( + setCurrentStep(WIZARD_STEPS.TIME_RANGE)} + next={() => setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} + nextActive={nextActive} + /> + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/step_types.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/step_types.ts new file mode 100644 index 0000000000000..b25d8b23b0bb3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/step_types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum WIZARD_STEPS { + TIME_RANGE, + PICK_FIELDS, + JOB_DETAILS, + SUMMARY, +} + +export interface StepProps { + isCurrentStep: boolean; + setCurrentStep: React.Dispatch>; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/index.ts similarity index 85% rename from x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/index.ts rename to x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/index.ts index f58978bef5c6a..f0f441d48afcb 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/index.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './default_configs'; +export { SummaryStep } from './summary'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/summary.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/summary.tsx new file mode 100644 index 0000000000000..d6cdccbc87c00 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/summary.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useState, useEffect } from 'react'; +import { EuiButton, EuiHorizontalRule } from '@elastic/eui'; +import { WizardNav } from '../../../../../data_frame/components/wizard_nav'; +import { WIZARD_STEPS, StepProps } from '../step_types'; +import { JobCreatorContext } from '../job_creator_context'; +import { KibanaContext, isKibanaContext } from '../../../../../data_frame/common/kibana_context'; +import { mlJobService } from '../../../../../services/job_service'; + +export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => { + const kibanaContext = useContext(KibanaContext); + if (!isKibanaContext(kibanaContext)) { + return null; + } + + const { jobCreator } = useContext(JobCreatorContext); + const [progress, setProgress] = useState(0); + + function setProgressWrapper(p: number) { + setProgress(p); + } + + useEffect(() => { + jobCreator.subscribeToProgress(setProgressWrapper); + }, []); + + function start() { + jobCreator.createAndStartJob(); + } + + function viewResults() { + const url = mlJobService.createResultsUrl( + [jobCreator.jobId], + jobCreator.start, + jobCreator.end, + 'timeseriesexplorer' + ); + window.open(url, '_blank'); + } + + return ( + + {isCurrentStep && ( + + + {jobCreator.jobId} +
+ {jobCreator.start} : {jobCreator.end} +
+ {JSON.stringify(jobCreator.detectors, null, 2)} +
+ {jobCreator.bucketSpan} +
+ {progress === 0 && ( + setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} /> + )} + + {progress < 100 && ( + 0}> + Create job + + )} + {progress === 100 && ( + + View results + + )} +
+ )} + {isCurrentStep === false && ( + + {jobCreator.jobId} +
+ {jobCreator.start} : {jobCreator.end} +
+ {JSON.stringify(jobCreator.detectors, null, 2)} +
+ {jobCreator.bucketSpan} +
+ )} +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/index.ts new file mode 100644 index 0000000000000..05ce3a68a1b6c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TimeRangeStep } from './time_range'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx new file mode 100644 index 0000000000000..05d19f14c0212 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useContext, useState, useEffect } from 'react'; +import { timefilter } from 'ui/timefilter'; +import moment from 'moment'; +import { WizardNav } from '../../../../../data_frame/components/wizard_nav'; +import { WIZARD_STEPS, StepProps } from '../step_types'; +import { JobCreatorContext } from '../job_creator_context'; +import { KibanaContext, isKibanaContext } from '../../../../../data_frame/common/kibana_context'; +import { + FullTimeRangeSelector, + getTimeFilterRange, +} from '../../../../../components/full_time_range_selector'; +import { EventRateChart } from '../charts/event_rate_chart'; +import { LineChartPoint } from '../../../common/chart_loader'; + +export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) => { + const kibanaContext = useContext(KibanaContext); + if (!isKibanaContext(kibanaContext)) { + return null; + } + + const { + jobCreator, + jobCreatorUpdate, + jobCreatorUpdated, + chartLoader, + chartInterval, + } = useContext(JobCreatorContext); + + const [start, setStart] = useState(jobCreator.start); + const [end, setEnd] = useState(jobCreator.end); + const [eventRateChartData, setEventRateChartData] = useState([]); + + async function loadChart() { + const resp = await chartLoader.loadEventRateChart( + jobCreator.start, + jobCreator.end, + chartInterval.getInterval().asMilliseconds() + ); + setEventRateChartData(resp); + } + + useEffect(() => { + jobCreator.setTimeRange(start, end); + chartInterval.setBounds({ + min: moment(start), + max: moment(end), + }); + jobCreatorUpdate(); + loadChart(); + }, [start, end]); + + useEffect(() => { + setStart(jobCreator.start); + setEnd(jobCreator.end); + }, [jobCreatorUpdated]); + + const timefilterChange = () => { + const { to, from } = getTimeFilterRange(); + if (to >= from) { + setStart(from); + setEnd(to); + } + }; + + useEffect(() => { + timefilter.on('timeUpdate', timefilterChange); + return () => { + timefilter.off('timeUpdate', timefilterChange); + }; + }, []); + + return ( + +
+ +
+
+ +
+ + {isCurrentStep && ( + + + setCurrentStep(WIZARD_STEPS.PICK_FIELDS)} nextActive={true} /> + + )} +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx new file mode 100644 index 0000000000000..14616be6e0c64 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); +import { timefilter } from 'ui/timefilter'; + +import { I18nContext } from 'ui/i18n'; +import { IPrivate } from 'ui/private'; +import { InjectorService } from '../../../../../common/types/angular'; + +import { SearchItemsProvider } from '../../../new_job/utils/new_job_utils'; +import { Page, PageProps } from './page'; +import { JOB_TYPE } from '../../common/job_creator/util/constants'; + +import { KibanaContext } from '../../../../data_frame/common/kibana_context'; + +module.directive('mlNewJobPage', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + timefilter.enableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + const indexPatterns = $injector.get('indexPatterns'); + const kbnBaseUrl = $injector.get('kbnBaseUrl'); + const kibanaConfig = $injector.get('config'); + const Private: IPrivate = $injector.get('Private'); + const $route = $injector.get('$route'); + const existingJobsAndGroups = $route.current.locals.existingJobsAndGroups; + + if ($route.current.locals.jobType === undefined) { + return; + } + const jobType: JOB_TYPE = $route.current.locals.jobType; + + const createSearchItems = Private(SearchItemsProvider); + const { indexPattern, savedSearch, combinedQuery } = createSearchItems(); + + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kbnBaseUrl, + kibanaConfig, + }; + + const props: PageProps = { + existingJobsAndGroups, + jobType, + }; + + ReactDOM.render( + + + {React.createElement(Page, props)} + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/page.tsx new file mode 100644 index 0000000000000..4352df146205c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/page.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext, useEffect, Fragment } from 'react'; + +import { EuiPage, EuiPageBody, EuiPageContentBody, EuiSpacer } from '@elastic/eui'; +import { Wizard } from './wizard'; +import { jobCreatorFactory } from '../../common/job_creator'; +import { JOB_TYPE } from '../../common/job_creator/util/constants'; +import { ChartLoader } from '../../common/chart_loader'; +import { ResultsLoader } from '../../common/results_loader'; +import { KibanaContext, isKibanaContext } from '../../../../data_frame/common/kibana_context'; +import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; +import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; + +const PAGE_WIDTH = 1200; // document.querySelector('.single-metric-job-container').width(); +const BAR_TARGET = PAGE_WIDTH > 2000 ? 1000 : PAGE_WIDTH / 2; +const MAX_BARS = BAR_TARGET + (BAR_TARGET / 100) * 100; // 100% larger than bar target + +export interface PageProps { + existingJobsAndGroups: any; + jobType: JOB_TYPE; +} + +export const Page: FC = ({ existingJobsAndGroups, jobType }) => { + const kibanaContext = useContext(KibanaContext); + if (!isKibanaContext(kibanaContext)) { + return null; + } + + const jobCreator = jobCreatorFactory(jobType)( + kibanaContext.currentIndexPattern, + kibanaContext.currentSavedSearch, + kibanaContext.combinedQuery + ); + jobCreator.bucketSpan = '15m'; + const { from, to } = getTimeFilterRange(); + jobCreator.setTimeRange(from, to); + + if (jobType === JOB_TYPE.SINGLE_METRIC) { + jobCreator.modelPlot = true; + } + + const chartInterval = new MlTimeBuckets(); + chartInterval.setBarTarget(BAR_TARGET); + chartInterval.setMaxBars(MAX_BARS); + chartInterval.setInterval('auto'); + + const chartLoader = new ChartLoader( + kibanaContext.currentIndexPattern, + kibanaContext.currentSavedSearch, + kibanaContext.combinedQuery + ); + + const resultsLoader = new ResultsLoader(jobCreator, chartInterval, chartLoader); + + useEffect(() => { + return () => { + jobCreator.forceStopRefreshPolls(); + }; + }); + + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts new file mode 100644 index 0000000000000..10502541469f8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uiRoutes from 'ui/routes'; + +// @ts-ignore +import { checkFullLicense } from '../../../../license/check_license'; +// @ts-ignore +import { checkGetJobsPrivilege } from '../../../../privilege/check_privilege'; +// @ts-ignore +import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; + +import { + getCreateSingleMetricJobBreadcrumbs, + getCreateMultiMetricJobBreadcrumbs, + getCreatePopulationJobBreadcrumbs, + // @ts-ignore +} from '../../../breadcrumbs'; + +import { loadNewJobCapabilities } from '../../../../services/new_job_capabilities_service'; + +import { mlJobService } from '../../../../services/job_service'; +import { JOB_TYPE } from '../../common/job_creator/util/constants'; + +const template = ``; + +interface Route { + id: JOB_TYPE; + k7Breadcrumbs: () => any; +} + +const routes: Route[] = [ + { + id: JOB_TYPE.SINGLE_METRIC, + k7Breadcrumbs: getCreateSingleMetricJobBreadcrumbs, + }, + { + id: JOB_TYPE.MULTI_METRIC, + k7Breadcrumbs: getCreateMultiMetricJobBreadcrumbs, + }, + { + id: JOB_TYPE.POPULATION, + k7Breadcrumbs: getCreatePopulationJobBreadcrumbs, + }, +]; + +routes.forEach((route: Route) => { + uiRoutes.when(`/jobs/new_job/new_new_job/${route.id}`, { + template, + k7Breadcrumbs: route.k7Breadcrumbs, + resolve: { + CheckLicense: checkFullLicense, + privileges: checkGetJobsPrivilege, + indexPattern: loadCurrentIndexPattern, + savedSearch: loadCurrentSavedSearch, + loadNewJobCapabilities, + existingJobsAndGroups: mlJobService.getJobAndGroupIds, + jobType: () => route.id, + }, + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx new file mode 100644 index 0000000000000..11a4e83b757b3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext, useReducer, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiSteps, EuiStepStatus } from '@elastic/eui'; +import { WIZARD_STEPS } from '../components/step_types'; + +import { TimeRangeStep } from '../components/time_range_step'; + +import { PickFieldsStep } from '../components/pick_fields_step'; +import { JobDetailsStep } from '../components/job_details_step'; +import { SummaryStep } from '../components/summary_step'; +import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; + +import { + JobCreatorContext, + JobCreatorContextValue, + ExistingJobsAndGroups, +} from '../components/job_creator_context'; +import { KibanaContext, isKibanaContext } from '../../../../data_frame/common/kibana_context'; + +import { + SingleMetricJobCreator, + MultiMetricJobCreator, + PopulationJobCreator, +} from '../../common/job_creator'; +import { ChartLoader } from '../../common/chart_loader'; +import { ResultsLoader } from '../../common/results_loader'; +import { newJobCapsService } from '../../../../services/new_job_capabilities_service'; + +interface Props { + jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator; + chartLoader: ChartLoader; + resultsLoader: ResultsLoader; + chartInterval: MlTimeBuckets; + existingJobsAndGroups: ExistingJobsAndGroups; +} + +export const Wizard: FC = ({ + jobCreator, + chartLoader, + resultsLoader, + chartInterval, + existingJobsAndGroups, +}) => { + const kibanaContext = useContext(KibanaContext); + if (!isKibanaContext(kibanaContext)) { + return null; + } + + const [jobCreatorUpdated, jobCreatorUpdate] = useReducer<(s: number) => number>(s => s + 1, 0); + + const jobCreatorContext: JobCreatorContextValue = { + jobCreatorUpdated, + jobCreatorUpdate, + jobCreator, + chartLoader, + resultsLoader, + chartInterval, + fields: newJobCapsService.fields, + aggs: newJobCapsService.aggs, + existingJobsAndGroups, + }; + + const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.TIME_RANGE); + + const stepsConfig = [ + { + title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', { + defaultMessage: 'Time range', + }), + children: ( + + ), + status: currentStep >= WIZARD_STEPS.TIME_RANGE ? undefined : ('incomplete' as EuiStepStatus), + }, + { + title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', { + defaultMessage: 'Pick fields', + }), + children: ( + + ), + status: currentStep >= WIZARD_STEPS.PICK_FIELDS ? undefined : ('incomplete' as EuiStepStatus), + }, + + { + title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', { + defaultMessage: 'Job details', + }), + children: ( + + ), + status: currentStep >= WIZARD_STEPS.JOB_DETAILS ? undefined : ('incomplete' as EuiStepStatus), + }, + + { + title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', { + defaultMessage: 'Summary', + }), + children: ( + + ), + status: currentStep >= WIZARD_STEPS.SUMMARY ? undefined : ('incomplete' as EuiStepStatus), + }, + ]; + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/services/job_service.d.ts index 432a8d5ae3274..66cf9cd6a3fa7 100644 --- a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/job_service.d.ts @@ -9,6 +9,8 @@ declare interface JobService { openJob(jobId: string): Promise; saveNewDatafeed(datafeedConfig: any, jobId: string): Promise; startDatafeed(datafeedId: string, jobId: string, start: number, end: number): Promise; + createResultsUrl(jobId: string[], start: number, end: number, location: string): string; + getJobAndGroupIds(): { jobs: string[]; groups: string[] }; } export const mlJobService: JobService; diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.js b/x-pack/legacy/plugins/ml/public/services/job_service.js index 6a9bdc74d2f40..b4fea1017e5d1 100644 --- a/x-pack/legacy/plugins/ml/public/services/job_service.js +++ b/x-pack/legacy/plugins/ml/public/services/job_service.js @@ -739,6 +739,20 @@ class JobService { } + async getJobAndGroupIds() { + const existingJobsAndGroups = { + jobs: [], + groups: [], + }; + try { + const { jobs: tempJobs, groups } = await ml.jobs.getAllJobAndGroupIds(); + existingJobsAndGroups.jobs = tempJobs; + existingJobsAndGroups.groups = groups; + return existingJobsAndGroups; + } catch (error) { + return existingJobsAndGroups; + } + } } // private function used to check the job saving response diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index 1c904eb7cecd3..6d2246f1c43eb 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -6,6 +6,7 @@ import { Annotation } from '../../../common/types/annotations'; import { Privileges } from '../../../common/types/privileges'; +import { DslName, AggFieldNamePair } from '../../../common/types/fields'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -58,6 +59,33 @@ declare interface Ml { jobAuditMessages(jobId: string, from: string): Promise; deletingJobTasks(): Promise; newJobCaps(indexPatternTitle: string, isRollup: boolean): Promise; + newJobLineChart( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: object, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string | null, + splitFieldValue: string | null + ): Promise; + newJobPopulationsChart( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: object, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string + ): Promise; + getAllJobAndGroupIds(): Promise; + getLookBackProgress( + jobId: string, + start: number, + end: number + ): Promise<{ progress: number; isRunning: boolean }>; }; } diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js index f8bc87e1896b4..39b646998b426 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js @@ -134,4 +134,77 @@ export const jobs = { method: 'GET', }); }, + + newJobLineChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue + ) { + return http({ + url: `${basePath}/jobs/new_job_line_chart`, + method: 'POST', + data: { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue + } + }); + }, + + newJobPopulationsChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + ) { + return http({ + url: `${basePath}/jobs/new_job_population_chart`, + method: 'POST', + data: { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + } + }); + }, + + getAllJobAndGroupIds() { + return http({ + url: `${basePath}/jobs/all_jobs_and_group_ids`, + method: 'GET', + }); + }, + + getLookBackProgress(jobId, start, end) { + return http({ + url: `${basePath}/jobs/look_back_progress`, + method: 'POST', + data: { + jobId, + start, + end, + } + }); + }, }; diff --git a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts index 55f0a009cb94d..75265468152cd 100644 --- a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts @@ -6,6 +6,7 @@ import { IndexPatternWithType } from '../../common/types/kibana'; import { Field, Aggregation, AggId, FieldId, NewJobCaps } from '../../common/types/fields'; +import { ES_FIELD_TYPES } from '../../common/constants/field_types'; import { ml } from './ml_api_service'; // called in the angular routing resolve block to initialize the @@ -24,6 +25,8 @@ export function loadNewJobCapabilities(indexPatterns: any, $route: Record categoryFieldTypes.includes(f.type)); + } + public async initializeFromIndexPattern(indexPattern: IndexPatternWithType) { try { const resp = await ml.jobs.newJobCaps(indexPattern.title, indexPattern.type === 'rollup'); diff --git a/x-pack/legacy/plugins/ml/public/services/results_service.d.ts b/x-pack/legacy/plugins/ml/public/services/results_service.d.ts new file mode 100644 index 0000000000000..b30a13ad175cf --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/services/results_service.d.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +declare interface MlResultsService { + getScoresByBucket: ( + jobIds: string[], + earliestMs: number, + latestMs: number, + interval: string | number, + maxResults: number + ) => Promise; + getScheduledEventsByBucket: () => Promise; + getTopInfluencers: () => Promise; + getTopInfluencerValues: () => Promise; + getOverallBucketScores: () => Promise; + getInfluencerValueMaxScoreByTime: () => Promise; + getRecordInfluencers: () => Promise; + getRecordsForInfluencer: () => Promise; + getRecordsForDetector: () => Promise; + getRecords: () => Promise; + getRecordsForCriteria: () => Promise; + getMetricData: () => Promise; + getEventRateData: ( + index: string, + query: any, + timeFieldName: string, + earliestMs: number, + latestMs: number, + interval: string | number + ) => Promise; + getEventDistributionData: () => Promise; + getModelPlotOutput: ( + jobId: string, + detectorIndex: number, + criteriaFields: string[], + earliestMs: number, + latestMs: number, + interval: string | number, + aggType: { + min: string; + max: string; + } + ) => Promise; + getRecordMaxScoreByTime: () => Promise; +} + +export const mlResultsService: MlResultsService; diff --git a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts b/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts new file mode 100644 index 0000000000000..093076f9a09e8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Moment } from 'moment'; + +declare interface TimefilterBounds { + min: Moment; + max: Moment; +} + +export class MlTimeBuckets { + setBarTarget: (barTarget: number) => void; + setMaxBars: (maxBars: number) => void; + setInterval: (interval: string) => void; + setBounds: (bounds: TimefilterBounds) => void; + getBounds: () => { min: any; max: any }; + getInterval: () => { + asMilliseconds: () => number; + }; +} diff --git a/x-pack/legacy/plugins/ml/public/util/string_utils.d.ts b/x-pack/legacy/plugins/ml/public/util/string_utils.d.ts new file mode 100644 index 0000000000000..10b0cbbe9aeb0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/string_utils.d.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function escapeForElasticsearchQuery(str: string): string; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/legacy/plugins/ml/server/models/job_service/index.js index 9cae2ee0a958a..c180a205cb850 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/index.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/index.js @@ -9,6 +9,7 @@ import { datafeedsProvider } from './datafeeds'; import { jobsProvider } from './jobs'; import { groupsProvider } from './groups'; import { newJobCapsProvider } from './new_job_caps'; +import { newJobChartsProvider } from './new_job'; export function jobServiceProvider(callWithRequest, request) { return { @@ -16,5 +17,6 @@ export function jobServiceProvider(callWithRequest, request) { ...jobsProvider(callWithRequest), ...groupsProvider(callWithRequest), ...newJobCapsProvider(callWithRequest, request), + ...newJobChartsProvider(callWithRequest, request), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js index 0e928fce8ca11..5e58c0cb8d1fe 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js @@ -13,6 +13,7 @@ import { resultsServiceProvider } from '../results_service'; import { CalendarManager } from '../calendar'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../common/util/job_utils'; +import { groupsProvider } from './groups'; import { uniq } from 'lodash'; export function jobsProvider(callWithRequest) { @@ -339,6 +340,50 @@ export function jobsProvider(callWithRequest) { return results; } + async function getAllJobAndGroupIds() { + const { getAllGroups } = groupsProvider(callWithRequest); + const jobs = await callWithRequest('ml.jobs'); + const allJobIds = jobs.jobs.map(job => job.job_id); + const groups = await getAllGroups(); + const allGroupIds = groups.map(group => group.id); + + return { + jobs: allJobIds, + groups: allGroupIds, + }; + } + + async function getLookBackProgress(jobId, start, end) { + const datafeedId = `datafeed-${jobId}`; + const [jobStats, isRunning] = await Promise.all([ + callWithRequest('ml.jobStats', { jobId: [jobId] }), + isDatafeedRunning(datafeedId) + ]); + + if (jobStats.jobs.length) { + const time = jobStats.jobs[0].data_counts.latest_record_timestamp; + const progress = (time - start) / (end - start); + return { + progress: (progress > 0 ? Math.round(progress * 100) : 0), + isRunning + }; + } + return { progress: 0, isRunning: false }; + } + + async function isDatafeedRunning(datafeedId) { + const stats = await callWithRequest('ml.datafeedStats', { datafeedId: [datafeedId] }); + if (stats.datafeeds.length) { + const state = stats.datafeeds[0].state; + return ( + state === DATAFEED_STATE.STARTED || + state === DATAFEED_STATE.STARTING || + state === DATAFEED_STATE.STOPPING + ); + } + return false; + } + return { forceDeleteJob, deleteJobs, @@ -348,5 +393,7 @@ export function jobsProvider(callWithRequest) { createFullJobsList, deletingJobTasks, jobsExist, + getAllJobAndGroupIds, + getLookBackProgress, }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts new file mode 100644 index 0000000000000..a8b6ba494a070 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { newJobLineChartProvider } from './line_chart'; +import { newJobPopulationChartProvider } from './population_chart'; +export type callWithRequestType = (action: string, params: any) => Promise; + +export function newJobChartsProvider(callWithRequest: callWithRequestType) { + const { newJobLineChart } = newJobLineChartProvider(callWithRequest); + const { newJobPopulationChart } = newJobPopulationChartProvider(callWithRequest); + + return { + newJobLineChart, + newJobPopulationChart, + }; +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts new file mode 100644 index 0000000000000..758b834ed7b3a --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { newJobChartsProvider } from './charts'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts new file mode 100644 index 0000000000000..e319f5a115140 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { DslName, AggFieldNamePair } from '../../../../common/types/fields'; +import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; + +export type callWithRequestType = (action: string, params: any) => Promise; + +const EVENT_RATE_COUNT_FIELD = '__ml_event_rate_count__'; +const OVER_FIELD_EXAMPLES_COUNT = 40; + +type DtrIndex = number; +type TimeStamp = number; +type Value = number | undefined | null; +interface Result { + time: TimeStamp; + value: Value; +} + +interface ProcessedResults { + success: boolean; + results: Record; + totalResults: number; +} + +export function newJobLineChartProvider(callWithRequest: callWithRequestType) { + async function newJobLineChart( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: object, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string | null, + splitFieldValue: string | null + ) { + const json: object = getSearchJsonFromConfig( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue + ); + + const results = await callWithRequest('search', json); + return processSearchResults(results, aggFieldNamePairs.map(af => af.field)); + } + + return { + newJobLineChart, + }; +} + +function processSearchResults(resp: any, fields: string[]): ProcessedResults { + const aggregationsByTime = get(resp, ['aggregations', 'times', 'buckets'], []); + // let highestValue: number; + // let lowestValue: number; + + const tempResults: Record = {}; + fields.forEach((f, i) => (tempResults[i] = [])); + + aggregationsByTime.forEach((dataForTime: any) => { + const time: TimeStamp = +dataForTime.key; + const docCount = +dataForTime.doc_count; + + fields.forEach((field, i) => { + let value; + if (field === EVENT_RATE_COUNT_FIELD) { + value = docCount; + } else if (typeof dataForTime[i].value !== 'undefined') { + value = dataForTime[i].value; + } else if (typeof dataForTime[i].values !== 'undefined') { + value = dataForTime[i].values[ML_MEDIAN_PERCENTS]; + } + + // let value: Value = get(dataForTime, ['field_value', 'value']); + + // if (value === undefined && field !== null) { + // value = get(dataForTime, ['field_value', 'values', ML_MEDIAN_PERCENTS]); + // } + + // if (value === undefined && field === null) { + // value = dataForTime.doc_count; + // } + // if ( + // (value !== null && value !== undefined && !isFinite(value)) || + // dataForTime.doc_count === 0 + // ) { + // value = null; + // } + + // if (value !== null && value !== undefined) { + // highestValue = highestValue === undefined ? value : Math.max(value, highestValue); + // lowestValue = lowestValue === undefined ? value : Math.min(value, lowestValue); + // } + + tempResults[i].push({ + time, + value, + }); + }); + }); + + // const results: Record = {}; + // Object.entries(tempResults).forEach(([fieldIdx, results2]) => { + // results[+fieldIdx] = results2.sort((a, b) => a.time - b.time); + // }); + + return { + success: true, + results: tempResults, + totalResults: resp.hits.total, + }; +} + +function getSearchJsonFromConfig( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: any, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string | null, + splitFieldValue: string | null +): object { + const json = { + index: indexPatternTitle, + size: 0, + rest_total_hits_as_int: true, + body: { + query: {}, + aggs: { + times: { + date_histogram: { + field: timeField, + interval: intervalMs, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: {}, + }, + }, + }, + }; + + query.bool.must.push({ + range: { + [timeField]: { + gte: start, + lte: end, + format: 'epoch_millis', + }, + }, + }); + + if (splitFieldName !== null && splitFieldValue !== null) { + query.bool.must.push({ + term: { + [splitFieldName]: splitFieldValue, + }, + }); + } + + json.body.query = query; + + const aggs: Record> = {}; + + aggFieldNamePairs.forEach(({ agg, field }, i) => { + if (field !== null) { + aggs[i] = { + [agg]: { field }, + }; + + if (agg === 'percentiles') { + aggs[i][agg].percents = [ML_MEDIAN_PERCENTS]; + } + } + }); + + json.body.aggs.times.aggs = aggs; + // json.body.aggs.times.aggs = { + // field_value: { + // [agg]: { field }, + // }, + // }; + + // if (Object.keys(formConfig.fields).length) { + // json.body.aggs.times.aggs = {}; + // _.each(formConfig.fields, field => { + // if (field.id !== EVENT_RATE_COUNT_FIELD) { + // json.body.aggs.times.aggs[field.id] = { + // [field.agg.type.dslName]: { field: field.name }, + // }; + + // if (field.agg.type.dslName === 'percentiles') { + // json.body.aggs.times.aggs[field.id][field.agg.type.dslName].percents = [ + // ML_MEDIAN_PERCENTS, + // ]; + // } + // } + // }); + // } + + return json; +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts new file mode 100644 index 0000000000000..88e9f3e513f5b --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { DslName, AggFieldNamePair } from '../../../../common/types/fields'; +import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; + +export type callWithRequestType = (action: string, params: any) => Promise; + +const EVENT_RATE_COUNT_FIELD = '__ml_event_rate_count__'; +const OVER_FIELD_EXAMPLES_COUNT = 40; + +type DtrIndex = number; +type TimeStamp = number; +type Value = number | undefined | null; +interface Thing { + label: string; + value: Value; +} +interface Result { + time: TimeStamp; + values: Thing[]; +} + +interface ProcessedResults { + success: boolean; + results: Record; + totalResults: number; +} + +export function newJobPopulationChartProvider(callWithRequest: callWithRequestType) { + async function newJobPopulationChart( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: object, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string | null + ) { + const json: object = getPopulationSearchJsonFromConfig( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName + ); + + try { + const results = await callWithRequest('search', json); + return processSearchResults(results, aggFieldNamePairs.map(af => af.field)); + } catch (error) { + return { error }; + } + } + + return { + newJobPopulationChart, + }; +} + +function processSearchResults(resp: any, fields: string[]): ProcessedResults { + const aggregationsByTime = get(resp, ['aggregations', 'times', 'buckets'], []); + // let highestValue: number; + // let lowestValue: number; + + const tempResults: Record = {}; + fields.forEach((f, i) => (tempResults[i] = [])); + + aggregationsByTime.forEach((dataForTime: any) => { + const time: TimeStamp = +dataForTime.key; + const docCount = +dataForTime.doc_count; + + fields.forEach((field, i) => { + // FIX ALL OF THIS!!!!!! + // let value; + // if (fields[i] === EVENT_RATE_COUNT_FIELD) { + // value = docCount; + // } else if (typeof dataForTime[i].value !== 'undefined') { + // value = dataForTime[i].value; + // } else if (typeof dataForTime[i].values !== 'undefined') { + // value = dataForTime[i].values[ML_MEDIAN_PERCENTS]; + // } + const populationBuckets = get(dataForTime, ['population', 'buckets'], []); + const values: Thing[] = []; + if (field === EVENT_RATE_COUNT_FIELD) { + // populationBuckets.forEach(b => { + // // check to see if the data is split. + // if (b[i] === undefined) { + // values.push({ label: b.key, value: b.doc_count }); + // } else { + // // a split is being used, so an additional filter was added to the search + // values.push({ label: b.key, value: b[i].doc_count }); + // } + // }); + } else if (typeof dataForTime.population !== 'undefined') { + populationBuckets.forEach((b: any) => { + const tempBucket = b[i]; + let value = null; + // check to see if the data is split + // if the field has been split, an additional filter and aggregation + // has been added to the search in the form of splitValue + const tempValue = + tempBucket.value === undefined && tempBucket.splitValue !== undefined + ? tempBucket.splitValue + : tempBucket; + + // check to see if values is exists rather than value. + // if values exists, the aggregation was median + if (tempValue.value === undefined && tempValue.values !== undefined) { + value = tempValue.values[ML_MEDIAN_PERCENTS]; + } else { + value = tempValue.value; + } + values.push({ label: b.key, value: isFinite(value) ? value : null }); + }); + } + + // const highestValueField = _.reduce(values, (p, c) => (c.value > p.value ? c : p), { + // value: 0, + // }); + + tempResults[i].push({ + time, + values, + }); + + // if (this.chartData.detectors[i]) { + // this.chartData.detectors[i].line.push({ + // date, + // time, + // values, + // }); + + // init swimlane + // this.chartData.detectors[i].swimlane.push({ + // date, + // time, + // value: 0, + // color: '', + // percentComplete: 0, + // }); + + // this.chartData.detectors[i].highestValue = Math.ceil( + // Math.max(this.chartData.detectors[i].highestValue, Math.abs(highestValueField.value)) + // ); + // } + + // tempResults[i].push({ + // time, + // value, + // }); + }); + }); + + // const results: Record = {}; + // Object.entries(tempResults).forEach(([fieldIdx, results2]) => { + // results[+fieldIdx] = results2.sort((a, b) => a.time - b.time); + // }); + + return { + success: true, + results: tempResults, + totalResults: resp.hits.total, + }; +} + +function getPopulationSearchJsonFromConfig( + indexPatternTitle: string, + timeField: string, + start: number, + end: number, + intervalMs: number, + query: any, + aggFieldNamePairs: AggFieldNamePair[], + splitFieldName: string | null +): object { + const json = { + index: indexPatternTitle, + size: 0, + rest_total_hits_as_int: true, + body: { + query: {}, + aggs: { + times: { + date_histogram: { + field: timeField, + interval: intervalMs, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: {}, + }, + }, + }, + }; + + query.bool.must.push({ + range: { + [timeField]: { + gte: start, + lte: end, + format: 'epoch_millis', + }, + }, + }); + + json.body.query = query; + + // const aggs: Record> = {}; + const aggs: any = {}; + + aggFieldNamePairs.forEach(({ agg, field, by }, i) => { + if (field === EVENT_RATE_COUNT_FIELD) { + const h = 7; + } else { + if (by !== undefined && by.field !== null && by.value !== null) { + // if the field is split, add a filter to the aggregation to just select the + // fields which match the first split value (the front chart + aggs[i] = { + filter: { + term: { + [by.field]: by.value, + }, + }, + aggs: { + splitValue: { + [agg]: { field }, + }, + }, + }; + if (agg === 'percentiles') { + aggs[i].aggs.splitValue[agg].percents = [ML_MEDIAN_PERCENTS]; + } + } else { + aggs[i] = { + [agg]: { field }, + }; + + if (agg === 'percentiles') { + aggs[i][agg].percents = [ML_MEDIAN_PERCENTS]; + } + } + } + }); + + if (splitFieldName !== undefined) { + // the over field should not be undefined. the user should not have got this far if it is. + // add the wrapping terms based aggregation to divide the results up into + // over field values. + // we just want the first 40, or whatever OVER_FIELD_EXAMPLES_COUNT is set to. + json.body.aggs.times.aggs = { + population: { + terms: { + field: splitFieldName, + size: OVER_FIELD_EXAMPLES_COUNT, + }, + aggs, + }, + }; + } else { + json.body.aggs.times.aggs = aggs; + } + return json; +} diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.js b/x-pack/legacy/plugins/ml/server/routes/job_service.js index 90d36c988f5ee..6311d341618be 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_service.js +++ b/x-pack/legacy/plugins/ml/server/routes/job_service.js @@ -196,4 +196,99 @@ export function jobServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route } }); + route({ + method: 'POST', + path: '/api/ml/jobs/new_job_line_chart', + handler(request) { + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue + } = request.payload; + const { newJobLineChart } = jobServiceProvider(callWithRequest, request); + return newJobLineChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue, + ).catch(resp => wrapError(resp)); + }, + config: { + ...commonRouteConfig + } + }); + + route({ + method: 'POST', + path: '/api/ml/jobs/new_job_population_chart', + handler(request) { + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + } = request.payload; + const { newJobPopulationChart } = jobServiceProvider(callWithRequest, request); + return newJobPopulationChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + ).catch(resp => wrapError(resp)); + }, + config: { + ...commonRouteConfig + } + }); + + route({ + method: 'GET', + path: '/api/ml/jobs/all_jobs_and_group_ids', + handler(request) { + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); + const { getAllJobAndGroupIds } = jobServiceProvider(callWithRequest); + return getAllJobAndGroupIds() + .catch(resp => wrapError(resp)); + }, + config: { + ...commonRouteConfig + } + }); + + route({ + method: 'POST', + path: '/api/ml/jobs/look_back_progress', + handler(request) { + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); + const { getLookBackProgress } = jobServiceProvider(callWithRequest); + const { jobId, start, end } = request.payload; + return getLookBackProgress(jobId, start, end) + .catch(resp => wrapError(resp)); + }, + config: { + ...commonRouteConfig + } + }); + } From 16b1c022aae8f908c0c47a352e158c1b0ce62b8e Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jul 2019 14:10:29 +0100 Subject: [PATCH 02/15] small refactor --- .../full_time_range_selector.tsx | 6 +++--- .../job_service/new_job/population_chart.ts | 17 ++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx index 0e39a0fbc4521..9918d2dd7d83d 100644 --- a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Query } from 'ui/embeddable'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { IndexPattern } from 'ui/index_patterns'; import { EuiButton } from '@elastic/eui'; import { setFullTimeRange } from './full_time_range_selector_service'; @@ -20,7 +20,7 @@ interface Props { // Component for rendering a button which automatically sets the range of the time filter // to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query. -export const FullTimeRangeSelector: React.SFC = ({ indexPattern, query, disabled }) => { +export const FullTimeRangeSelector: FC = ({ indexPattern, query, disabled }) => { return ( setFullTimeRange(indexPattern, query)}> Promise; @@ -76,21 +76,13 @@ function processSearchResults(resp: any, fields: string[]): ProcessedResults { aggregationsByTime.forEach((dataForTime: any) => { const time: TimeStamp = +dataForTime.key; - const docCount = +dataForTime.doc_count; + // const docCount = +dataForTime.doc_count; fields.forEach((field, i) => { - // FIX ALL OF THIS!!!!!! - // let value; - // if (fields[i] === EVENT_RATE_COUNT_FIELD) { - // value = docCount; - // } else if (typeof dataForTime[i].value !== 'undefined') { - // value = dataForTime[i].value; - // } else if (typeof dataForTime[i].values !== 'undefined') { - // value = dataForTime[i].values[ML_MEDIAN_PERCENTS]; - // } const populationBuckets = get(dataForTime, ['population', 'buckets'], []); const values: Thing[] = []; if (field === EVENT_RATE_COUNT_FIELD) { + // TODO event rate // populationBuckets.forEach(b => { // // check to see if the data is split. // if (b[i] === undefined) { @@ -217,12 +209,11 @@ function getPopulationSearchJsonFromConfig( json.body.query = query; - // const aggs: Record> = {}; const aggs: any = {}; aggFieldNamePairs.forEach(({ agg, field, by }, i) => { if (field === EVENT_RATE_COUNT_FIELD) { - const h = 7; + // TODO event rate } else { if (by !== undefined && by.field !== null && by.value !== null) { // if the field is split, add a filter to the aggregation to just select the From 3e9edbb398cbac1caa335317ef6f1b884fc663c3 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jul 2019 15:50:13 +0100 Subject: [PATCH 03/15] fixing type problems --- .../jobs/new_job_new/pages/components/job_creator_context.ts | 4 ++-- .../ml/public/jobs/new_job_new/pages/new_job/directive.tsx | 1 + .../ml/public/jobs/new_job_new/pages/new_job/wizard.tsx | 3 ++- .../ml/server/models/job_service/new_job/line_chart.ts | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts index 3974c73bfcb2c..c88deeda9f3bf 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createContext, Dispatch } from 'react'; +import { createContext } from 'react'; import { Field, Aggregation } from '../../../../../common/types/fields'; import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; import { @@ -22,7 +22,7 @@ export interface ExistingJobsAndGroups { export interface JobCreatorContextValue { jobCreatorUpdated: number; - jobCreatorUpdate: Dispatch; + jobCreatorUpdate: () => void; jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator; chartLoader: ChartLoader; resultsLoader: ResultsLoader; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx index 14616be6e0c64..f13f1222c5033 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx @@ -27,6 +27,7 @@ module.directive('mlNewJobPage', ($injector: InjectorService) => { scope: {}, restrict: 'E', link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + // @ts-ignore: working around missing types timefilter.enableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx index 11a4e83b757b3..f2c0546c9124e 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx @@ -54,7 +54,8 @@ export const Wizard: FC = ({ return null; } - const [jobCreatorUpdated, jobCreatorUpdate] = useReducer<(s: number) => number>(s => s + 1, 0); + const [jobCreatorUpdated, setJobCreatorUpdate] = useReducer<(s: number) => number>(s => s + 1, 0); + const jobCreatorUpdate = () => setJobCreatorUpdate(jobCreatorUpdated); const jobCreatorContext: JobCreatorContextValue = { jobCreatorUpdated, diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts index e319f5a115140..e30cc7c1017c1 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -5,13 +5,13 @@ */ import { get } from 'lodash'; -import { DslName, AggFieldNamePair } from '../../../../common/types/fields'; +import { AggFieldNamePair } from '../../../../common/types/fields'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; export type callWithRequestType = (action: string, params: any) => Promise; const EVENT_RATE_COUNT_FIELD = '__ml_event_rate_count__'; -const OVER_FIELD_EXAMPLES_COUNT = 40; +// const OVER_FIELD_EXAMPLES_COUNT = 40; type DtrIndex = number; type TimeStamp = number; From 8b82370a5229e81720a17894f5fa5b82f7b16831 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jul 2019 16:26:59 +0100 Subject: [PATCH 04/15] updating snapshots --- ...index.test.tsx.snap => full_time_range_selector.test.tsx.snap} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/{index.test.tsx.snap => full_time_range_selector.test.tsx.snap} (100%) diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/index.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap From 7aa7173ea2c50af9bb3dcd6d58b1762f4448cd56 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jul 2019 16:30:48 +0100 Subject: [PATCH 05/15] updating translations ids for temporary new job links --- .../wizard/steps/job_type/job_type.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html index a105dc6175f57..6e7d3e5e537d1 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html @@ -221,21 +221,21 @@

@@ -253,21 +253,21 @@

@@ -285,21 +285,21 @@

From d1f77c24f016bf819765a4a46a6ff3ce124b4d71 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jul 2019 16:35:48 +0100 Subject: [PATCH 06/15] updating translation ids --- .../pages/components/job_details_step/groups_input.tsx | 9 +++------ .../ml/public/jobs/new_job_new/pages/new_job/wizard.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx index 06fed41923d86..3c000ddc068ae 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx @@ -73,12 +73,9 @@ export const GroupsInput: FC = ({ selectedGroupNames, setSelectedGroupNam > = ({ const stepsConfig = [ { - title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', { + title: i18n.translate('xpack.ml.newJob.wizard.step.timeRangeTitle', { defaultMessage: 'Time range', }), children: ( @@ -85,7 +85,7 @@ export const Wizard: FC = ({ status: currentStep >= WIZARD_STEPS.TIME_RANGE ? undefined : ('incomplete' as EuiStepStatus), }, { - title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', { + title: i18n.translate('xpack.ml.newJob.wizard.step.pickFieldsTitle', { defaultMessage: 'Pick fields', }), children: ( @@ -98,7 +98,7 @@ export const Wizard: FC = ({ }, { - title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', { + title: i18n.translate('xpack.ml.newJob.wizard.step.jobDetailsTitle', { defaultMessage: 'Job details', }), children: ( @@ -111,7 +111,7 @@ export const Wizard: FC = ({ }, { - title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', { + title: i18n.translate('xpack.ml.newJob.wizard.step.summaryTitle', { defaultMessage: 'Summary', }), children: ( From 9ee244227ccd28836ec6e94d304f41d1399db4e5 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jul 2019 18:11:20 +0100 Subject: [PATCH 07/15] fixing population by field selection --- .../components/split_field/by_field.tsx | 23 +++++++++++++++++-- .../components/split_field/split_field.tsx | 2 ++ .../split_field/split_field_select.tsx | 10 ++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx index 1fd714f986719..7cb89ad9c0e01 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx @@ -6,6 +6,7 @@ import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; +import { Field } from '../../../../../../../../common/types/fields'; import { SplitFieldSelect } from './split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; @@ -26,9 +27,19 @@ export const ByFieldSelector: FC = ({ detectorIndex }) => { } const jobCreator = jc as PopulationJobCreator; - const { categoryFields } = newJobCapsService; + const { categoryFields: allCategoryFields } = newJobCapsService; const [byField, setByField] = useState(jobCreator.getByField(detectorIndex)); + const [categoryFields, setCategoryFields] = useState(getFilteredCategoryFields()); + + function getFilteredCategoryFields(): Field[] { + // remove the split (over) field from the by field options + const sf = jobCreator.splitField; + if (sf !== null) { + return allCategoryFields.filter(f => f.name !== sf.name); + } + return allCategoryFields; + } useEffect(() => { jobCreator.setByField(byField, detectorIndex); @@ -38,9 +49,17 @@ export const ByFieldSelector: FC = ({ detectorIndex }) => { useEffect(() => { const bf = jobCreator.getByField(detectorIndex); setByField(bf); + + const filteredFields = getFilteredCategoryFields(); + setCategoryFields(filteredFields); }, [jobCreatorUpdated]); return ( - + ); }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx index 8b43714460651..ba66ded2e1e4f 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx @@ -23,6 +23,7 @@ export const SplitFieldSelector: FC = () => { return ; } const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator; + const canClearSelection = isMultiMetricJobCreator(jc); const { categoryFields } = newJobCapsService; const [splitField, setSplitField] = useState(jobCreator.splitField); @@ -42,6 +43,7 @@ export const SplitFieldSelector: FC = () => { fields={categoryFields} changeHandler={setSplitField} selectedField={splitField} + isClearable={canClearSelection} /> ); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field_select.tsx index 069edb7eaeafd..a30212aeb37bf 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field_select.tsx @@ -18,9 +18,15 @@ interface Props { fields: Field[]; changeHandler(f: SplitField): void; selectedField: SplitField; + isClearable: boolean; } -export const SplitFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { +export const SplitFieldSelect: FC = ({ + fields, + changeHandler, + selectedField, + isClearable, +}) => { const options: EuiComboBoxOptionProps[] = fields.map( f => ({ @@ -49,7 +55,7 @@ export const SplitFieldSelect: FC = ({ fields, changeHandler, selectedFie options={options} selectedOptions={selection} onChange={onChange} - isClearable={true} + isClearable={isClearable} /> ); }; From 693b5b1a3b04ae13f763c3d2264e5f658286e5df Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jul 2019 18:48:09 +0100 Subject: [PATCH 08/15] small refactor to by field select --- .../components/split_field/by_field.tsx | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx index 7cb89ad9c0e01..765303eb5f967 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx @@ -6,7 +6,6 @@ import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; -import { Field } from '../../../../../../../../common/types/fields'; import { SplitFieldSelect } from './split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; @@ -30,15 +29,22 @@ export const ByFieldSelector: FC = ({ detectorIndex }) => { const { categoryFields: allCategoryFields } = newJobCapsService; const [byField, setByField] = useState(jobCreator.getByField(detectorIndex)); - const [categoryFields, setCategoryFields] = useState(getFilteredCategoryFields()); + const categoryFields = useFilteredCategoryFields(); - function getFilteredCategoryFields(): Field[] { - // remove the split (over) field from the by field options - const sf = jobCreator.splitField; - if (sf !== null) { - return allCategoryFields.filter(f => f.name !== sf.name); - } - return allCategoryFields; + // remove the split (over) field from the by field options + function useFilteredCategoryFields() { + const [fields, setFields] = useState(allCategoryFields); + + useEffect(() => { + const sf = jobCreator.splitField; + if (sf !== null) { + setFields(allCategoryFields.filter(f => f.name !== sf.name)); + } else { + setFields(allCategoryFields); + } + }, [jobCreatorUpdated]); + + return fields; } useEffect(() => { @@ -49,9 +55,6 @@ export const ByFieldSelector: FC = ({ detectorIndex }) => { useEffect(() => { const bf = jobCreator.getByField(detectorIndex); setByField(bf); - - const filteredFields = getFilteredCategoryFields(); - setCategoryFields(filteredFields); }, [jobCreatorUpdated]); return ( From fbde12b02e43c709e0ec5f2f635dd1f42a6c22e7 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 11 Jul 2019 11:37:41 +0100 Subject: [PATCH 09/15] moving component descriptions to separate files --- .../groups/description.tsx} | 18 +++---- .../{ => components/groups}/groups_input.tsx | 53 +++++++------------ .../components/groups/index.ts | 7 +++ .../job_description/description.tsx} | 22 ++++---- .../components/job_description/index.ts | 7 +++ .../job_description/job_description_input.tsx | 26 +++++++++ .../components/job_id/description.tsx | 32 +++++++++++ .../components/job_id/index.ts | 7 +++ .../components/job_id/job_id_input.tsx | 22 ++++++++ .../job_details_step/job_details.tsx | 6 +-- .../components/bucket_span/bucket_span.tsx | 7 ++- .../bucket_span/bucket_span_input.tsx | 27 +++------- .../components/bucket_span/description.tsx | 32 +++++++++++ .../components/influencers/description.tsx | 32 +++++++++++ .../components/influencers/influencers.tsx | 15 +++--- .../influencers/influencers_select.tsx | 34 +++--------- .../components/split_cards/split_cards.tsx | 4 +- .../components/split_field/description.tsx | 18 ++++--- 18 files changed, 247 insertions(+), 122 deletions(-) rename x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/{job_id_input.tsx => components/groups/description.tsx} (58%) rename x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/{ => components/groups}/groups_input.tsx (55%) create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/index.ts rename x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/{job_description_input.tsx => components/job_description/description.tsx} (52%) create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/job_description_input.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/description.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/job_id_input.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/description.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_id_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/description.tsx similarity index 58% rename from x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_id_input.tsx rename to x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/description.tsx index bea348097b260..56308e52eff88 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_id_input.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/description.tsx @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC } from 'react'; -import { EuiDescribedFormGroup, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import React, { Fragment, memo, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; interface Props { - jobId: string; - setJobId: (id: string) => void; + children: JSX.Element; } -export const JobIdInput: FC = ({ jobId, setJobId }) => { +export const Description: FC = memo(({ children }) => { + const title = 'Groups'; return ( Job Id} + title={

{title}

} description={ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt @@ -24,9 +24,9 @@ export const JobIdInput: FC = ({ jobId, setJobId }) => { } > - - setJobId(e.target.value)} /> + + {children}
); -}; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/groups_input.tsx similarity index 55% rename from x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx rename to x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/groups_input.tsx index 3c000ddc068ae..6547ce6e0cc71 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/groups_input.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/groups_input.tsx @@ -4,16 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useContext } from 'react'; -import { - EuiDescribedFormGroup, - EuiFormRow, - EuiComboBox, - EuiComboBoxOptionProps, -} from '@elastic/eui'; +import React, { FC, useContext } from 'react'; +import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { JobCreatorContext } from '../job_creator_context'; -import { tabColor } from '../../../../../../common/util/group_color_utils'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { tabColor } from '../../../../../../../../common/util/group_color_utils'; +import { Description } from './description'; interface Props { selectedGroupNames: string[]; @@ -61,30 +57,19 @@ export const GroupsInput: FC = ({ selectedGroupNames, setSelectedGroupNam } return ( - Groups} - description={ - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam. - - } - > - - - - + + + ); }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/index.ts new file mode 100644 index 0000000000000..bdf4215883632 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/groups/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { GroupsInput } from './groups_input'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_description_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/description.tsx similarity index 52% rename from x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_description_input.tsx rename to x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/description.tsx index 5d610759e509a..768be30dcc35d 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_description_input.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/description.tsx @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC } from 'react'; -import { EuiDescribedFormGroup, EuiFormRow, EuiTextArea } from '@elastic/eui'; +import React, { Fragment, memo, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; interface Props { - jobDescription: string; - setJobDescription: (id: string) => void; + children: JSX.Element; } -export const JobDescriptionInput: FC = ({ jobDescription, setJobDescription }) => { +export const Description: FC = memo(({ children }) => { + const title = 'Job description'; return ( Job description} + title={

{title}

} description={ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt @@ -24,13 +24,9 @@ export const JobDescriptionInput: FC = ({ jobDescription, setJobDescripti } > - - setJobDescription(e.target.value)} - /> + + {children}
); -}; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/index.ts new file mode 100644 index 0000000000000..c8a2ed92fc63a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { JobDescriptionInput } from './job_description_input'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/job_description_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/job_description_input.tsx new file mode 100644 index 0000000000000..c01131de57393 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_description/job_description_input.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiTextArea } from '@elastic/eui'; +import { Description } from './description'; + +interface Props { + jobDescription: string; + setJobDescription: (id: string) => void; +} + +export const JobDescriptionInput: FC = ({ jobDescription, setJobDescription }) => { + return ( + + setJobDescription(e.target.value)} + /> + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/description.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/description.tsx new file mode 100644 index 0000000000000..249e0cc7218af --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/description.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, memo, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +interface Props { + children: JSX.Element; +} + +export const Description: FC = memo(({ children }) => { + const title = 'Job ID'; + return ( + {title}} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + {children} + + + ); +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/index.ts new file mode 100644 index 0000000000000..fd4c1581ca340 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { JobIdInput } from './job_id_input'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/job_id_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/job_id_input.tsx new file mode 100644 index 0000000000000..a93d1f0c6c4bc --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/components/job_id/job_id_input.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiFieldText } from '@elastic/eui'; +import { Description } from './description'; + +interface Props { + jobId: string; + setJobId: (id: string) => void; +} + +export const JobIdInput: FC = ({ jobId, setJobId }) => { + return ( + + setJobId(e.target.value)} /> + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_details.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_details.tsx index 50ea9d17f5086..7734a77eab25a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_details.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_details_step/job_details.tsx @@ -7,9 +7,9 @@ import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { WizardNav } from '../../../../../data_frame/components/wizard_nav'; -import { JobIdInput } from './job_id_input'; -import { JobDescriptionInput } from './job_description_input'; -import { GroupsInput } from './groups_input'; +import { JobIdInput } from './components/job_id'; +import { JobDescriptionInput } from './components/job_description'; +import { GroupsInput } from './components/groups'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { KibanaContext, isKibanaContext } from '../../../../../data_frame/common/kibana_context'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx index 2095d3bd839b9..8b4c7504d46a5 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx @@ -8,6 +8,7 @@ import React, { FC, useContext, useEffect, useState } from 'react'; import { BucketSpanInput } from './bucket_span_input'; import { JobCreatorContext } from '../../../job_creator_context'; +import { Description } from './description'; export const BucketSpan: FC = () => { const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); @@ -22,5 +23,9 @@ export const BucketSpan: FC = () => { setBucketSpan(jobCreator.bucketSpan); }, [jobCreatorUpdated]); - return ; + return ( + + + + ); }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx index 14f47a66ed9ba..4f984f13edb81 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC } from 'react'; -import { EuiDescribedFormGroup, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import React, { FC } from 'react'; +import { EuiFieldText } from '@elastic/eui'; interface Props { bucketSpan: string; @@ -14,23 +14,10 @@ interface Props { export const BucketSpanInput: FC = ({ bucketSpan, setBucketSpan }) => { return ( - Bucket span} - description={ - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam. - - } - > - - setBucketSpan(e.target.value)} - /> - - + setBucketSpan(e.target.value)} + /> ); }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/description.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/description.tsx new file mode 100644 index 0000000000000..cb3b1d6892a3f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span/description.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, memo, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +interface Props { + children: JSX.Element; +} + +export const Description: FC = memo(({ children }) => { + const title = 'Bucket span'; + return ( + {title}} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + {children} + + + ); +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/description.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/description.tsx new file mode 100644 index 0000000000000..a98237de38a06 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/description.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, memo, FC } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +interface Props { + children: JSX.Element; +} + +export const Description: FC = memo(({ children }) => { + const title = 'Influencers'; + return ( + {title}} + description={ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam. + + } + > + + {children} + + + ); +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx index 43a68b0ab7df5..e30ea84c284fc 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx @@ -15,6 +15,7 @@ import { PopulationJobCreator, isPopulationJobCreator, } from '../../../../../common/job_creator'; +import { Description } from './description'; export const Influencers: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); @@ -46,11 +47,13 @@ export const Influencers: FC = () => { }, [jobCreatorUpdated]); return ( - + + + ); }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index ea2f45c3702d9..42c83f2c89df2 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -4,13 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC } from 'react'; -import { - EuiComboBox, - EuiComboBoxOptionProps, - EuiDescribedFormGroup, - EuiFormRow, -} from '@elastic/eui'; +import React, { FC } from 'react'; +import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { Field, SplitField } from '../../../../../../../../common/types/fields'; @@ -37,24 +32,11 @@ export const InfluencersSelect: FC = ({ } return ( - Influencers} - description={ - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam. - - } - > - - - - + ); }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx index 7456f595b31cb..3f5947313ec5c 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, ReactElement, memo, Fragment } from 'react'; +import React, { FC, memo, Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { SplitField } from '../../../../../../../../common/types/fields'; @@ -14,7 +14,7 @@ interface Props { fieldValues: string[]; splitField: SplitField; numberOfDetectors: number; - children: ReactElement; + children: JSX.Element; jobType: JOB_TYPE; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx index 4d87099e8ce3d..3b031b2db676b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx @@ -4,22 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, ReactElement, FC } from 'react'; +import React, { Fragment, memo, FC } from 'react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; interface Props { - children: ReactElement; + children: JSX.Element; jobType: JOB_TYPE; } -export const Description: FC = ({ children, jobType }) => { +export const Description: FC = memo(({ children, jobType }) => { if (jobType === JOB_TYPE.MULTI_METRIC) { + const title = 'Split field'; return ( Split field} + title={

{title}

} description={ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor @@ -27,16 +28,17 @@ export const Description: FC = ({ children, jobType }) => { } > - + {children}
); } else if (jobType === JOB_TYPE.POPULATION) { + const title = 'Population field'; return ( Population} + title={

{title}

} description={ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor @@ -44,7 +46,7 @@ export const Description: FC = ({ children, jobType }) => { } > - + {children}
@@ -52,4 +54,4 @@ export const Description: FC = ({ children, jobType }) => { } else { return ; } -}; +}); From 6dd0ce6f8f321dbbc2feb234a652c2be5c0a06ed Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 11 Jul 2019 11:52:38 +0100 Subject: [PATCH 10/15] moving chart grid component into own file --- .../multi_metric_view/chart_grid.tsx | 76 ++++++++++++++++ .../multi_metric_view/metric_selection.tsx | 68 +-------------- .../components/population_view/chart_grid.tsx | 86 +++++++++++++++++++ .../population_view/metric_selection.tsx | 78 ++--------------- 4 files changed, 170 insertions(+), 138 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/chart_grid.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx new file mode 100644 index 0000000000000..63b3b95779804 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; + +import { AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; +import { ChartSettings } from '../../../charts/common/settings'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { ModelItem, Anomaly } from '../../../../../common/results_loader'; +import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { SplitCards } from '../split_cards'; +import { DetectorTitle } from '../detector_title'; +import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; + +interface ChartGridProps { + aggFieldPairList: AggFieldPair[]; + chartSettings: ChartSettings; + splitField: SplitField; + fieldValues: string[]; + lineChartsData: LineChartData; + modelData: Record; + anomalyData: Record; + deleteDetector?: (index: number) => void; + jobType: JOB_TYPE; +} + +export const ChartGrid: FC = ({ + aggFieldPairList, + chartSettings, + splitField, + fieldValues, + lineChartsData, + modelData, + anomalyData, + deleteDetector, + jobType, +}) => { + return ( + + + {aggFieldPairList.map((af, i) => ( + + {lineChartsData[i] !== undefined && ( + + + + + )} + + ))} + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index fd054eb8ac106..e845f2124da3a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -5,21 +5,18 @@ */ import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; -import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; + import { JobCreatorContext } from '../../../job_creator_context'; import { MultiMetricJobCreator, isMultiMetricJobCreator } from '../../../../../common/job_creator'; import { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; import { LineChartData } from '../../../../../common/chart_loader'; import { DropDownLabel, DropDownProps } from '../agg_select'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; -import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; +import { AggFieldPair } from '../../../../../../../../common/types/fields'; import { defaultChartSettings, ChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; -import { DetectorTitle } from '../detector_title'; import { JobProgress } from '../job_progress'; -import { SplitCards } from '../split_cards'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { ChartGrid } from './chart_grid'; interface Props { isActive: boolean; @@ -197,62 +194,3 @@ export const MultiMetricDetectors: FC = ({ isActive, setIsValid }) => { ); }; - -interface ChartGridProps { - aggFieldPairList: AggFieldPair[]; - chartSettings: ChartSettings; - splitField: SplitField; - fieldValues: string[]; - lineChartsData: LineChartData; - modelData: Record; - anomalyData: Record; - deleteDetector?: (index: number) => void; - jobType: JOB_TYPE; -} - -const ChartGrid: FC = ({ - aggFieldPairList, - chartSettings, - splitField, - fieldValues, - lineChartsData, - modelData, - anomalyData, - deleteDetector, - jobType, -}) => { - return ( - - - {aggFieldPairList.map((af, i) => ( - - {lineChartsData[i] !== undefined && ( - - - - - )} - - ))} - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/chart_grid.tsx new file mode 100644 index 0000000000000..4ef192b8ac72e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/chart_grid.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; +import { ChartSettings } from '../../../charts/common/settings'; +import { LineChartData } from '../../../../../common/chart_loader'; +import { ModelItem, Anomaly } from '../../../../../common/results_loader'; +import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { SplitCards } from '../split_cards'; +import { DetectorTitle } from '../detector_title'; +import { ByFieldSelector } from '../split_field'; +import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; + +type DetectorFieldValues = Record; + +interface ChartGridProps { + aggFieldPairList: AggFieldPair[]; + chartSettings: ChartSettings; + splitField: SplitField; + lineChartsData: LineChartData; + modelData: Record; + anomalyData: Record; + deleteDetector?: (index: number) => void; + jobType: JOB_TYPE; + fieldValuesPerDetector: DetectorFieldValues; +} + +export const ChartGrid: FC = ({ + aggFieldPairList, + chartSettings, + splitField, + lineChartsData, + modelData, + anomalyData, + deleteDetector, + jobType, + fieldValuesPerDetector, +}) => { + return ( + + {aggFieldPairList.map((af, i) => ( + + {lineChartsData[i] !== undefined && ( + + + + + + + {deleteDetector !== undefined && } + + + + + + + )} + + ))} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index 41eaec397f5bd..069553c6c8af5 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -5,23 +5,21 @@ */ import React, { Fragment, FC, useContext, useEffect, useState, useReducer } from 'react'; -import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { EuiHorizontalRule } from '@elastic/eui'; + import { JobCreatorContext } from '../../../job_creator_context'; import { PopulationJobCreator, isPopulationJobCreator } from '../../../../../common/job_creator'; import { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; import { LineChartData } from '../../../../../common/chart_loader'; import { DropDownLabel, DropDownProps } from '../agg_select'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { Field, AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; -import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; +import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; import { defaultChartSettings, ChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; -import { DetectorTitle } from '../detector_title'; import { JobProgress } from '../job_progress'; -import { SplitCards } from '../split_cards'; -import { SplitFieldSelector, ByFieldSelector } from '../split_field'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { SplitFieldSelector } from '../split_field'; import { MlTimeBuckets } from '../../../../../../../util/ml_time_buckets'; +import { ChartGrid } from './chart_grid'; interface Props { isActive: boolean; @@ -268,69 +266,3 @@ export const PopulationDetectors: FC = ({ isActive, setIsValid }) => {
); }; - -interface ChartGridProps { - aggFieldPairList: AggFieldPair[]; - chartSettings: ChartSettings; - splitField: SplitField; - lineChartsData: LineChartData; - modelData: Record; - anomalyData: Record; - deleteDetector?: (index: number) => void; - jobType: JOB_TYPE; - fieldValuesPerDetector: DetectorFieldValues; -} - -const ChartGrid: FC = ({ - aggFieldPairList, - chartSettings, - splitField, - lineChartsData, - modelData, - anomalyData, - deleteDetector, - jobType, - fieldValuesPerDetector, -}) => { - return ( - - {aggFieldPairList.map((af, i) => ( - - {lineChartsData[i] !== undefined && ( - - - - - - - {deleteDetector !== undefined && } - - - - - - - )} - - ))} - - ); -}; From 6ab297012fc38911aaa068c81145d86f4f5951fd Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 11 Jul 2019 16:44:23 +0100 Subject: [PATCH 11/15] fixing event rate count detector --- .../legacy/plugins/ml/common/types/fields.ts | 2 + .../common/chart_loader/chart_loader.ts | 6 +- .../multi_metric_view/metric_selection.tsx | 11 +-- .../population_view/metric_selection.tsx | 15 ++-- .../single_metric_view/metric_selection.tsx | 54 ++++--------- .../services/new_job_capabilities_service.ts | 80 ++++++++++++++++++- .../models/job_service/new_job/line_chart.ts | 59 +------------- .../job_service/new_job/population_chart.ts | 74 +++++------------ 8 files changed, 136 insertions(+), 165 deletions(-) diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts index d9a4446588bfc..7e09f14515a13 100644 --- a/x-pack/legacy/plugins/ml/common/types/fields.ts +++ b/x-pack/legacy/plugins/ml/common/types/fields.ts @@ -7,6 +7,8 @@ import { ES_FIELD_TYPES } from '../../common/constants/field_types'; import { ML_JOB_AGGREGATION } from '../../common/constants/aggregation_types'; +export const EVENT_RATE_FIELD_ID = '__ml_event_rate_count__'; + export type FieldId = string; export type AggId = ML_JOB_AGGREGATION; export type SplitField = Field | null; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts index fbd9f40541970..539420cc94675 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts @@ -57,7 +57,7 @@ export class ChartLoader { this._query, aggFieldPairs.map(af => ({ agg: af.agg.dslName, - field: af.field.name, + field: af.field.id, })), splitFieldName, splitFieldValue @@ -87,12 +87,12 @@ export class ChartLoader { aggFieldPairs.map(af => { const by = af.by !== undefined && af.by.field !== null && af.by.value !== null - ? { field: af.by.field.name, value: af.by.value } + ? { field: af.by.field.id, value: af.by.value } : { field: null, value: null }; return { agg: af.agg.dslName, - field: af.field.name, + field: af.field.id, by, }; }), diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index e845f2124da3a..a2d88b77f96a3 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -12,7 +12,7 @@ import { Results, ModelItem, Anomaly } from '../../../../../common/results_loade import { LineChartData } from '../../../../../common/chart_loader'; import { DropDownLabel, DropDownProps } from '../agg_select'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { AggFieldPair } from '../../../../../../../../common/types/fields'; +import { AggFieldPair, EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields'; import { defaultChartSettings, ChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; import { JobProgress } from '../job_progress'; @@ -78,13 +78,9 @@ export const MultiMetricDetectors: FC = ({ isActive, setIsValid }) => { setAnomalyData(results.anomalies); } - // subscribe to progress useEffect(() => { + // subscribe to progress and results jobCreator.subscribeToProgress(setProgress); - }, []); - - // subscribe to results - useEffect(() => { resultsLoader.subscribeToResults(setResultsWrapper); }, []); @@ -92,7 +88,8 @@ export const MultiMetricDetectors: FC = ({ isActive, setIsValid }) => { useEffect(() => { jobCreator.removeAllDetectors(); aggFieldPairList.forEach(pair => { - jobCreator.addDetector(pair.agg, pair.field); + const field = pair.field.id === EVENT_RATE_FIELD_ID ? null : pair.field; + jobCreator.addDetector(pair.agg, field); }); jobCreatorUpdate(); loadCharts(); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index 069553c6c8af5..7dacef1d7cae7 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -13,7 +13,11 @@ import { Results, ModelItem, Anomaly } from '../../../../../common/results_loade import { LineChartData } from '../../../../../common/chart_loader'; import { DropDownLabel, DropDownProps } from '../agg_select'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { + Field, + AggFieldPair, + EVENT_RATE_FIELD_ID, +} from '../../../../../../../../common/types/fields'; import { defaultChartSettings, ChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; import { JobProgress } from '../job_progress'; @@ -84,13 +88,9 @@ export const PopulationDetectors: FC = ({ isActive, setIsValid }) => { setAnomalyData(results.anomalies); } - // subscribe to progress useEffect(() => { + // subscribe to progress and results jobCreator.subscribeToProgress(setProgress); - }, []); - - // subscribe to results - useEffect(() => { resultsLoader.subscribeToResults(setResultsWrapper); }, []); @@ -98,7 +98,8 @@ export const PopulationDetectors: FC = ({ isActive, setIsValid }) => { useEffect(() => { jobCreator.removeAllDetectors(); aggFieldPairList.forEach((pair, i) => { - jobCreator.addDetector(pair.agg, pair.field); + const field = pair.field.id === EVENT_RATE_FIELD_ID ? null : pair.field; + jobCreator.addDetector(pair.agg, field); if (pair.by !== undefined) { // re-add by fields jobCreator.setByField(pair.by.field, i); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index e3f68450efc7d..4679e607e50da 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -73,13 +73,9 @@ export const SingleMetricDetectors: FC = ({ isActive, setIsValid }) => { } } - // subscribe to progress useEffect(() => { + // subscribe to progress and results jobCreator.subscribeToProgress(setProgress); - }, []); - - // subscribe to results - useEffect(() => { resultsLoader.subscribeToResults(setResultsWrapper); }, []); @@ -119,40 +115,24 @@ export const SingleMetricDetectors: FC = ({ isActive, setIsValid }) => { return ( {isActive && ( - - - {lineChartsData[DTR_IDX] !== undefined && ( - - )} - + )} - {isActive === false && ( + {lineChartsData[DTR_IDX] !== undefined && ( - {lineChartsData[DTR_IDX] !== undefined && ( - - - - - )} + + {isActive === false && } )} diff --git a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts index 75265468152cd..0d2d7c8f4263d 100644 --- a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts @@ -5,8 +5,16 @@ */ import { IndexPatternWithType } from '../../common/types/kibana'; -import { Field, Aggregation, AggId, FieldId, NewJobCaps } from '../../common/types/fields'; +import { + Field, + Aggregation, + AggId, + FieldId, + NewJobCaps, + EVENT_RATE_FIELD_ID, +} from '../../common/types/fields'; import { ES_FIELD_TYPES } from '../../common/constants/field_types'; +import { ML_JOB_AGGREGATION } from '../../common/constants/aggregation_types'; import { ml } from './ml_api_service'; // called in the angular routing resolve block to initialize the @@ -30,10 +38,12 @@ const categoryFieldTypes = [ES_FIELD_TYPES.TEXT, ES_FIELD_TYPES.KEYWORD, ES_FIEL class NewJobCapsService { private _fields: Field[]; private _aggs: Aggregation[]; + private _includeCountAgg: boolean; - constructor() { + constructor(includeCountAgg = true) { this._fields = []; this._aggs = []; + this._includeCountAgg = includeCountAgg; } get fields(): Field[] { @@ -59,6 +69,14 @@ class NewJobCapsService { try { const resp = await ml.jobs.newJobCaps(indexPattern.title, indexPattern.type === 'rollup'); const { fields, aggs } = createObjects(resp, indexPattern.title); + + if (this._includeCountAgg === true) { + const { countField, countAggs } = createCountFieldAndAggs(); + + fields.push(countField); + aggs.push(...countAggs); + } + this._fields = fields; this._aggs = aggs; } catch (error) { @@ -139,4 +157,62 @@ function mix(field: Field, agg: Aggregation) { field.aggs.push(agg); } +function createCountFieldAndAggs() { + const countField: Field = { + id: EVENT_RATE_FIELD_ID, + name: 'Event rate', + type: ES_FIELD_TYPES.INTEGER, + aggregatable: true, + aggs: [], + }; + + const countAggs: Aggregation[] = [ + { + id: ML_JOB_AGGREGATION.COUNT, + title: 'Count', + kibanaName: 'count', + dslName: 'count', + type: 'metrics', + mlModelPlotAgg: { + min: 'min;', + max: 'max', + }, + fields: [countField], + }, + { + id: ML_JOB_AGGREGATION.HIGH_COUNT, + title: 'High count', + kibanaName: 'count', + dslName: 'count', + type: 'metrics', + mlModelPlotAgg: { + min: 'min;', + max: 'max', + }, + fields: [countField], + }, + { + id: ML_JOB_AGGREGATION.LOW_COUNT, + title: 'Low count', + kibanaName: 'count', + dslName: 'count', + type: 'metrics', + mlModelPlotAgg: { + min: 'min;', + max: 'max', + }, + fields: [countField], + }, + ]; + + if (countField.aggs !== undefined) { + countField.aggs.push(...countAggs); + } + + return { + countField, + countAggs, + }; +} + export const newJobCapsService = new NewJobCapsService(); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts index e30cc7c1017c1..59a33db1da2e9 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -5,14 +5,11 @@ */ import { get } from 'lodash'; -import { AggFieldNamePair } from '../../../../common/types/fields'; +import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; export type callWithRequestType = (action: string, params: any) => Promise; -const EVENT_RATE_COUNT_FIELD = '__ml_event_rate_count__'; -// const OVER_FIELD_EXAMPLES_COUNT = 40; - type DtrIndex = number; type TimeStamp = number; type Value = number | undefined | null; @@ -62,8 +59,6 @@ export function newJobLineChartProvider(callWithRequest: callWithRequestType) { function processSearchResults(resp: any, fields: string[]): ProcessedResults { const aggregationsByTime = get(resp, ['aggregations', 'times', 'buckets'], []); - // let highestValue: number; - // let lowestValue: number; const tempResults: Record = {}; fields.forEach((f, i) => (tempResults[i] = [])); @@ -74,7 +69,7 @@ function processSearchResults(resp: any, fields: string[]): ProcessedResults { fields.forEach((field, i) => { let value; - if (field === EVENT_RATE_COUNT_FIELD) { + if (field === EVENT_RATE_FIELD_ID) { value = docCount; } else if (typeof dataForTime[i].value !== 'undefined') { value = dataForTime[i].value; @@ -82,27 +77,6 @@ function processSearchResults(resp: any, fields: string[]): ProcessedResults { value = dataForTime[i].values[ML_MEDIAN_PERCENTS]; } - // let value: Value = get(dataForTime, ['field_value', 'value']); - - // if (value === undefined && field !== null) { - // value = get(dataForTime, ['field_value', 'values', ML_MEDIAN_PERCENTS]); - // } - - // if (value === undefined && field === null) { - // value = dataForTime.doc_count; - // } - // if ( - // (value !== null && value !== undefined && !isFinite(value)) || - // dataForTime.doc_count === 0 - // ) { - // value = null; - // } - - // if (value !== null && value !== undefined) { - // highestValue = highestValue === undefined ? value : Math.max(value, highestValue); - // lowestValue = lowestValue === undefined ? value : Math.min(value, lowestValue); - // } - tempResults[i].push({ time, value, @@ -110,11 +84,6 @@ function processSearchResults(resp: any, fields: string[]): ProcessedResults { }); }); - // const results: Record = {}; - // Object.entries(tempResults).forEach(([fieldIdx, results2]) => { - // results[+fieldIdx] = results2.sort((a, b) => a.time - b.time); - // }); - return { success: true, results: tempResults, @@ -179,7 +148,7 @@ function getSearchJsonFromConfig( const aggs: Record> = {}; aggFieldNamePairs.forEach(({ agg, field }, i) => { - if (field !== null) { + if (field !== null && field !== EVENT_RATE_FIELD_ID) { aggs[i] = { [agg]: { field }, }; @@ -191,28 +160,6 @@ function getSearchJsonFromConfig( }); json.body.aggs.times.aggs = aggs; - // json.body.aggs.times.aggs = { - // field_value: { - // [agg]: { field }, - // }, - // }; - - // if (Object.keys(formConfig.fields).length) { - // json.body.aggs.times.aggs = {}; - // _.each(formConfig.fields, field => { - // if (field.id !== EVENT_RATE_COUNT_FIELD) { - // json.body.aggs.times.aggs[field.id] = { - // [field.agg.type.dslName]: { field: field.name }, - // }; - - // if (field.agg.type.dslName === 'percentiles') { - // json.body.aggs.times.aggs[field.id][field.agg.type.dslName].percents = [ - // ML_MEDIAN_PERCENTS, - // ]; - // } - // } - // }); - // } return json; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts index 4d8d71199da15..69a0472800bf6 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -5,12 +5,11 @@ */ import { get } from 'lodash'; -import { AggFieldNamePair } from '../../../../common/types/fields'; +import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; export type callWithRequestType = (action: string, params: any) => Promise; -const EVENT_RATE_COUNT_FIELD = '__ml_event_rate_count__'; const OVER_FIELD_EXAMPLES_COUNT = 40; type DtrIndex = number; @@ -68,30 +67,26 @@ export function newJobPopulationChartProvider(callWithRequest: callWithRequestTy function processSearchResults(resp: any, fields: string[]): ProcessedResults { const aggregationsByTime = get(resp, ['aggregations', 'times', 'buckets'], []); - // let highestValue: number; - // let lowestValue: number; const tempResults: Record = {}; fields.forEach((f, i) => (tempResults[i] = [])); aggregationsByTime.forEach((dataForTime: any) => { const time: TimeStamp = +dataForTime.key; - // const docCount = +dataForTime.doc_count; fields.forEach((field, i) => { const populationBuckets = get(dataForTime, ['population', 'buckets'], []); const values: Thing[] = []; - if (field === EVENT_RATE_COUNT_FIELD) { - // TODO event rate - // populationBuckets.forEach(b => { - // // check to see if the data is split. - // if (b[i] === undefined) { - // values.push({ label: b.key, value: b.doc_count }); - // } else { - // // a split is being used, so an additional filter was added to the search - // values.push({ label: b.key, value: b[i].doc_count }); - // } - // }); + if (field === EVENT_RATE_FIELD_ID) { + populationBuckets.forEach((b: any) => { + // check to see if the data is split. + if (b[i] === undefined) { + values.push({ label: b.key, value: b.doc_count }); + } else { + // a split is being used, so an additional filter was added to the search + values.push({ label: b.key, value: b[i].doc_count }); + } + }); } else if (typeof dataForTime.population !== 'undefined') { populationBuckets.forEach((b: any) => { const tempBucket = b[i]; @@ -115,48 +110,13 @@ function processSearchResults(resp: any, fields: string[]): ProcessedResults { }); } - // const highestValueField = _.reduce(values, (p, c) => (c.value > p.value ? c : p), { - // value: 0, - // }); - tempResults[i].push({ time, values, }); - - // if (this.chartData.detectors[i]) { - // this.chartData.detectors[i].line.push({ - // date, - // time, - // values, - // }); - - // init swimlane - // this.chartData.detectors[i].swimlane.push({ - // date, - // time, - // value: 0, - // color: '', - // percentComplete: 0, - // }); - - // this.chartData.detectors[i].highestValue = Math.ceil( - // Math.max(this.chartData.detectors[i].highestValue, Math.abs(highestValueField.value)) - // ); - // } - - // tempResults[i].push({ - // time, - // value, - // }); }); }); - // const results: Record = {}; - // Object.entries(tempResults).forEach(([fieldIdx, results2]) => { - // results[+fieldIdx] = results2.sort((a, b) => a.time - b.time); - // }); - return { success: true, results: tempResults, @@ -212,8 +172,16 @@ function getPopulationSearchJsonFromConfig( const aggs: any = {}; aggFieldNamePairs.forEach(({ agg, field, by }, i) => { - if (field === EVENT_RATE_COUNT_FIELD) { - // TODO event rate + if (field === EVENT_RATE_FIELD_ID) { + if (by !== undefined && by.field !== null && by.value !== null) { + aggs[i] = { + filter: { + term: { + [by.field]: by.value, + }, + }, + }; + } } else { if (by !== undefined && by.field !== null && by.value !== null) { // if the field is split, add a filter to the aggregation to just select the From c084e880ead16b5b4617a83e052dc4a9d60530e7 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 11 Jul 2019 16:45:55 +0100 Subject: [PATCH 12/15] fixing decimal place rounding --- .../jobs/new_job_new/pages/components/charts/common/axes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/axes.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/axes.tsx index d1b8f1fe284b8..d068609760da8 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/axes.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/axes.tsx @@ -17,7 +17,7 @@ interface Props { // round to 2dp function tickFormatter(d: number): string { - return Math.round((d * 100) / 100).toString(); + return (Math.round(d * 100) / 100).toString(); } export const Axes: FC = ({ chartData }) => { From c11cbeff7ff01eb2c31a7042cb7441cb13f060ef Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 12 Jul 2019 10:38:07 +0100 Subject: [PATCH 13/15] changes based on review --- .../jobs/new_job_new/common/chart_settings.ts | 36 ---------------- .../common/results_loader/results_loader.ts | 42 +++++++++---------- .../components/charts/common/settings.ts | 2 +- .../components/influencers/influencers.tsx | 1 - .../influencers/influencers_select.tsx | 18 ++++---- .../multi_metric_view/chart_grid.tsx | 2 +- .../multi_metric_view/metric_selection.tsx | 6 ++- .../multi_metric_view/metric_selector.tsx | 6 ++- .../components/population_view/chart_grid.tsx | 2 +- .../population_view/metric_selection.tsx | 6 ++- .../population_view/metric_selector.tsx | 6 ++- .../single_metric_view/metric_selection.tsx | 10 +++-- .../services/new_job_capabilities_service.ts | 6 +-- 13 files changed, 57 insertions(+), 86 deletions(-) delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_settings.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_settings.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_settings.ts deleted file mode 100644 index fbb5efb718e2c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_settings.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { timefilter } from 'ui/timefilter'; -// @ts-ignore -import { IntervalHelperProvider } from '../../../util/ml_time_buckets'; - -export class ChartSettings { - private _interval: any; - private PAGE_WIDTH: number; - private BAR_TARGET: number; - private MAX_BARS: number; - - constructor(pageWidth: number) { - this.PAGE_WIDTH = pageWidth; // angular.element('.single-metric-job-container').width(); - this.BAR_TARGET = this.PAGE_WIDTH > 2000 ? 1000 : this.PAGE_WIDTH / 2; - this.MAX_BARS = this.BAR_TARGET + (this.BAR_TARGET / 100) * 100; // 100% larger than bar target - - const MlTimeBuckets = IntervalHelperProvider(); - this._interval = new MlTimeBuckets(); - this._interval.setBarTarget(this.BAR_TARGET); - this._interval.setMaxBars(this.MAX_BARS); - this._interval.setInterval('auto'); - - const tf = timefilter as any; - const bounds = tf.getActiveBounds(); - this._interval.setBounds(bounds); - } - - public get intervalSeconds(): number { - return this._interval.getInterval().asSeconds(); - } -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts index cddf97fdcf41a..ef237d3c46b39 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts @@ -65,7 +65,6 @@ export class ResultsLoader { private _lastModelTimeStamp: number = 0; private _lastResultsTimeout: any = null; private _chartLoader: ChartLoader; - // private _detectorSplitFieldWithValue: DetectorSplitFieldFilter; private _results: Results = { progress: 0, @@ -86,26 +85,27 @@ export class ResultsLoader { } progressSubscriber = async (progress: number) => { - if (this._resultsSearchRunning === false) { - if (progress - this._results.progress > 5 || progress === 100) { - if (this._splitFieldFiltersLoaded === false) { - this._splitFieldFiltersLoaded = true; - // load detector field filters if this is the first run. - await this._populateDetectorSplitFieldFilters(); - } + if ( + this._resultsSearchRunning === false && + (progress - this._results.progress > 5 || progress === 100) + ) { + if (this._splitFieldFiltersLoaded === false) { + this._splitFieldFiltersLoaded = true; + // load detector field filters if this is the first run. + await this._populateDetectorSplitFieldFilters(); + } - this._updateData(progress, false); - - if (progress === 100) { - // after the job has finished, do one final update - // a while after the last 100% has been received. - // note, there may be multiple 100% progresses sent as they will only stop once the - // datafeed has stopped. - clearTimeout(this._lastResultsTimeout); - this._lastResultsTimeout = setTimeout(() => { - this._updateData(progress, true); - }, LAST_UPDATE_DELAY_MS); - } + this._updateData(progress, false); + + if (progress === 100) { + // after the job has finished, do one final update + // a while after the last 100% has been received. + // note, there may be multiple 100% progresses sent as they will only stop once the + // datafeed has stopped. + clearTimeout(this._lastResultsTimeout); + this._lastResultsTimeout = setTimeout(() => { + this._updateData(progress, true); + }, LAST_UPDATE_DELAY_MS); } } }; @@ -133,7 +133,7 @@ export class ResultsLoader { } public subscribeToResults(func: ResultsSubscriber) { - this._results$.subscribe(func); + return this._results$.subscribe(func); } public get progress() { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts index 288600d52d29e..53ff0bf5d15ae 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts @@ -10,7 +10,7 @@ export const MODEL_COLOR = '#006BB4'; export interface ChartSettings { width: string; height: string; - cols: number; + cols: 1 | 2 | 3; intervalMs: number; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx index e30ea84c284fc..1fe9a2a92d65f 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers.tsx @@ -52,7 +52,6 @@ export const Influencers: FC = () => { fields={fields} changeHandler={setInfluencers} selectedInfluencers={influencers} - splitField={splitField} /> ); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index 42c83f2c89df2..e4c38056e0389 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -7,24 +7,20 @@ import React, { FC } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; -import { Field, SplitField } from '../../../../../../../../common/types/fields'; +import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields'; interface Props { fields: Field[]; changeHandler(i: string[]): void; selectedInfluencers: string[]; - splitField: SplitField; } -export const InfluencersSelect: FC = ({ - fields, - changeHandler, - selectedInfluencers, - splitField, -}) => { - const options: EuiComboBoxOptionProps[] = fields.map(f => ({ - label: f.name, - })); +export const InfluencersSelect: FC = ({ fields, changeHandler, selectedInfluencers }) => { + const options: EuiComboBoxOptionProps[] = fields + .filter(f => f.id !== EVENT_RATE_FIELD_ID) + .map(f => ({ + label: f.name, + })); const selection: EuiComboBoxOptionProps[] = selectedInfluencers.map(i => ({ label: i })); function onChange(selectedOptions: EuiComboBoxOptionProps[]) { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx index 63b3b95779804..a9801d52ce6d9 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx @@ -46,7 +46,7 @@ export const ChartGrid: FC = ({ numberOfDetectors={aggFieldPairList.length} jobType={jobType} > - + {aggFieldPairList.map((af, i) => ( {lineChartsData[i] !== undefined && ( diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index a2d88b77f96a3..8124a06298e84 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -81,7 +81,10 @@ export const MultiMetricDetectors: FC = ({ isActive, setIsValid }) => { useEffect(() => { // subscribe to progress and results jobCreator.subscribeToProgress(setProgress); - resultsLoader.subscribeToResults(setResultsWrapper); + const subscription = resultsLoader.subscribeToResults(setResultsWrapper); + return () => { + subscription.unsubscribe(); + }; }, []); // watch for changes in detector list length @@ -181,7 +184,6 @@ export const MultiMetricDetectors: FC = ({ isActive, setIsValid }) => { fields={fields} detectorChangeHandler={detectorChangeHandler} selectedOptions={selectedOptions} - maxWidth={560} removeOptions={aggFieldPairList} /> )} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx index 6b56172e4100a..e82f7aa9a9b30 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx @@ -13,10 +13,12 @@ interface Props { fields: Field[]; detectorChangeHandler: (options: DropDownLabel[]) => void; selectedOptions: DropDownProps; - maxWidth: number; + maxWidth?: number; removeOptions: AggFieldPair[]; } +const MAX_WIDTH = 560; + export const MetricSelector: FC = ({ fields, detectorChangeHandler, @@ -25,7 +27,7 @@ export const MetricSelector: FC = ({ removeOptions, }) => { return ( - + = ({ fieldValuesPerDetector, }) => { return ( - + {aggFieldPairList.map((af, i) => ( {lineChartsData[i] !== undefined && ( diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index 7dacef1d7cae7..259a8f910069d 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -91,7 +91,10 @@ export const PopulationDetectors: FC = ({ isActive, setIsValid }) => { useEffect(() => { // subscribe to progress and results jobCreator.subscribeToProgress(setProgress); - resultsLoader.subscribeToResults(setResultsWrapper); + const subscription = resultsLoader.subscribeToResults(setResultsWrapper); + return () => { + subscription.unsubscribe(); + }; }, []); // watch for changes in detector list length @@ -257,7 +260,6 @@ export const PopulationDetectors: FC = ({ isActive, setIsValid }) => { fields={fields} detectorChangeHandler={detectorChangeHandler} selectedOptions={selectedOptions} - maxWidth={560} removeOptions={aggFieldPairList} /> )} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selector.tsx index 6b56172e4100a..e82f7aa9a9b30 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selector.tsx @@ -13,10 +13,12 @@ interface Props { fields: Field[]; detectorChangeHandler: (options: DropDownLabel[]) => void; selectedOptions: DropDownProps; - maxWidth: number; + maxWidth?: number; removeOptions: AggFieldPair[]; } +const MAX_WIDTH = 560; + export const MetricSelector: FC = ({ fields, detectorChangeHandler, @@ -25,7 +27,7 @@ export const MetricSelector: FC = ({ removeOptions, }) => { return ( - + = ({ isActive, setIsValid }) => { useEffect(() => { // subscribe to progress and results jobCreator.subscribeToProgress(setProgress); - resultsLoader.subscribeToResults(setResultsWrapper); + const subscription = resultsLoader.subscribeToResults(setResultsWrapper); + return () => { + subscription.unsubscribe(); + }; }, []); useEffect(() => { if (aggFieldPair !== null) { - jobCreator.setDetector(aggFieldPair.agg, aggFieldPair.field); + const field = aggFieldPair.field.id === EVENT_RATE_FIELD_ID ? null : aggFieldPair.field; + jobCreator.setDetector(aggFieldPair.agg, field); jobCreatorUpdate(); loadChart(); setIsValid(aggFieldPair !== null); diff --git a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts index 0d2d7c8f4263d..8b10ee80e8db2 100644 --- a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts @@ -174,7 +174,7 @@ function createCountFieldAndAggs() { dslName: 'count', type: 'metrics', mlModelPlotAgg: { - min: 'min;', + min: 'min', max: 'max', }, fields: [countField], @@ -186,7 +186,7 @@ function createCountFieldAndAggs() { dslName: 'count', type: 'metrics', mlModelPlotAgg: { - min: 'min;', + min: 'min', max: 'max', }, fields: [countField], @@ -198,7 +198,7 @@ function createCountFieldAndAggs() { dslName: 'count', type: 'metrics', mlModelPlotAgg: { - min: 'min;', + min: 'min', max: 'max', }, fields: [countField], From 9f91250c88e7bc7f0e66af548119d0a4fe0d2950 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 12 Jul 2019 11:52:56 +0100 Subject: [PATCH 14/15] more changes based on review --- .../legacy/plugins/ml/common/types/kibana.ts | 5 + .../plugins/ml/common/util/anomaly_utils.js | 158 +++++++++++------- .../common/chart_loader/chart_loader.ts | 31 ++-- .../components/charts/common/settings.ts | 12 +- .../event_rate_chart/event_rate_chart.tsx | 4 +- .../components/agg_select/agg_select.tsx | 1 - .../components/influencers/influencers.tsx | 4 +- .../multi_metric_view/metric_selection.tsx | 2 +- .../population_view/metric_selection.tsx | 2 +- .../single_metric_view/metric_selection.tsx | 2 +- .../components/split_field/by_field.tsx | 48 +++--- .../components/split_field/description.tsx | 2 +- .../components/split_field/split_field.tsx | 4 +- .../jobs/new_job_new/pages/new_job/route.ts | 7 +- .../ml/public/util/ml_time_buckets.d.ts | 4 +- 15 files changed, 171 insertions(+), 115 deletions(-) diff --git a/x-pack/legacy/plugins/ml/common/types/kibana.ts b/x-pack/legacy/plugins/ml/common/types/kibana.ts index dbcd37ff3b9a8..5695fc17302b6 100644 --- a/x-pack/legacy/plugins/ml/common/types/kibana.ts +++ b/x-pack/legacy/plugins/ml/common/types/kibana.ts @@ -16,3 +16,8 @@ export interface IndexPatternWithType extends IndexPattern { export type IndexPatternTitle = string; export type callWithRequestType = (action: string, params?: any) => Promise; + +export interface Route { + id: string; + k7Breadcrumbs: () => any; +} diff --git a/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js index b4ce49e5639d8..d201a971dda5c 100644 --- a/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js @@ -4,27 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ - - /* -* Contains functions for operations commonly performed on anomaly data -* to extract information for display in dashboards. -*/ + * Contains functions for operations commonly performed on anomaly data + * to extract information for display in dashboards. + */ -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule'; import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact'; -import { ANOMALY_SEVERITY } from '../constants/anomalies'; - +import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../constants/anomalies'; // List of function descriptions for which actual values from record level results should be displayed. -const DISPLAY_ACTUAL_FUNCTIONS = ['count', 'distinct_count', 'lat_long', 'mean', 'max', 'min', 'sum', - 'median', 'varp', 'info_content', 'time']; +const DISPLAY_ACTUAL_FUNCTIONS = [ + 'count', + 'distinct_count', + 'lat_long', + 'mean', + 'max', + 'min', + 'sum', + 'median', + 'varp', + 'info_content', + 'time', +]; // List of function descriptions for which typical values from record level results should be displayed. -const DISPLAY_TYPICAL_FUNCTIONS = ['count', 'distinct_count', 'lat_long', 'mean', 'max', 'min', 'sum', - 'median', 'varp', 'info_content', 'time']; +const DISPLAY_TYPICAL_FUNCTIONS = [ + 'count', + 'distinct_count', + 'lat_long', + 'mean', + 'max', + 'min', + 'sum', + 'median', + 'varp', + 'info_content', + 'time', +]; let severityTypes; @@ -33,26 +51,44 @@ function getSeverityTypes() { return severityTypes; } - return severityTypes = { - critical: { id: 'critical', label: i18n.translate('xpack.ml.anomalyUtils.severity.criticalLabel', { - defaultMessage: 'critical', - }) }, - major: { id: 'major', label: i18n.translate('xpack.ml.anomalyUtils.severity.majorLabel', { - defaultMessage: 'major', - }) }, - minor: { id: 'minor', label: i18n.translate('xpack.ml.anomalyUtils.severity.minorLabel', { - defaultMessage: 'minor', - }) }, - warning: { id: 'warning', label: i18n.translate('xpack.ml.anomalyUtils.severity.warningLabel', { - defaultMessage: 'warning', - }) }, - unknown: { id: 'unknown', label: i18n.translate('xpack.ml.anomalyUtils.severity.unknownLabel', { - defaultMessage: 'unknown', - }) }, - low: { id: 'low', label: i18n.translate('xpack.ml.anomalyUtils.severityWithLow.lowLabel', { - defaultMessage: 'low', - }) }, - }; + return (severityTypes = { + critical: { + id: ANOMALY_SEVERITY.CRITICAL, + label: i18n.translate('xpack.ml.anomalyUtils.severity.criticalLabel', { + defaultMessage: 'critical', + }), + }, + major: { + id: ANOMALY_SEVERITY.MAJOR, + label: i18n.translate('xpack.ml.anomalyUtils.severity.majorLabel', { + defaultMessage: 'major', + }), + }, + minor: { + id: ANOMALY_SEVERITY.MINOR, + label: i18n.translate('xpack.ml.anomalyUtils.severity.minorLabel', { + defaultMessage: 'minor', + }), + }, + warning: { + id: ANOMALY_SEVERITY.WARNING, + label: i18n.translate('xpack.ml.anomalyUtils.severity.warningLabel', { + defaultMessage: 'warning', + }), + }, + unknown: { + id: ANOMALY_SEVERITY.UNKNOWN, + label: i18n.translate('xpack.ml.anomalyUtils.severity.unknownLabel', { + defaultMessage: 'unknown', + }), + }, + low: { + id: ANOMALY_SEVERITY.LOW, + label: i18n.translate('xpack.ml.anomalyUtils.severityWithLow.lowLabel', { + defaultMessage: 'low', + }), + }, + }); } // Returns a severity label (one of critical, major, minor, warning or unknown) @@ -60,13 +96,13 @@ function getSeverityTypes() { export function getSeverity(normalizedScore) { const severityTypesList = getSeverityTypes(); - if (normalizedScore >= 75) { + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { return severityTypesList.critical; - } else if (normalizedScore >= 50) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { return severityTypesList.major; - } else if (normalizedScore >= 25) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { return severityTypesList.minor; - } else if (normalizedScore >= 0) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { return severityTypesList.warning; } else { return severityTypesList.unknown; @@ -95,15 +131,15 @@ export function getSeverityType(normalizedScore) { export function getSeverityWithLow(normalizedScore) { const severityTypesList = getSeverityTypes(); - if (normalizedScore >= 75) { + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { return severityTypesList.critical; - } else if (normalizedScore >= 50) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { return severityTypesList.major; - } else if (normalizedScore >= 25) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { return severityTypesList.minor; - } else if (normalizedScore >= 3) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { return severityTypesList.warning; - } else if (normalizedScore >= 0) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { return severityTypesList.low; } else { return severityTypesList.unknown; @@ -113,15 +149,15 @@ export function getSeverityWithLow(normalizedScore) { // Returns a severity RGB color (one of critical, major, minor, warning, low_warning or unknown) // for the supplied normalized anomaly score (a value between 0 and 100). export function getSeverityColor(normalizedScore) { - if (normalizedScore >= 75) { + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { return '#fe5050'; - } else if (normalizedScore >= 50) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { return '#fba740'; - } else if (normalizedScore >= 25) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { return '#fdec25'; - } else if (normalizedScore >= 3) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { return '#8bc8fb'; - } else if (normalizedScore >= 0) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { return '#d2e9f7'; } else { return '#ffffff'; @@ -157,15 +193,15 @@ export function getMultiBucketImpactLabel(multiBucketImpact) { export function getEntityFieldName(record) { // Analyses with by and over fields, will have a top-level by_field_name, but // the by_field_value(s) will be in the nested causes array. - if (_.has(record, 'by_field_name') && _.has(record, 'by_field_value')) { + if (record.by_field_name !== undefined && record.by_field_value !== undefined) { return record.by_field_name; } - if (_.has(record, 'over_field_name')) { + if (record.over_field_name !== undefined) { return record.over_field_name; } - if (_.has(record, 'partition_field_name')) { + if (record.partition_field_name !== undefined) { return record.partition_field_name; } @@ -176,15 +212,15 @@ export function getEntityFieldName(record) { // obtained from Elasticsearch. The function looks first for a by_field, then over_field, // then partition_field, returning undefined if none of these fields are present. export function getEntityFieldValue(record) { - if (_.has(record, 'by_field_value')) { + if (record.by_field_value !== undefined) { return record.by_field_value; } - if (_.has(record, 'over_field_value')) { + if (record.over_field_value !== undefined) { return record.over_field_value; } - if (_.has(record, 'partition_field_value')) { + if (record.partition_field_value !== undefined) { return record.partition_field_value; } @@ -199,7 +235,7 @@ export function getEntityFieldList(record) { entityFields.push({ fieldName: record.partition_field_name, fieldValue: record.partition_field_value, - fieldType: 'partition' + fieldType: 'partition', }); } @@ -207,7 +243,7 @@ export function getEntityFieldList(record) { entityFields.push({ fieldName: record.over_field_name, fieldValue: record.over_field_value, - fieldType: 'over' + fieldType: 'over', }); } @@ -218,7 +254,7 @@ export function getEntityFieldList(record) { entityFields.push({ fieldName: record.by_field_name, fieldValue: record.by_field_value, - fieldType: 'by' + fieldType: 'by', }); } @@ -229,22 +265,24 @@ export function getEntityFieldList(record) { // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. export function showActualForFunction(functionDescription) { - return _.indexOf(DISPLAY_ACTUAL_FUNCTIONS, functionDescription) > -1; + return DISPLAY_ACTUAL_FUNCTIONS.indexOf(functionDescription) > -1; } // Returns whether typical values should be displayed for a record with the specified function description. // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. export function showTypicalForFunction(functionDescription) { - return _.indexOf(DISPLAY_TYPICAL_FUNCTIONS, functionDescription) > -1; + return DISPLAY_TYPICAL_FUNCTIONS.indexOf(functionDescription) > -1; } // Returns whether a rule can be configured against the specified anomaly. export function isRuleSupported(record) { // A rule can be configured with a numeric condition if the function supports it, // and/or with scope if there is a partitioning fields. - return (CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(record.function) === -1) || - (getEntityFieldName(record) !== undefined); + return ( + CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(record.function) === -1 || + getEntityFieldName(record) !== undefined + ); } // Two functions for converting aggregation type names. @@ -290,5 +328,5 @@ export const aggregationTypeTransform = { } return newAggType; - } + }, }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts index 539420cc94675..71f806927bba7 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/chart_loader/chart_loader.ts @@ -55,10 +55,7 @@ export class ChartLoader { end, intervalMs, this._query, - aggFieldPairs.map(af => ({ - agg: af.agg.dslName, - field: af.field.id, - })), + aggFieldPairs.map(getAggFieldPairNames), splitFieldName, splitFieldValue ); @@ -84,18 +81,7 @@ export class ChartLoader { end, intervalMs, this._query, - aggFieldPairs.map(af => { - const by = - af.by !== undefined && af.by.field !== null && af.by.value !== null - ? { field: af.by.field.id, value: af.by.value } - : { field: null, value: null }; - - return { - agg: af.agg.dslName, - field: af.field.id, - by, - }; - }), + aggFieldPairs.map(getAggFieldPairNames), splitFieldName ); return resp.results; @@ -135,3 +121,16 @@ export class ChartLoader { return results; } } + +export function getAggFieldPairNames(af: AggFieldPair) { + const by = + af.by !== undefined && af.by.field !== null && af.by.value !== null + ? { field: af.by.field.id, value: af.by.value } + : { field: null, value: null }; + + return { + agg: af.agg.dslName, + field: af.field.id, + by, + }; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts index 53ff0bf5d15ae..6543466bfd254 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts @@ -4,8 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -export const LINE_COLOR = '#006BB4'; -export const MODEL_COLOR = '#006BB4'; +import chrome from 'ui/chrome'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; + +const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode'); +const themeName = IS_DARK_THEME ? darkTheme : lightTheme; + +export const LINE_COLOR = themeName.euiColorPrimary; +export const MODEL_COLOR = themeName.euiColorPrimary; +export const EVENT_RATE_COLOR = themeName.euiColorPrimary; export interface ChartSettings { width: string; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 53df4772f2e1c..011df2bc550a7 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -9,6 +9,7 @@ import { BarSeries, Chart, getSpecId, ScaleType, Settings, TooltipType } from '@ import { Axes } from '../common/axes'; import { getCustomColor } from '../common/utils'; import { LineChartPoint } from '../../../../common/chart_loader'; +import { EVENT_RATE_COLOR } from '../common/settings'; interface Props { eventRateChartData: LineChartPoint[]; @@ -18,7 +19,6 @@ interface Props { } const SPEC_ID = 'event_rate'; -const COLOR = '#006BB4'; export const EventRateChart: FC = ({ eventRateChartData, height, width, showAxis }) => { return ( @@ -34,7 +34,7 @@ export const EventRateChart: FC = ({ eventRateChartData, height, width, s xAccessor={'time'} yAccessors={['value']} data={eventRateChartData} - customSeriesColors={getCustomColor(SPEC_ID, COLOR)} + customSeriesColors={getCustomColor(SPEC_ID, EVENT_RATE_COLOR)} />
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/agg_select.tsx index e406436d2aab0..a870811154f6a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/agg_select.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -59,7 +59,6 @@ export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, r return ( { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); if (isMultiMetricJobCreator(jc) === false && isPopulationJobCreator(jc) === false) { - return ; + return null; } const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index 8124a06298e84..8164af18f59a1 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -34,7 +34,7 @@ export const MultiMetricDetectors: FC = ({ isActive, setIsValid }) => { } = useContext(JobCreatorContext); if (isMultiMetricJobCreator(jc) === false) { - return ; + return null; } const jobCreator = jc as MultiMetricJobCreator; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index 259a8f910069d..58c526a38b148 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -43,7 +43,7 @@ export const PopulationDetectors: FC = ({ isActive, setIsValid }) => { } = useContext(JobCreatorContext); if (isPopulationJobCreator(jc) === false) { - return ; + return null; } const jobCreator = jc as PopulationJobCreator; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index 4e83aee4c7e26..79053fa55a5b8 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -36,7 +36,7 @@ export const SingleMetricDetectors: FC = ({ isActive, setIsValid }) => { } = useContext(JobCreatorContext); if (isSingleMetricJobCreator(jc) === false) { - return ; + return null; } const jobCreator = jc as SingleMetricJobCreator; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx index 765303eb5f967..6e16b946d8ac3 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; +import React, { FC, useContext, useEffect, useState } from 'react'; import { SplitFieldSelect } from './split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; +import { Field } from '../../../../../../../../common/types/fields'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; import { + MultiMetricJobCreator, isMultiMetricJobCreator, PopulationJobCreator, isPopulationJobCreator, @@ -22,30 +24,18 @@ interface Props { export const ByFieldSelector: FC = ({ detectorIndex }) => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); if (isMultiMetricJobCreator(jc) === false && isPopulationJobCreator(jc) === false) { - return ; + return null; } const jobCreator = jc as PopulationJobCreator; const { categoryFields: allCategoryFields } = newJobCapsService; const [byField, setByField] = useState(jobCreator.getByField(detectorIndex)); - const categoryFields = useFilteredCategoryFields(); - - // remove the split (over) field from the by field options - function useFilteredCategoryFields() { - const [fields, setFields] = useState(allCategoryFields); - - useEffect(() => { - const sf = jobCreator.splitField; - if (sf !== null) { - setFields(allCategoryFields.filter(f => f.name !== sf.name)); - } else { - setFields(allCategoryFields); - } - }, [jobCreatorUpdated]); - - return fields; - } + const categoryFields = useFilteredCategoryFields( + allCategoryFields, + jobCreator, + jobCreatorUpdated + ); useEffect(() => { jobCreator.setByField(byField, detectorIndex); @@ -66,3 +56,23 @@ export const ByFieldSelector: FC = ({ detectorIndex }) => { /> ); }; + +// remove the split (over) field from the by field options +function useFilteredCategoryFields( + allCategoryFields: Field[], + jobCreator: MultiMetricJobCreator | PopulationJobCreator, + jobCreatorUpdated: number +) { + const [fields, setFields] = useState(allCategoryFields); + + useEffect(() => { + const sf = jobCreator.splitField; + if (sf !== null) { + setFields(allCategoryFields.filter(f => f.name !== sf.name)); + } else { + setFields(allCategoryFields); + } + }, [jobCreatorUpdated]); + + return fields; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx index 3b031b2db676b..1227f15dd5a5a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/description.tsx @@ -52,6 +52,6 @@ export const Description: FC = memo(({ children, jobType }) => { ); } else { - return ; + return null; } }); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx index ba66ded2e1e4f..de0546996ef96 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; +import React, { FC, useContext, useEffect, useState } from 'react'; import { SplitFieldSelect } from './split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; @@ -20,7 +20,7 @@ import { export const SplitFieldSelector: FC = () => { const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); if (isMultiMetricJobCreator(jc) === false && isPopulationJobCreator(jc) === false) { - return ; + return null; } const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator; const canClearSelection = isMultiMetricJobCreator(jc); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts index 10502541469f8..1a81108974881 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts @@ -20,6 +20,8 @@ import { // @ts-ignore } from '../../../breadcrumbs'; +import { Route } from '../../../../../common/types/kibana'; + import { loadNewJobCapabilities } from '../../../../services/new_job_capabilities_service'; import { mlJobService } from '../../../../services/job_service'; @@ -27,11 +29,6 @@ import { JOB_TYPE } from '../../common/job_creator/util/constants'; const template = ``; -interface Route { - id: JOB_TYPE; - k7Breadcrumbs: () => any; -} - const routes: Route[] = [ { id: JOB_TYPE.SINGLE_METRIC, diff --git a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts b/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts index 093076f9a09e8..81c150efa2d24 100644 --- a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts +++ b/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts @@ -6,7 +6,7 @@ import { Moment } from 'moment'; -declare interface TimefilterBounds { +declare interface TimeFilterBounds { min: Moment; max: Moment; } @@ -15,7 +15,7 @@ export class MlTimeBuckets { setBarTarget: (barTarget: number) => void; setMaxBars: (maxBars: number) => void; setInterval: (interval: string) => void; - setBounds: (bounds: TimefilterBounds) => void; + setBounds: (bounds: TimeFilterBounds) => void; getBounds: () => { min: any; max: any }; getInterval: () => { asMilliseconds: () => number; From f78e4b7ce9f3da7796f1a9117d167f188136c5d5 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 12 Jul 2019 12:46:52 +0100 Subject: [PATCH 15/15] fixing test --- .../ml/public/services/new_job_capabilities._service.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities._service.test.ts b/x-pack/legacy/plugins/ml/public/services/new_job_capabilities._service.test.ts index 22663b166d482..361d08d0290e4 100644 --- a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities._service.test.ts +++ b/x-pack/legacy/plugins/ml/public/services/new_job_capabilities._service.test.ts @@ -36,8 +36,8 @@ describe('new_job_capabilities_service', () => { const meanAgg = aggs.find(a => a.id === 'mean') || { fields: [] }; const distinctCountAgg = aggs.find(a => a.id === 'distinct_count') || { fields: [] }; - expect(fields).toHaveLength(2); - expect(aggs).toHaveLength(12); + expect(fields).toHaveLength(3); + expect(aggs).toHaveLength(15); expect(responseTimeField.aggs).toHaveLength(12); expect(airlineField.aggs).toHaveLength(1);