"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.TimeSeriesExplorer = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _lodash = require("lodash"); var _momentTimezone = _interopRequireDefault(require("moment-timezone")); var _rxjs = require("rxjs"); var _operators = require("rxjs/operators"); var _propTypes = _interopRequireDefault(require("prop-types")); var _react = _interopRequireWildcard(require("react")); var _i18n = require("@kbn/i18n"); var _i18nReact = require("@kbn/i18n-react"); var _eui = require("@elastic/eui"); var _public = require("@kbn/kibana-utils-plugin/public"); var _timeseriesexplorer_help_popover = require("./timeseriesexplorer_help_popover"); var _search = require("../../../common/constants/search"); var _job_utils = require("../../../common/util/job_utils"); var _annotation_flyout = require("../components/annotations/annotation_flyout"); var _annotations_table = require("../components/annotations/annotations_table"); var _anomalies_table = require("../components/anomalies_table/anomalies_table"); var _forecasting_modal = require("./components/forecasting_modal/forecasting_modal"); var _loading_indicator = require("../components/loading_indicator/loading_indicator"); var _select_interval = require("../components/controls/select_interval/select_interval"); var _select_severity = require("../components/controls/select_severity/select_severity"); var _timeseriesexplorer_no_chart_data = require("./components/timeseriesexplorer_no_chart_data"); var _timeseriesexplorer_page = require("./timeseriesexplorer_page"); var _ml_api_service = require("../services/ml_api_service"); var _field_format_service = require("../services/field_format_service"); var _forecast_service = require("../services/forecast_service"); var _job_service = require("../services/job_service"); var _results_service = require("../services/results_service"); var _time_buckets = require("../util/time_buckets"); var _timeseriesexplorer_constants = require("./timeseriesexplorer_constants"); var _timeseries_search_service = require("./timeseries_search_service"); var _timeseriesexplorer_utils = require("./timeseriesexplorer_utils"); var _settings = require("../../../common/constants/settings"); var _get_controls_for_detector = require("./get_controls_for_detector"); var _series_controls = require("./components/series_controls"); var _timeseries_chart_with_tooltip = require("./components/timeseries_chart/timeseries_chart_with_tooltip"); var _mlAnomalyUtils = require("@kbn/ml-anomaly-utils"); var _get_function_description = require("./get_function_description"); var _get_viewable_detectors = require("./timeseriesexplorer_utils/get_viewable_detectors"); var _timeseriesexplorer_chart_data_error = require("./components/timeseriesexplorer_chart_data_error"); var _components = require("../explorer/components"); var _explorer_utils = require("../explorer/explorer_utils"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ /* * React component for rendering Single Metric Viewer. */ // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be // obtained as it is not aggregatable e.g. 'all distinct kpi_indicator values' const allValuesLabel = _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionValuesLabel', { defaultMessage: 'all' }); function getTimeseriesexplorerDefaultState() { return { chartDetails: undefined, contextAggregationInterval: undefined, contextChartData: undefined, contextForecastData: undefined, // Not chartable if e.g. model plot with terms for a varp detector dataNotChartable: false, entitiesLoading: false, entityValues: {}, focusAnnotationData: [], focusAggregationInterval: {}, focusChartData: undefined, focusForecastData: undefined, fullRefresh: true, hasResults: false, // Counter to keep track of what data sets have been loaded. loadCounter: 0, loading: false, modelPlotEnabled: false, // Toggles display of annotations in the focus chart showAnnotations: true, showAnnotationsCheckbox: true, // Toggles display of forecast data in the focus chart showForecast: true, showForecastCheckbox: false, // Toggles display of model bounds in the focus chart showModelBounds: true, showModelBoundsCheckbox: false, svgWidth: 0, tableData: undefined, zoomFrom: undefined, zoomTo: undefined, zoomFromFocusLoaded: undefined, zoomToFocusLoaded: undefined, chartDataError: undefined, sourceIndicesWithGeoFields: {} }; } const containerPadding = 34; class TimeSeriesExplorer extends _react.default.Component { constructor(...args) { super(...args); (0, _defineProperty2.default)(this, "state", getTimeseriesexplorerDefaultState()); (0, _defineProperty2.default)(this, "subscriptions", new _rxjs.Subscription()); (0, _defineProperty2.default)(this, "resizeRef", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "resizeChecker", undefined); (0, _defineProperty2.default)(this, "resizeHandler", () => { this.setState({ svgWidth: this.resizeRef.current !== null ? this.resizeRef.current.offsetWidth - containerPadding : 0 }); }); (0, _defineProperty2.default)(this, "unmounted", false); /** * Subject for listening brush time range selection. */ (0, _defineProperty2.default)(this, "contextChart$", new _rxjs.Subject()); /** * Returns field names that don't have a selection yet. */ (0, _defineProperty2.default)(this, "getFieldNamesWithEmptyValues", () => { const latestEntityControls = this.getControlsForDetector(); return latestEntityControls.filter(({ fieldValue }) => fieldValue === null).map(({ fieldName }) => fieldName); }); /** * Checks if all entity control dropdowns have a selection. */ (0, _defineProperty2.default)(this, "arePartitioningFieldsProvided", () => { const fieldNamesWithEmptyValues = this.getFieldNamesWithEmptyValues(); return fieldNamesWithEmptyValues.length === 0; }); (0, _defineProperty2.default)(this, "toggleShowAnnotationsHandler", () => { this.setState(prevState => ({ showAnnotations: !prevState.showAnnotations })); }); (0, _defineProperty2.default)(this, "toggleShowForecastHandler", () => { this.setState(prevState => ({ showForecast: !prevState.showForecast })); }); (0, _defineProperty2.default)(this, "toggleShowModelBoundsHandler", () => { this.setState({ showModelBounds: !this.state.showModelBounds }); }); (0, _defineProperty2.default)(this, "setFunctionDescription", selectedFuction => { this.props.appStateHandler(_timeseriesexplorer_constants.APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION, selectedFuction); }); (0, _defineProperty2.default)(this, "previousChartProps", {}); (0, _defineProperty2.default)(this, "previousShowAnnotations", undefined); (0, _defineProperty2.default)(this, "previousShowForecast", undefined); (0, _defineProperty2.default)(this, "previousShowModelBounds", undefined); (0, _defineProperty2.default)(this, "tableFilter", (field, value, operator) => { const entities = this.getControlsForDetector(); const entity = entities.find(({ fieldName }) => fieldName === field); if (entity === undefined) { return; } const { appStateHandler } = this.props; let resultValue = ''; if (operator === '+' && entity.fieldValue !== value) { resultValue = value; } else if (operator === '-' && entity.fieldValue === value) { resultValue = null; } else { return; } const resultEntities = { ...entities.reduce((appStateEntities, appStateEntity) => { appStateEntities[appStateEntity.fieldName] = appStateEntity.fieldValue; return appStateEntities; }, {}), [entity.fieldName]: resultValue }; appStateHandler(_timeseriesexplorer_constants.APP_STATE_ACTION.SET_ENTITIES, resultEntities); }); (0, _defineProperty2.default)(this, "contextChartSelectedInitCallDone", false); (0, _defineProperty2.default)(this, "contextChartSelected", selection => { const zoomState = { from: selection.from.toISOString(), to: selection.to.toISOString() }; if ((0, _lodash.isEqual)(this.props.zoom, zoomState) && this.state.focusChartData !== undefined && this.props.previousRefresh === this.props.lastRefresh) { return; } this.contextChart$.next(selection); this.props.appStateHandler(_timeseriesexplorer_constants.APP_STATE_ACTION.SET_ZOOM, zoomState); }); (0, _defineProperty2.default)(this, "loadAnomaliesTableData", (earliestMs, latestMs) => { const { dateFormatTz, selectedDetectorIndex, selectedJobId, tableInterval, tableSeverity, functionDescription } = this.props; const selectedJob = _job_service.mlJobService.getJob(selectedJobId); const entityControls = this.getControlsForDetector(); return _ml_api_service.ml.results.getAnomaliesTableData([selectedJob.job_id], this.getCriteriaFields(selectedDetectorIndex, entityControls), [], tableInterval, tableSeverity, earliestMs, latestMs, dateFormatTz, _search.ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, undefined, undefined, functionDescription).pipe((0, _operators.map)(resp => { const anomalies = resp.anomalies; const detectorsByJob = _job_service.mlJobService.detectorsByJob; anomalies.forEach(anomaly => { // Add a detector property to each anomaly. // Default to functionDescription if no description available. // TODO - when job_service is moved server_side, move this to server endpoint. const jobId = anomaly.jobId; const detector = (0, _lodash.get)(detectorsByJob, [jobId, anomaly.detectorIndex]); anomaly.detector = (0, _lodash.get)(detector, ['detector_description'], anomaly.source.function_description); // For detectors with rules, add a property with the rule count. const customRules = detector.custom_rules; if (customRules !== undefined) { anomaly.rulesLength = customRules.length; } // Add properties used for building the links menu. // TODO - when job_service is moved server_side, move this to server endpoint. if ((0, _lodash.has)(_job_service.mlJobService.customUrlsByJob, jobId)) { anomaly.customUrls = _job_service.mlJobService.customUrlsByJob[jobId]; } }); return { tableData: { anomalies, interval: resp.interval, examplesByJobId: resp.examplesByJobId, showViewSeriesLink: false } }; })); }); (0, _defineProperty2.default)(this, "setForecastId", forecastId => { this.props.appStateHandler(_timeseriesexplorer_constants.APP_STATE_ACTION.SET_FORECAST_ID, forecastId); }); (0, _defineProperty2.default)(this, "displayErrorToastMessages", (error, errorMsg) => { if (this.props.toastNotificationService) { this.props.toastNotificationService.displayErrorToast(error, errorMsg, 2000); } this.setState({ loading: false, chartDataError: errorMsg }); }); (0, _defineProperty2.default)(this, "loadSingleMetricData", (fullRefresh = true) => { const { autoZoomDuration, bounds, selectedDetectorIndex, selectedForecastId, selectedJobId, zoom, functionDescription } = this.props; const { loadCounter: currentLoadCounter } = this.state; const currentSelectedJob = _job_service.mlJobService.getJob(selectedJobId); if (currentSelectedJob === undefined) { return; } if ((0, _get_function_description.isMetricDetector)(currentSelectedJob, selectedDetectorIndex) && functionDescription === undefined) { return; } const functionToPlotByIfMetric = _mlAnomalyUtils.aggregationTypeTransform.toES(functionDescription); this.contextChartSelectedInitCallDone = false; // Only when `fullRefresh` is true we'll reset all data // and show the loading spinner within the page. const entityControls = this.getControlsForDetector(); this.setState({ fullRefresh, loadCounter: currentLoadCounter + 1, loading: true, chartDataError: undefined, ...(fullRefresh ? { chartDetails: undefined, contextChartData: undefined, contextForecastData: undefined, focusChartData: undefined, focusForecastData: undefined, modelPlotEnabled: (0, _job_utils.isModelPlotChartableForDetector)(currentSelectedJob, selectedDetectorIndex) && (0, _job_utils.isModelPlotEnabled)(currentSelectedJob, selectedDetectorIndex, entityControls), hasResults: false, dataNotChartable: false } : {}) }, () => { const { loadCounter, modelPlotEnabled } = this.state; const jobs = (0, _timeseriesexplorer_utils.createTimeSeriesJobData)(_job_service.mlJobService.jobs); const selectedJob = _job_service.mlJobService.getJob(selectedJobId); const detectorIndex = selectedDetectorIndex; let awaitingCount = 3; const stateUpdate = {}; // finish() function, called after each data set has been loaded and processed. // The last one to call it will trigger the page render. const finish = counterVar => { awaitingCount--; if (awaitingCount === 0 && counterVar === loadCounter) { stateUpdate.hasResults = Array.isArray(stateUpdate.contextChartData) && stateUpdate.contextChartData.length > 0 || Array.isArray(stateUpdate.contextForecastData) && stateUpdate.contextForecastData.length > 0; stateUpdate.loading = false; // Set zoomFrom/zoomTo attributes in scope which will result in the metric chart automatically // selecting the specified range in the context chart, and so loading that date range in the focus chart. // Only touch the zoom range if data for the context chart has been loaded and all necessary // partition fields have a selection. if (stateUpdate.contextChartData.length && this.arePartitioningFieldsProvided() === true) { // Check for a zoom parameter in the appState (URL). let focusRange = (0, _timeseriesexplorer_utils.calculateInitialFocusRange)(zoom, stateUpdate.contextAggregationInterval, bounds); if (focusRange === undefined || this.previousSelectedForecastId !== this.props.selectedForecastId) { focusRange = (0, _timeseriesexplorer_utils.calculateDefaultFocusRange)(autoZoomDuration, stateUpdate.contextAggregationInterval, stateUpdate.contextChartData, stateUpdate.contextForecastData); this.previousSelectedForecastId = this.props.selectedForecastId; } this.contextChartSelected({ from: focusRange[0], to: focusRange[1] }); } this.setState(stateUpdate); } }; const nonBlankEntities = entityControls.filter(entity => { return entity.fieldValue !== null; }); if (modelPlotEnabled === false && (0, _job_utils.isSourceDataChartableForDetector)(selectedJob, detectorIndex) === false && nonBlankEntities.length > 0) { // For detectors where model plot has been enabled with a terms filter and the // selected entity(s) are not in the terms list, indicate that data cannot be viewed. stateUpdate.hasResults = false; stateUpdate.loading = false; stateUpdate.dataNotChartable = true; this.setState(stateUpdate); return; } // Calculate the aggregation interval for the context chart. // Context chart swimlane will display bucket anomaly score at the same interval. stateUpdate.contextAggregationInterval = (0, _timeseriesexplorer_utils.calculateAggregationInterval)(bounds, _timeseriesexplorer_constants.CHARTS_POINT_TARGET, jobs, selectedJob); // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected // to some extent with all detector functions if not searching complete buckets. const searchBounds = (0, _time_buckets.getBoundsRoundedToInterval)(bounds, stateUpdate.contextAggregationInterval, false); // Query 1 - load metric data at low granularity across full time range. // Pass a counter flag into the finish() function to make sure we only process the results // for the most recent call to the load the data in cases where the job selection and time filter // have been altered in quick succession (such as from the job picker with 'Apply time range'). const counter = loadCounter; _timeseries_search_service.mlTimeSeriesSearchService.getMetricData(selectedJob, detectorIndex, nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.asMilliseconds(), functionToPlotByIfMetric).toPromise().then(resp => { const fullRangeChartData = (0, _timeseriesexplorer_utils.processMetricPlotResults)(resp.results, modelPlotEnabled); stateUpdate.contextChartData = fullRangeChartData; finish(counter); }).catch(err => { const errorMsg = _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.metricDataErrorMessage', { defaultMessage: 'Error getting metric data' }); this.displayErrorToastMessages(err, errorMsg); }); // Query 2 - load max record score at same granularity as context chart // across full time range for use in the swimlane. _results_service.mlResultsService.getRecordMaxScoreByTime(selectedJob.job_id, this.getCriteriaFields(detectorIndex, entityControls), searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.asMilliseconds(), functionToPlotByIfMetric).then(resp => { const fullRangeRecordScoreData = (0, _timeseriesexplorer_utils.processRecordScoreResults)(resp.results); stateUpdate.swimlaneData = fullRangeRecordScoreData; finish(counter); }).catch(err => { const errorMsg = _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.bucketAnomalyScoresErrorMessage', { defaultMessage: 'Error getting bucket anomaly scores' }); this.displayErrorToastMessages(err, errorMsg); }); // Query 3 - load details on the chart used in the chart title (charting function and entity(s)). _timeseries_search_service.mlTimeSeriesSearchService.getChartDetails(selectedJob, detectorIndex, entityControls, searchBounds.min.valueOf(), searchBounds.max.valueOf()).then(resp => { stateUpdate.chartDetails = resp.results; finish(counter); }).catch(err => { this.displayErrorToastMessages(err, _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.entityCountsErrorMessage', { defaultMessage: 'Error getting entity counts' })); }); // Plus query for forecast data if there is a forecastId stored in the appState. if (selectedForecastId !== undefined) { awaitingCount++; let aggType = undefined; const detector = selectedJob.analysis_config.detectors[detectorIndex]; const esAgg = (0, _job_utils.mlFunctionToESAggregation)(detector.function); if (modelPlotEnabled === false && (esAgg === 'sum' || esAgg === 'count')) { aggType = { avg: 'sum', max: 'sum', min: 'sum' }; } _forecast_service.mlForecastService.getForecastData(selectedJob, detectorIndex, selectedForecastId, nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.asMilliseconds(), aggType).toPromise().then(resp => { stateUpdate.contextForecastData = (0, _timeseriesexplorer_utils.processForecastResults)(resp.results); finish(counter); }).catch(err => { this.displayErrorToastMessages(err, _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.forecastDataErrorMessage', { defaultMessage: 'Error loading forecast data for forecast ID {forecastId}', values: { forecastId: selectedForecastId } })); }); } }); }); /** * Updates local state of detector related controls from the global state. * @param callback to invoke after a state update. */ (0, _defineProperty2.default)(this, "getControlsForDetector", () => { const { selectedDetectorIndex, selectedEntities, selectedJobId } = this.props; return (0, _get_controls_for_detector.getControlsForDetector)(selectedDetectorIndex, selectedEntities, selectedJobId); }); } getFocusAggregationInterval(selection) { const { selectedJobId } = this.props; const jobs = (0, _timeseriesexplorer_utils.createTimeSeriesJobData)(_job_service.mlJobService.jobs); const selectedJob = _job_service.mlJobService.getJob(selectedJobId); // Calculate the aggregation interval for the focus chart. const bounds = { min: (0, _momentTimezone.default)(selection.from), max: (0, _momentTimezone.default)(selection.to) }; return (0, _timeseriesexplorer_utils.calculateAggregationInterval)(bounds, _timeseriesexplorer_constants.CHARTS_POINT_TARGET, jobs, selectedJob); } /** * Gets focus data for the current component state/ */ getFocusData(selection) { const { selectedJobId, selectedForecastId, selectedDetectorIndex, functionDescription } = this.props; const { modelPlotEnabled } = this.state; const selectedJob = _job_service.mlJobService.getJob(selectedJobId); if ((0, _get_function_description.isMetricDetector)(selectedJob, selectedDetectorIndex) && functionDescription === undefined) { return; } const entityControls = this.getControlsForDetector(); // Calculate the aggregation interval for the focus chart. const bounds = { min: (0, _momentTimezone.default)(selection.from), max: (0, _momentTimezone.default)(selection.to) }; const focusAggregationInterval = this.getFocusAggregationInterval(selection); // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected // to some extent with all detector functions if not searching complete buckets. const searchBounds = (0, _time_buckets.getBoundsRoundedToInterval)(bounds, focusAggregationInterval, false); return (0, _timeseriesexplorer_utils.getFocusData)(this.getCriteriaFields(selectedDetectorIndex, entityControls), selectedDetectorIndex, focusAggregationInterval, selectedForecastId, modelPlotEnabled, entityControls.filter(entity => entity.fieldValue !== null), searchBounds, selectedJob, functionDescription, _timeseriesexplorer_constants.TIME_FIELD_NAME); } /** * Updates criteria fields for API calls, e.g. getAnomaliesTableData * @param detectorIndex * @param entities */ getCriteriaFields(detectorIndex, entities) { // Only filter on the entity if the field has a value. const nonBlankEntities = entities.filter(entity => entity.fieldValue !== null); return [{ fieldName: 'detector_index', fieldValue: detectorIndex }, ...nonBlankEntities]; } loadForJobId(jobId) { const { appStateHandler, selectedDetectorIndex } = this.props; const selectedJob = _job_service.mlJobService.getJob(jobId); if (selectedJob === undefined) { return; } const detectors = (0, _get_viewable_detectors.getViewableDetectors)(selectedJob); // Check the supplied index is valid. const appStateDtrIdx = selectedDetectorIndex; let detectorIndex = appStateDtrIdx !== undefined ? appStateDtrIdx : detectors[0].index; if ((0, _lodash.find)(detectors, { index: detectorIndex }) === undefined) { const warningText = _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.requestedDetectorIndexNotValidWarningMessage', { defaultMessage: 'Requested detector index {detectorIndex} is not valid for job {jobId}', values: { detectorIndex, jobId: selectedJob.job_id } }); if (this.props.toastNotificationService) { this.props.toastNotificationService.displayWarningToast(warningText); } detectorIndex = detectors[0].index; } const detectorId = detectorIndex; if (detectorId !== selectedDetectorIndex) { appStateHandler(_timeseriesexplorer_constants.APP_STATE_ACTION.SET_DETECTOR_INDEX, detectorId); } // Populate the map of jobs / detectors / field formatters for the selected IDs and refresh. _field_format_service.mlFieldFormatService.populateFormats([jobId]); } componentDidMount() { // if timeRange used in the url is incorrect // perhaps due to user's advanced setting using incorrect date-maths const { invalidTimeRangeError } = this.props; if (invalidTimeRangeError) { if (this.props.toastNotificationService) { this.props.toastNotificationService.displayWarningToast(_i18n.i18n.translate('xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout', { defaultMessage: 'The time filter was changed to the full range for this job due to an invalid default time filter. Check the advanced settings for {field}.', values: { field: _settings.ANOMALY_DETECTION_DEFAULT_TIME_RANGE } })); } } // Required to redraw the time series chart when the container is resized. this.resizeChecker = new _public.ResizeChecker(this.resizeRef.current); this.resizeChecker.on('resize', () => { this.resizeHandler(); }); this.resizeHandler(); // Listen for context chart updates. this.subscriptions.add(this.contextChart$.pipe((0, _operators.tap)(selection => { this.setState({ zoomFrom: selection.from, zoomTo: selection.to }); }), (0, _operators.debounceTime)(500), (0, _operators.tap)(selection => { const { contextChartData, contextForecastData, focusChartData, zoomFromFocusLoaded, zoomToFocusLoaded } = this.state; if ((contextChartData === undefined || contextChartData.length === 0) && (contextForecastData === undefined || contextForecastData.length === 0)) { return; } if (this.contextChartSelectedInitCallDone === false && focusChartData === undefined || zoomFromFocusLoaded.getTime() !== selection.from.getTime() || zoomToFocusLoaded.getTime() !== selection.to.getTime()) { this.contextChartSelectedInitCallDone = true; this.setState({ loading: true, fullRefresh: false }); } }), (0, _operators.switchMap)(selection => { const { selectedJobId } = this.props; const jobs = (0, _timeseriesexplorer_utils.createTimeSeriesJobData)(_job_service.mlJobService.jobs); const selectedJob = _job_service.mlJobService.getJob(selectedJobId); // Calculate the aggregation interval for the focus chart. const bounds = { min: (0, _momentTimezone.default)(selection.from), max: (0, _momentTimezone.default)(selection.to) }; const focusAggregationInterval = (0, _timeseriesexplorer_utils.calculateAggregationInterval)(bounds, _timeseriesexplorer_constants.CHARTS_POINT_TARGET, jobs, selectedJob); // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected // to some extent with all detector functions if not searching complete buckets. const searchBounds = (0, _time_buckets.getBoundsRoundedToInterval)(bounds, focusAggregationInterval, false); return (0, _rxjs.forkJoin)([this.getFocusData(selection), // Load the data for the anomalies table. this.loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf())]); }), (0, _operators.withLatestFrom)(this.contextChart$)).subscribe(([[refreshFocusData, tableData], selection]) => { const { modelPlotEnabled } = this.state; // All the data is ready now for a state update. this.setState({ focusAggregationInterval: this.getFocusAggregationInterval({ from: selection.from, to: selection.to }), loading: false, showModelBoundsCheckbox: modelPlotEnabled && refreshFocusData.focusChartData.length > 0, zoomFromFocusLoaded: selection.from, zoomToFocusLoaded: selection.to, ...refreshFocusData, ...tableData }); })); this.componentDidUpdate(); } componentDidUpdate(previousProps) { if (previousProps === undefined || previousProps.selectedJobId !== this.props.selectedJobId) { const selectedJob = _job_service.mlJobService.getJob(this.props.selectedJobId); this.contextChartSelectedInitCallDone = false; (0, _explorer_utils.getDataViewsAndIndicesWithGeoFields)([selectedJob], this.props.dataViewsService).then(({ getSourceIndicesWithGeoFieldsResp }) => this.setState({ fullRefresh: false, loading: true, sourceIndicesWithGeoFields: getSourceIndicesWithGeoFieldsResp }, () => { this.loadForJobId(this.props.selectedJobId); })).catch(console.error); // eslint-disable-line no-console } if (previousProps === undefined || previousProps.selectedForecastId !== this.props.selectedForecastId) { if (this.props.selectedForecastId !== undefined) { // Ensure the forecast data will be shown if hidden previously. this.setState({ showForecast: true }); // Not best practice but we need the previous value for another comparison // once all the data was loaded. if (previousProps !== undefined) { this.previousSelectedForecastId = previousProps.selectedForecastId; } } } if (previousProps === undefined || !(0, _lodash.isEqual)(previousProps.bounds, this.props.bounds) || !(0, _lodash.isEqual)(previousProps.lastRefresh, this.props.lastRefresh) && previousProps.lastRefresh !== 0 || !(0, _lodash.isEqual)(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) || !(0, _lodash.isEqual)(previousProps.selectedEntities, this.props.selectedEntities) || previousProps.selectedForecastId !== this.props.selectedForecastId || previousProps.selectedJobId !== this.props.selectedJobId || previousProps.functionDescription !== this.props.functionDescription) { const fullRefresh = previousProps === undefined || !(0, _lodash.isEqual)(previousProps.bounds, this.props.bounds) || !(0, _lodash.isEqual)(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) || !(0, _lodash.isEqual)(previousProps.selectedEntities, this.props.selectedEntities) || previousProps.selectedForecastId !== this.props.selectedForecastId || previousProps.selectedJobId !== this.props.selectedJobId || previousProps.functionDescription !== this.props.functionDescription; this.loadSingleMetricData(fullRefresh); } if (previousProps === undefined) { return; } // Reload the anomalies table if the Interval or Threshold controls are changed. const tableControlsListener = () => { const { zoomFrom, zoomTo } = this.state; if (zoomFrom !== undefined && zoomTo !== undefined) { this.loadAnomaliesTableData(zoomFrom.getTime(), zoomTo.getTime()).subscribe(res => this.setState(res)); } }; if (previousProps.tableInterval !== this.props.tableInterval || previousProps.tableSeverity !== this.props.tableSeverity) { tableControlsListener(); } } componentWillUnmount() { this.subscriptions.unsubscribe(); this.resizeChecker.destroy(); this.unmounted = true; } render() { const { autoZoomDuration, bounds, dateFormatTz, lastRefresh, selectedDetectorIndex, selectedJobId } = this.props; const { chartDetails, contextAggregationInterval, contextChartData, contextForecastData, dataNotChartable, focusAggregationInterval, focusAnnotationError, focusAnnotationData, focusChartData, focusForecastData, fullRefresh, hasResults, loading, modelPlotEnabled, showAnnotations, showAnnotationsCheckbox, showForecast, showForecastCheckbox, showModelBounds, showModelBoundsCheckbox, svgWidth, swimlaneData, tableData, zoomFrom, zoomTo, zoomFromFocusLoaded, zoomToFocusLoaded, chartDataError, sourceIndicesWithGeoFields } = this.state; const chartProps = { modelPlotEnabled, contextChartData, contextChartSelected: this.contextChartSelected, contextForecastData, contextAggregationInterval, swimlaneData, focusAnnotationData, focusChartData, focusForecastData, focusAggregationInterval, svgWidth, zoomFrom, zoomTo, zoomFromFocusLoaded, zoomToFocusLoaded, autoZoomDuration }; const jobs = (0, _timeseriesexplorer_utils.createTimeSeriesJobData)(_job_service.mlJobService.jobs); if (selectedDetectorIndex === undefined || _job_service.mlJobService.getJob(selectedJobId) === undefined) { return /*#__PURE__*/_react.default.createElement(_timeseriesexplorer_page.TimeSeriesExplorerPage, { dateFormatTz: dateFormatTz, resizeRef: this.resizeRef }, /*#__PURE__*/_react.default.createElement(_components.ExplorerNoJobsSelected, null)); } const selectedJob = _job_service.mlJobService.getJob(selectedJobId); const entityControls = this.getControlsForDetector(); const fieldNamesWithEmptyValues = this.getFieldNamesWithEmptyValues(); const arePartitioningFieldsProvided = this.arePartitioningFieldsProvided(); const detectors = (0, _get_viewable_detectors.getViewableDetectors)(selectedJob); let renderFocusChartOnly = true; if ((0, _lodash.isEqual)(this.previousChartProps.focusForecastData, chartProps.focusForecastData) && (0, _lodash.isEqual)(this.previousChartProps.focusChartData, chartProps.focusChartData) && (0, _lodash.isEqual)(this.previousChartProps.focusAnnotationData, chartProps.focusAnnotationData) && this.previousShowForecast === showForecast && this.previousShowModelBounds === showModelBounds && this.props.previousRefresh === lastRefresh) { renderFocusChartOnly = false; } this.previousChartProps = chartProps; this.previousShowForecast = showForecast; this.previousShowModelBounds = showModelBounds; return /*#__PURE__*/_react.default.createElement(_timeseriesexplorer_page.TimeSeriesExplorerPage, { dateFormatTz: dateFormatTz, resizeRef: this.resizeRef }, fieldNamesWithEmptyValues.length > 0 && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_eui.EuiCallOut, { title: /*#__PURE__*/_react.default.createElement(_i18nReact.FormattedMessage, { id: "xpack.ml.timeSeriesExplorer.singleMetricRequiredMessage", defaultMessage: "To view a single metric, select {missingValuesCount, plural, one {a value for {fieldName1}} other {values for {fieldName1} and {fieldName2}}}.", values: { missingValuesCount: fieldNamesWithEmptyValues.length, fieldName1: fieldNamesWithEmptyValues[0], fieldName2: fieldNamesWithEmptyValues[1] } }), iconType: "help", size: "s" }), /*#__PURE__*/_react.default.createElement(_eui.EuiSpacer, { size: "m" })), /*#__PURE__*/_react.default.createElement(_series_controls.SeriesControls, { selectedJobId: selectedJobId, appStateHandler: this.props.appStateHandler, selectedDetectorIndex: selectedDetectorIndex, selectedEntities: this.props.selectedEntities, bounds: bounds, functionDescription: this.props.functionDescription, setFunctionDescription: this.setFunctionDescription }, arePartitioningFieldsProvided && /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { style: { textAlign: 'right' } }, /*#__PURE__*/_react.default.createElement(_eui.EuiFormRow, { hasEmptyLabelSpace: true, style: { maxWidth: '100%' } }, /*#__PURE__*/_react.default.createElement(_forecasting_modal.ForecastingModal, { job: selectedJob, detectorIndex: selectedDetectorIndex, entities: entityControls, setForecastId: this.setForecastId, className: "forecast-controls" })))), /*#__PURE__*/_react.default.createElement(_eui.EuiSpacer, { size: "m" }), fullRefresh && loading === true && /*#__PURE__*/_react.default.createElement(_loading_indicator.LoadingIndicator, { label: _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.loadingLabel', { defaultMessage: 'Loading' }) }), loading === false && chartDataError !== undefined && /*#__PURE__*/_react.default.createElement(_timeseriesexplorer_chart_data_error.TimeseriesexplorerChartDataError, { errorMsg: chartDataError }), arePartitioningFieldsProvided && jobs.length > 0 && (fullRefresh === false || loading === false) && hasResults === false && chartDataError === undefined && /*#__PURE__*/_react.default.createElement(_timeseriesexplorer_no_chart_data.TimeseriesexplorerNoChartData, { dataNotChartable: dataNotChartable, entities: entityControls }), arePartitioningFieldsProvided && jobs.length > 0 && (fullRefresh === false || loading === false) && hasResults === true && /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(_eui.EuiFlexGroup, { gutterSize: "xs", alignItems: "center" }, /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react.default.createElement(_eui.EuiTitle, { size: 'xs' }, /*#__PURE__*/_react.default.createElement("h2", null, /*#__PURE__*/_react.default.createElement("span", null, _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.singleTimeSeriesAnalysisTitle', { defaultMessage: 'Single time series analysis of {functionLabel}', values: { functionLabel: chartDetails.functionLabel } })), "\xA0", chartDetails.entityData.count === 1 && /*#__PURE__*/_react.default.createElement(_eui.EuiTextColor, { color: 'success', size: 's', component: 'span' }, chartDetails.entityData.entities.length > 0 && '(', chartDetails.entityData.entities.map(entity => { return `${entity.fieldName}: ${entity.fieldValue}`; }).join(', '), chartDetails.entityData.entities.length > 0 && ')'), chartDetails.entityData.count !== 1 && /*#__PURE__*/_react.default.createElement(_eui.EuiTextColor, { color: 'success', size: 's', component: 'span' }, chartDetails.entityData.entities.map((countData, i) => { return /*#__PURE__*/_react.default.createElement(_react.Fragment, { key: countData.fieldName }, _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.countDataInChartDetailsDescription', { defaultMessage: '{openBrace}{cardinalityValue} distinct {fieldName} {cardinality, plural, one {} other { values}}{closeBrace}', values: { openBrace: i === 0 ? '(' : '', closeBrace: i === chartDetails.entityData.entities.length - 1 ? ')' : '', cardinalityValue: countData.cardinality === 0 ? allValuesLabel : countData.cardinality, cardinality: countData.cardinality, fieldName: countData.fieldName } }), i !== chartDetails.entityData.entities.length - 1 ? ', ' : ''); }))))), /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react.default.createElement(_timeseriesexplorer_help_popover.TimeSeriesExplorerHelpPopover, null))), /*#__PURE__*/_react.default.createElement(_eui.EuiFlexGroup, { style: { float: 'right' } }, showModelBoundsCheckbox && /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react.default.createElement(_eui.EuiCheckbox, { id: "toggleModelBoundsCheckbox", label: _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.showModelBoundsLabel', { defaultMessage: 'show model bounds' }), checked: showModelBounds, onChange: this.toggleShowModelBoundsHandler })), showAnnotationsCheckbox && /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react.default.createElement(_eui.EuiCheckbox, { id: "toggleAnnotationsCheckbox", label: _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.annotationsLabel', { defaultMessage: 'annotations' }), checked: showAnnotations, onChange: this.toggleShowAnnotationsHandler })), showForecastCheckbox && /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react.default.createElement(_eui.EuiCheckbox, { id: "toggleShowForecastCheckbox", label: /*#__PURE__*/_react.default.createElement("span", { "data-test-subj": 'mlForecastCheckbox' }, _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', { defaultMessage: 'show forecast' })), checked: showForecast, onChange: this.toggleShowForecastHandler }))), /*#__PURE__*/_react.default.createElement(_timeseries_chart_with_tooltip.TimeSeriesChartWithTooltips, { chartProps: chartProps, contextAggregationInterval: contextAggregationInterval, bounds: bounds, detectorIndex: selectedDetectorIndex, renderFocusChartOnly: renderFocusChartOnly, selectedJob: selectedJob, selectedEntities: this.props.selectedEntities, showAnnotations: showAnnotations, showForecast: showForecast, showModelBounds: showModelBounds, lastRefresh: lastRefresh }), focusAnnotationError !== undefined && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_eui.EuiTitle, { "data-test-subj": "mlAnomalyExplorerAnnotations error", size: 'xs' }, /*#__PURE__*/_react.default.createElement("h2", null, /*#__PURE__*/_react.default.createElement(_i18nReact.FormattedMessage, { id: "xpack.ml.timeSeriesExplorer.annotationsErrorTitle", defaultMessage: "Annotations" }))), /*#__PURE__*/_react.default.createElement(_eui.EuiPanel, null, /*#__PURE__*/_react.default.createElement(_eui.EuiCallOut, { title: _i18n.i18n.translate('xpack.ml.timeSeriesExplorer.annotationsErrorCallOutTitle', { defaultMessage: 'An error occurred loading annotations:' }), color: "danger", iconType: "warning" }, /*#__PURE__*/_react.default.createElement("p", null, focusAnnotationError))), /*#__PURE__*/_react.default.createElement(_eui.EuiSpacer, { size: "m" })), focusAnnotationData && focusAnnotationData.length > 0 && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_eui.EuiAccordion, { id: 'mlAnnotationsAccordion', buttonContent: /*#__PURE__*/_react.default.createElement(_eui.EuiTitle, { size: 'xs' }, /*#__PURE__*/_react.default.createElement("h2", null, /*#__PURE__*/_react.default.createElement(_i18nReact.FormattedMessage, { id: "xpack.ml.timeSeriesExplorer.annotationsTitle", defaultMessage: "Annotations {badge}", values: { badge: /*#__PURE__*/_react.default.createElement(_eui.EuiBadge, { color: 'hollow' }, /*#__PURE__*/_react.default.createElement(_i18nReact.FormattedMessage, { id: "xpack.ml.explorer.annotationsTitleTotalCount", defaultMessage: "Total: {count}", values: { count: focusAnnotationData.length } })) } }))), "data-test-subj": "mlAnomalyExplorerAnnotations loaded" }, /*#__PURE__*/_react.default.createElement(_annotations_table.AnnotationsTable, { chartDetails: chartDetails, detectorIndex: selectedDetectorIndex, detectors: detectors, jobIds: [this.props.selectedJobId], annotations: focusAnnotationData, isSingleMetricViewerLinkVisible: false, isNumberBadgeVisible: true })), /*#__PURE__*/_react.default.createElement(_eui.EuiSpacer, { size: "m" })), /*#__PURE__*/_react.default.createElement(_annotation_flyout.AnnotationFlyout, { chartDetails: chartDetails, detectorIndex: selectedDetectorIndex, detectors: detectors }), /*#__PURE__*/_react.default.createElement(_eui.EuiTitle, { size: 'xs' }, /*#__PURE__*/_react.default.createElement("h2", null, /*#__PURE__*/_react.default.createElement(_i18nReact.FormattedMessage, { id: "xpack.ml.timeSeriesExplorer.anomaliesTitle", defaultMessage: "Anomalies" }))), /*#__PURE__*/_react.default.createElement(_eui.EuiSpacer, { size: "s" }), /*#__PURE__*/_react.default.createElement(_eui.EuiFlexGroup, { direction: "row", gutterSize: "l", responsive: true }, /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react.default.createElement(_select_severity.SelectSeverity, null)), /*#__PURE__*/_react.default.createElement(_eui.EuiFlexItem, { grow: false }, /*#__PURE__*/_react.default.createElement(_select_interval.SelectInterval, null))), /*#__PURE__*/_react.default.createElement(_eui.EuiSpacer, { size: "m" })), arePartitioningFieldsProvided && jobs.length > 0 && hasResults === true && /*#__PURE__*/_react.default.createElement(_anomalies_table.AnomaliesTable, { bounds: bounds, tableData: tableData, filter: this.tableFilter, sourceIndicesWithGeoFields: sourceIndicesWithGeoFields, selectedJobs: [{ id: selectedJob.job_id, modelPlotEnabled }] })); } } exports.TimeSeriesExplorer = TimeSeriesExplorer; (0, _defineProperty2.default)(TimeSeriesExplorer, "propTypes", { appStateHandler: _propTypes.default.func.isRequired, autoZoomDuration: _propTypes.default.number.isRequired, bounds: _propTypes.default.object.isRequired, dateFormatTz: _propTypes.default.string.isRequired, lastRefresh: _propTypes.default.number.isRequired, previousRefresh: _propTypes.default.number.isRequired, selectedJobId: _propTypes.default.string.isRequired, selectedDetectorIndex: _propTypes.default.number, selectedEntities: _propTypes.default.object, selectedForecastId: _propTypes.default.string, tableInterval: _propTypes.default.string, tableSeverity: _propTypes.default.number, zoom: _propTypes.default.object, toastNotificationService: _propTypes.default.object, dataViewsService: _propTypes.default.object });