From 022c1d6b07fecaf9e68079efdc94c398a752f352 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 25 Sep 2019 17:00:01 +0100 Subject: [PATCH] [ML] Combines ui/time_buckets into MlTimeBuckets (#46227) * [ML] Combines ui/time_buckets into MlTimeBuckets * [ML] Add unit tests for ml calc_auto_interval * [ML] Rename MlTimeBuckets to TimeBuckets --- .../datavisualizer/index_based/page.tsx | 4 +- .../plugins/ml/public/explorer/explorer.js | 14 +- .../explorer_chart_distribution.js | 4 +- .../explorer_chart_distribution.test.js | 2 +- .../explorer_chart_single_metric.js | 4 +- .../explorer_chart_single_metric.test.js | 2 +- .../explorer_charts_container.test.js | 2 +- .../ml/public/explorer/explorer_controller.js | 4 +- .../explorer_react_wrapper_directive.js | 2 +- .../ml/public/explorer/explorer_swimlane.js | 6 +- .../public/explorer/explorer_swimlane.test.js | 20 +- .../event_rate_chart_directive.js | 2 +- .../components/utils/chart_data_utils.js | 4 +- .../create_job/create_job_chart_directive.js | 2 +- .../create_job/create_job_controller.js | 4 +- .../create_job/create_job_chart_directive.js | 2 +- .../create_job/create_job_controller.js | 4 +- .../create_job/create_job_service.js | 6 +- .../create_job/create_job_chart_directive.js | 2 +- .../create_job/create_job_controller.js | 4 +- .../common/results_loader/results_loader.ts | 6 +- .../components/charts/common/settings.ts | 8 +- .../pages/components/job_creator_context.ts | 6 +- .../jobs/new_job_new/pages/new_job/page.tsx | 4 +- .../jobs/new_job_new/pages/new_job/wizard.tsx | 4 +- .../timeseries_chart/timeseries_chart.js | 2 +- .../timeseries_chart/timeseries_chart.test.js | 5 +- .../timeseriesexplorer/timeseriesexplorer.js | 2 +- .../timeseriesexplorer_utils.js | 6 +- .../util/__tests__/calc_auto_interval.js | 145 +++++++ .../public/util/__tests__/ml_time_buckets.js | 8 +- ...auto_interval.js => calc_auto_interval.js} | 2 +- .../plugins/ml/public/util/ml_time_buckets.js | 185 -------- ...ml_time_buckets.d.ts => time_buckets.d.ts} | 2 +- .../plugins/ml/public/util/time_buckets.js | 404 ++++++++++++++++++ 35 files changed, 624 insertions(+), 259 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/util/__tests__/calc_auto_interval.js rename x-pack/legacy/plugins/ml/public/util/{ml_calc_auto_interval.js => calc_auto_interval.js} (98%) delete mode 100644 x-pack/legacy/plugins/ml/public/util/ml_time_buckets.js rename x-pack/legacy/plugins/ml/public/util/{ml_time_buckets.d.ts => time_buckets.d.ts} (95%) create mode 100644 x-pack/legacy/plugins/ml/public/util/time_buckets.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx index 1291d169f8365..e2837ea9de1fc 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx @@ -35,7 +35,7 @@ import { useKibanaContext, SavedSearchQuery } from '../../contexts/kibana'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; // @ts-ignore import { timeBasedIndexCheck } from '../../util/index_utils'; -import { MlTimeBuckets } from '../../util/ml_time_buckets'; +import { TimeBuckets } from '../../util/time_buckets'; import { FieldRequestConfig, FieldVisConfig } from './common'; import { ActionsPanel } from './components/actions_panel'; import { FieldsPanel } from './components/fields_panel'; @@ -269,7 +269,7 @@ export const Page: FC = () => { // Obtain the interval to use for date histogram aggregations // (such as the document count chart). Aim for 75 bars. - const buckets = new MlTimeBuckets(); + const buckets = new TimeBuckets(); const tf = timefilter as any; let earliest: number | undefined; diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/explorer/explorer.js index 44f0f80d835a0..87900403fa685 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer.js @@ -36,7 +36,7 @@ import { import { ExplorerSwimlane } from './explorer_swimlane'; import { KqlFilterBar } from '../components/kql_filter_bar'; import { formatHumanReadableDateTime } from '../util/date_utils'; -import { getBoundsRoundedToInterval } from 'plugins/ml/util/ml_time_buckets'; +import { getBoundsRoundedToInterval } from '../util/time_buckets'; import { getSelectedJobIds } from '../components/job_selector/job_select_service_utils'; import { InfluencersList } from '../components/influencers_list'; import { ALLOW_CELL_RANGE_SELECTION, dragSelect$, explorer$ } from './explorer_dashboard_service'; @@ -159,7 +159,7 @@ export const Explorer = injectI18n(injectObservablesAsProps( dateFormatTz: PropTypes.string.isRequired, globalState: PropTypes.object.isRequired, jobSelectService: PropTypes.object.isRequired, - MlTimeBuckets: PropTypes.func.isRequired, + TimeBuckets: PropTypes.func.isRequired, }; state = getExplorerDefaultState(); @@ -365,13 +365,13 @@ export const Explorer = injectI18n(injectObservablesAsProps( } getSwimlaneBucketInterval(selectedJobs) { - const { MlTimeBuckets } = this.props; + const { TimeBuckets } = this.props; const swimlaneWidth = getSwimlaneContainerWidth(this.state.noInfluencersConfigured); // Bucketing interval should be the maximum of the chart related interval (i.e. time range related) // and the max bucket span for the jobs shown in the chart. const bounds = timefilter.getActiveBounds(); - const buckets = new MlTimeBuckets(); + const buckets = new TimeBuckets(); buckets.setInterval('auto'); buckets.setBounds(bounds); @@ -1074,7 +1074,7 @@ export const Explorer = injectI18n(injectObservablesAsProps( globalState, intl, jobSelectService, - MlTimeBuckets, + TimeBuckets, } = this.props; const { @@ -1206,7 +1206,7 @@ export const Explorer = injectI18n(injectObservablesAsProps( chartWidth={swimlaneWidth} filterActive={filterActive} maskAll={maskAll} - MlTimeBuckets={MlTimeBuckets} + TimeBuckets={TimeBuckets} swimlaneCellClick={this.swimlaneCellClick} swimlaneData={overallSwimlaneData} swimlaneType={SWIMLANE_TYPE.OVERALL} @@ -1282,7 +1282,7 @@ export const Explorer = injectI18n(injectObservablesAsProps( chartWidth={swimlaneWidth} filterActive={filterActive} maskAll={maskAll} - MlTimeBuckets={MlTimeBuckets} + TimeBuckets={TimeBuckets} swimlaneCellClick={this.swimlaneCellClick} swimlaneData={viewBySwimlaneData} swimlaneType={SWIMLANE_TYPE.VIEW_BY} diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js index a38c8428e8d06..67efbb848b193 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js @@ -28,8 +28,8 @@ import { numTicksForDateFormat, removeLabelOverlap } from '../../util/chart_utils'; -import { TimeBuckets } from 'ui/time_buckets'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; +import { TimeBuckets } from '../../util/time_buckets'; import { mlEscape } from '../../util/string_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; @@ -255,7 +255,7 @@ export const ExplorerChartDistribution = injectI18n(class ExplorerChartDistribut const xAxisTickFormat = timeBuckets.getScaledDateFormat(); const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); - // +1 ms to account for the ms that was substracted for query aggregations. + // +1 ms to account for the ms that was subtracted for query aggregations. const interval = config.selectedLatest - config.selectedEarliest + 1; const tickValues = getTickValues(tickValuesStart, interval, config.plotEarliest, config.plotLatest); diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.js index 62841897bc8fc..4e083fafa97ac 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -10,7 +10,7 @@ import seriesConfig from './__mocks__/mock_series_config_rare.json'; // Mock TimeBuckets and mlFieldFormatService, they don't play well // with the jest based test setup yet. -jest.mock('ui/time_buckets', () => ({ +jest.mock('../../util/time_buckets', () => ({ TimeBuckets: function () { this.setBounds = jest.fn(); this.setInterval = jest.fn(); diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js index 74ba4195308d6..f7e05924f9e6a 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js @@ -36,8 +36,8 @@ import { showMultiBucketAnomalyMarker, showMultiBucketAnomalyTooltip, } from '../../util/chart_utils'; -import { TimeBuckets } from 'ui/time_buckets'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; +import { TimeBuckets } from '../../util/time_buckets'; import { mlEscape } from '../../util/string_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; @@ -194,7 +194,7 @@ export const ExplorerChartSingleMetric = injectI18n(class ExplorerChartSingleMet const xAxisTickFormat = timeBuckets.getScaledDateFormat(); const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); - // +1 ms to account for the ms that was substracted for query aggregations. + // +1 ms to account for the ms that was subtracted for query aggregations. const interval = config.selectedLatest - config.selectedEarliest + 1; const tickValues = getTickValues(tickValuesStart, interval, config.plotEarliest, config.plotLatest); diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js index d65d542ad9ca3..83d4fda0858a2 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -10,7 +10,7 @@ import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; // Mock TimeBuckets and mlFieldFormatService, they don't play well // with the jest based test setup yet. -jest.mock('ui/time_buckets', () => ({ +jest.mock('../../util/time_buckets', () => ({ TimeBuckets: function () { this.setBounds = jest.fn(); this.setInterval = jest.fn(); diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js index 0c410f418b2af..fc660f543b2cc 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js @@ -11,7 +11,7 @@ import seriesConfigRare from './__mocks__/mock_series_config_rare.json'; // Mock TimeBuckets and mlFieldFormatService, they don't play well // with the jest based test setup yet. -jest.mock('ui/time_buckets', () => ({ +jest.mock('../../util/time_buckets', () => ({ TimeBuckets: function () { this.setBounds = jest.fn(); this.setInterval = jest.fn(); diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js index 693c884a7bb90..65da5382eef66 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js @@ -24,7 +24,7 @@ import { getAnomalyExplorerBreadcrumbs } from './breadcrumbs'; import { checkFullLicense } from '../license/check_license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; import { getIndexPatterns, loadIndexPatterns } from '../util/index_utils'; -import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; +import { TimeBuckets } from 'plugins/ml/util/time_buckets'; import { explorer$ } from './explorer_dashboard_service'; import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service'; import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; @@ -74,7 +74,7 @@ module.controller('MlExplorerController', function ( timefilter.enableTimeRangeSelector(); timefilter.enableAutoRefreshSelector(); - $scope.MlTimeBuckets = MlTimeBuckets; + $scope.TimeBuckets = TimeBuckets; let resizeTimeout = null; diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_react_wrapper_directive.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_react_wrapper_directive.js index 0f6a8dcaa0b5a..40213a0649667 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_react_wrapper_directive.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_react_wrapper_directive.js @@ -39,7 +39,7 @@ module.directive('mlExplorerReactWrapper', function (config, globalState) { dateFormatTz, globalState, jobSelectService, - MlTimeBuckets: scope.MlTimeBuckets, + TimeBuckets: scope.TimeBuckets, }} /> , diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js index 5528d03e6af45..d8453db10f450 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js @@ -39,7 +39,7 @@ export const ExplorerSwimlane = injectI18n(class ExplorerSwimlane extends React. chartWidth: PropTypes.number.isRequired, filterActive: PropTypes.bool, maskAll: PropTypes.bool, - MlTimeBuckets: PropTypes.func.isRequired, + TimeBuckets: PropTypes.func.isRequired, swimlaneCellClick: PropTypes.func.isRequired, swimlaneData: PropTypes.shape({ laneLabels: PropTypes.array.isRequired @@ -248,7 +248,7 @@ export const ExplorerSwimlane = injectI18n(class ExplorerSwimlane extends React. chartWidth, filterActive, maskAll, - MlTimeBuckets, + TimeBuckets, swimlaneCellClick, swimlaneData, swimlaneType, @@ -285,7 +285,7 @@ export const ExplorerSwimlane = injectI18n(class ExplorerSwimlane extends React. .range([0, xAxisWidth]); // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new MlTimeBuckets(); + const timeBuckets = new TimeBuckets(); timeBuckets.setInterval(`${stepSecs}s`); const xAxisTickFormat = timeBuckets.getScaledDateFormat(); diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.js b/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.js index 6071e88cfc890..7063368bc2fe4 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.js @@ -30,17 +30,17 @@ jest.mock('./explorer_dashboard_service', () => ({ })); function getExplorerSwimlaneMocks() { - const MlTimeBucketsMethods = { + const TimeBucketsMethods = { setInterval: jest.fn(), getScaledDateFormat: jest.fn() }; - const MlTimeBuckets = jest.fn(() => MlTimeBucketsMethods); - MlTimeBuckets.mockMethods = MlTimeBucketsMethods; + const TimeBuckets = jest.fn(() => TimeBucketsMethods); + TimeBuckets.mockMethods = TimeBucketsMethods; const swimlaneData = { laneLabels: [] }; return { - MlTimeBuckets, + TimeBuckets, swimlaneData }; } @@ -65,7 +65,7 @@ describe('ExplorerSwimlane', () => { const wrapper = mountWithIntl( { // test calls to mock functions expect(dragSelect$.subscribe.mock.calls.length).toBeGreaterThanOrEqual(1); expect(wrapper.instance().dragSelectSubscriber.unsubscribe.mock.calls).toHaveLength(0); - expect(mocks.MlTimeBuckets.mockMethods.setInterval.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(mocks.TimeBuckets.mockMethods.setInterval.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(mocks.TimeBuckets.mockMethods.getScaledDateFormat.mock.calls.length).toBeGreaterThanOrEqual(1); expect(swimlaneRenderDoneListener.mock.calls.length).toBeGreaterThanOrEqual(1); }); @@ -91,7 +91,7 @@ describe('ExplorerSwimlane', () => { const wrapper = mountWithIntl( { // test calls to mock functions expect(dragSelect$.subscribe.mock.calls.length).toBeGreaterThanOrEqual(1); expect(wrapper.instance().dragSelectSubscriber.unsubscribe.mock.calls).toHaveLength(0); - expect(mocks.MlTimeBuckets.mockMethods.setInterval.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(mocks.MlTimeBuckets.mockMethods.getScaledDateFormat.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(mocks.TimeBuckets.mockMethods.setInterval.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(mocks.TimeBuckets.mockMethods.getScaledDateFormat.mock.calls.length).toBeGreaterThanOrEqual(1); expect(swimlaneRenderDoneListener.mock.calls.length).toBeGreaterThanOrEqual(1); }); }); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/event_rate_chart_directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/event_rate_chart_directive.js index 0921924e3ccd5..197a4b7605155 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/event_rate_chart_directive.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/event_rate_chart/event_rate_chart_directive.js @@ -16,7 +16,7 @@ import d3 from 'd3'; import angular from 'angular'; import moment from 'moment'; -import { TimeBuckets } from 'ui/time_buckets'; +import { TimeBuckets } from '../../../../../util/time_buckets'; import { numTicksForDateFormat } from 'plugins/ml/util/chart_utils'; import { uiModules } from 'ui/modules'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/utils/chart_data_utils.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/utils/chart_data_utils.js index 0a2a798e63d33..8692505d42575 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/utils/chart_data_utils.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/utils/chart_data_utils.js @@ -9,7 +9,7 @@ // various util functions for populating the chartData object used by the job wizards import _ from 'lodash'; -import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; +import { TimeBuckets } from 'plugins/ml/util/time_buckets'; import { calculateTextWidth } from 'plugins/ml/util/string_utils'; import { mlResultsService } from 'plugins/ml/services/results_service'; import { mlSimpleJobSearchService } from 'plugins/ml/jobs/new_job/simple/components/utils/search_service'; @@ -24,7 +24,7 @@ export function ChartDataUtilsProvider() { const MAX_BARS = BAR_TARGET + (BAR_TARGET / 100) * 100; // 100% larger that bar target const query = formConfig.combinedQuery; const bounds = timefilter.getActiveBounds(); - const buckets = new MlTimeBuckets(); + const buckets = new TimeBuckets(); buckets.setBarTarget(BAR_TARGET); buckets.setMaxBars(MAX_BARS); buckets.setInterval('auto'); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_chart_directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_chart_directive.js index 0f9ec3900fc86..0fddf214eaa40 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_chart_directive.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_chart_directive.js @@ -15,7 +15,7 @@ import d3 from 'd3'; import angular from 'angular'; import moment from 'moment'; -import { TimeBuckets } from 'ui/time_buckets'; +import { TimeBuckets } from '../../../../../util/time_buckets'; import { drawLineChartDots, numTicksForDateFormat } from 'plugins/ml/util/chart_utils'; import { uiModules } from 'ui/modules'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_controller.js index fa0553a884f43..fa3b28128e1d3 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_controller.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/multi_metric/create_job/create_job_controller.js @@ -21,7 +21,7 @@ import uiRoutes from 'ui/routes'; import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; import { checkLicenseExpired } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; +import { TimeBuckets } from 'plugins/ml/util/time_buckets'; import { getCreateMultiMetricJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; import { filterAggTypes } from 'plugins/ml/jobs/new_job/simple/components/utils/filter_agg_types'; import { validateJob } from 'plugins/ml/jobs/new_job/simple/components/utils/validate_job'; @@ -292,7 +292,7 @@ module } const bounds = timefilter.getActiveBounds(); - $scope.formConfig.chartInterval = new MlTimeBuckets(); + $scope.formConfig.chartInterval = new TimeBuckets(); $scope.formConfig.chartInterval.setBarTarget(BAR_TARGET); $scope.formConfig.chartInterval.setMaxBars(MAX_BARS); $scope.formConfig.chartInterval.setInterval('auto'); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js index 67345d9b385d0..e02d28f35ff2b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_chart_directive.js @@ -17,7 +17,7 @@ import angular from 'angular'; import moment from 'moment'; import { formatHumanReadableDateTime } from '../../../../../util/date_utils'; -import { TimeBuckets } from 'ui/time_buckets'; +import { TimeBuckets } from '../../../../../util/time_buckets'; import { numTicksForDateFormat } from '../../../../../util/chart_utils'; import { mlEscape } from '../../../../../util/string_utils'; import { mlChartTooltipService } from '../../../../../components/chart_tooltip/chart_tooltip_service'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_controller.js index 9fb36843168dd..6bb8180a55148 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_controller.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_controller.js @@ -20,7 +20,7 @@ import angular from 'angular'; import uiRoutes from 'ui/routes'; import { checkLicenseExpired } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; +import { TimeBuckets } from 'plugins/ml/util/time_buckets'; import { getCreatePopulationJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; import { filterAggTypes } from 'plugins/ml/jobs/new_job/simple/components/utils/filter_agg_types'; import { validateJob } from 'plugins/ml/jobs/new_job/simple/components/utils/validate_job'; @@ -302,7 +302,7 @@ module } const bounds = timefilter.getActiveBounds(); - $scope.formConfig.chartInterval = new MlTimeBuckets(); + $scope.formConfig.chartInterval = new TimeBuckets(); $scope.formConfig.chartInterval.setBarTarget(BAR_TARGET); $scope.formConfig.chartInterval.setMaxBars(MAX_BARS); $scope.formConfig.chartInterval.setInterval('auto'); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_service.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_service.js index baea576e91d35..7f6ec483fd7fd 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_service.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/population/create_job/create_job_service.js @@ -10,7 +10,7 @@ import _ from 'lodash'; import { EVENT_RATE_COUNT_FIELD, WIZARD_TYPE } from 'plugins/ml/jobs/new_job/simple/components/constants/general'; import { ML_MEDIAN_PERCENTS } from 'plugins/ml/../common/util/job_utils'; -import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; +import { TimeBuckets } from 'plugins/ml/util/time_buckets'; import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; import { mlJobService } from 'plugins/ml/services/job_service'; import { createJobForSaving } from 'plugins/ml/jobs/new_job/utils/new_job_utils'; @@ -76,7 +76,7 @@ export function PopulationJobServiceProvider() { }; }); - const searchJson = getSearchJsonFromConfig(formConfig, timefilter, MlTimeBuckets); + const searchJson = getSearchJsonFromConfig(formConfig, timefilter, TimeBuckets); ml.esSearch(searchJson) .then((resp) => { @@ -313,7 +313,7 @@ export function PopulationJobServiceProvider() { function getSearchJsonFromConfig(formConfig) { const bounds = timefilter.getActiveBounds(); - const buckets = new MlTimeBuckets(); + const buckets = new TimeBuckets(); buckets.setInterval('auto'); buckets.setBounds(bounds); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_chart_directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_chart_directive.js index 5754358cd5a64..a06fd3837a21f 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_chart_directive.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_chart_directive.js @@ -15,7 +15,7 @@ import d3 from 'd3'; import angular from 'angular'; import moment from 'moment'; -import { TimeBuckets } from 'ui/time_buckets'; +import { TimeBuckets } from '../../../../../util/time_buckets'; import { drawLineChartDots, numTicksForDateFormat } from 'plugins/ml/util/chart_utils'; import { uiModules } from 'ui/modules'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_controller.js index 9455442f0eddd..e8eec3ede47e1 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_controller.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/single_metric/create_job/create_job_controller.js @@ -22,7 +22,7 @@ import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; import { getSafeAggregationName } from 'plugins/ml/../common/util/job_utils'; import { checkLicenseExpired } from 'plugins/ml/license/check_license'; import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { MlTimeBuckets } from 'plugins/ml/util/ml_time_buckets'; +import { TimeBuckets } from 'plugins/ml/util/time_buckets'; import { getCreateSingleMetricJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; import { filterAggTypes } from 'plugins/ml/jobs/new_job/simple/components/utils/filter_agg_types'; import { validateJob } from 'plugins/ml/jobs/new_job/simple/components/utils/validate_job'; @@ -288,7 +288,7 @@ module } const bounds = timefilter.getActiveBounds(); - $scope.formConfig.chartInterval = new MlTimeBuckets(); + $scope.formConfig.chartInterval = new TimeBuckets(); $scope.formConfig.chartInterval.setBarTarget(BAR_TARGET); $scope.formConfig.chartInterval.setMaxBars(MAX_BARS); $scope.formConfig.chartInterval.setInterval('auto'); 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 7adf84b54c4eb..8f593ce411b45 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 @@ -8,7 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import { parseInterval } from 'ui/utils/parse_interval'; import { JobCreatorType, isMultiMetricJobCreator } from '../job_creator'; import { mlResultsService, ModelPlotOutputResults } from '../../../../services/results_service'; -import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; +import { TimeBuckets } from '../../../../util/time_buckets'; import { getSeverityType } from '../../../../../common/util/anomaly_utils'; import { ANOMALY_SEVERITY } from '../../../../../common/constants/anomalies'; import { getScoresByRecord } from './searches'; @@ -55,7 +55,7 @@ export class ResultsLoader { private _results$: BehaviorSubject; private _resultsSearchRunning = false; private _jobCreator: JobCreatorType; - private _chartInterval: MlTimeBuckets; + private _chartInterval: TimeBuckets; private _lastModelTimeStamp: number = 0; private _lastResultsTimeout: any = null; private _chartLoader: ChartLoader; @@ -69,7 +69,7 @@ export class ResultsLoader { private _detectorSplitFieldFilters: SplitFieldWithValue | null = null; private _splitFieldFiltersLoaded: boolean = false; - constructor(jobCreator: JobCreatorType, chartInterval: MlTimeBuckets, chartLoader: ChartLoader) { + constructor(jobCreator: JobCreatorType, chartInterval: TimeBuckets, chartLoader: ChartLoader) { this._jobCreator = jobCreator; this._chartInterval = chartInterval; this._results$ = new BehaviorSubject(this._results); 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 8e8bd8530c08a..b1852cbb259c1 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 @@ -12,7 +12,7 @@ import { isMultiMetricJobCreator, isPopulationJobCreator, } from '../../../../common/job_creator'; -import { MlTimeBuckets } from '../../../../../../util/ml_time_buckets'; +import { TimeBuckets } from '../../../../../../util/time_buckets'; const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode'); const themeName = IS_DARK_THEME ? darkTheme : lightTheme; @@ -57,7 +57,7 @@ export const seriesStyle = { }, }; -export function getChartSettings(jobCreator: JobCreatorType, chartInterval: MlTimeBuckets) { +export function getChartSettings(jobCreator: JobCreatorType, chartInterval: TimeBuckets) { const cs = { ...defaultChartSettings, intervalMs: chartInterval.getInterval().asMilliseconds(), @@ -65,10 +65,10 @@ export function getChartSettings(jobCreator: JobCreatorType, chartInterval: MlTi if (isPopulationJobCreator(jobCreator)) { // for population charts, use a larger interval based on - // the calculation from MlTimeBuckets, but without the + // the calculation from TimeBuckets, but without the // bar target and max bars which have been set for the // general chartInterval - const interval = new MlTimeBuckets(); + const interval = new TimeBuckets(); interval.setInterval('auto'); interval.setBounds(chartInterval.getBounds()); cs.intervalMs = interval.getInterval().asMilliseconds(); 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 76f9b66f42305..5fd3c98ed54c9 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 @@ -6,7 +6,7 @@ import { createContext } from 'react'; import { Field, Aggregation } from '../../../../../common/types/fields'; -import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; +import { TimeBuckets } from '../../../../util/time_buckets'; import { JobCreatorType, SingleMetricJobCreator } from '../../common/job_creator'; import { ChartLoader } from '../../common/chart_loader'; import { ResultsLoader } from '../../common/results_loader'; @@ -19,7 +19,7 @@ export interface JobCreatorContextValue { jobCreator: JobCreatorType; chartLoader: ChartLoader; resultsLoader: ResultsLoader; - chartInterval: MlTimeBuckets; + chartInterval: TimeBuckets; jobValidator: JobValidator; jobValidatorUpdated: number; fields: Field[]; @@ -33,7 +33,7 @@ export const JobCreatorContext = createContext({ jobCreator: {} as SingleMetricJobCreator, chartLoader: {} as ChartLoader, resultsLoader: {} as ResultsLoader, - chartInterval: {} as MlTimeBuckets, + chartInterval: {} as TimeBuckets, jobValidator: {} as JobValidator, jobValidatorUpdated: 0, fields: [], 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 index a3c249305d8e7..c5aa3c4269314 100644 --- 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 @@ -23,7 +23,7 @@ import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; import { useKibanaContext } from '../../../../contexts/kibana'; import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; -import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; +import { TimeBuckets } from '../../../../util/time_buckets'; import { newJobDefaults } from '../../../new_job/utils/new_job_defaults'; import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_service'; import { expandCombinedJobConfig } from '../../common/job_creator/configs'; @@ -101,7 +101,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { } } - const chartInterval = new MlTimeBuckets(); + const chartInterval = new TimeBuckets(); chartInterval.setBarTarget(BAR_TARGET); chartInterval.setMaxBars(MAX_BARS); chartInterval.setInterval('auto'); 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 fcd1c4808a30a..7c573bfe85270 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 @@ -18,7 +18,7 @@ import { PickFieldsStep } from '../components/pick_fields_step'; import { JobDetailsStep } from '../components/job_details_step'; import { ValidationStep } from '../components/validation_step'; import { SummaryStep } from '../components/summary_step'; -import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; +import { TimeBuckets } from '../../../../util/time_buckets'; import { useKibanaContext } from '../../../../contexts/kibana'; import { JobCreatorContext, JobCreatorContextValue } from '../components/job_creator_context'; @@ -34,7 +34,7 @@ interface Props { jobCreator: JobCreatorType; chartLoader: ChartLoader; resultsLoader: ResultsLoader; - chartInterval: MlTimeBuckets; + chartInterval: TimeBuckets; jobValidator: JobValidator; existingJobsAndGroups: ExistingJobsAndGroups; skipTimeRangeStep: boolean; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index ceb7cfa16f89c..1363342f81518 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -39,7 +39,7 @@ import { showMultiBucketAnomalyTooltip, } from '../../../util/chart_utils'; import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; -import { TimeBuckets } from 'ui/time_buckets'; +import { TimeBuckets } from '../../../util/time_buckets'; import { mlTableService } from '../../../services/table_service'; import { ContextChartMask } from '../context_chart_mask'; import { findChartPointForAnomalyTime } from '../../timeseriesexplorer_utils'; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js index 6374e752951b2..5d02273c4a85d 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js +++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js @@ -25,7 +25,7 @@ jest.mock('ui/chrome', () => ({ }), })); -jest.mock('ui/time_buckets', () => ({ +jest.mock('../../../util/time_buckets', () => ({ TimeBuckets: function () { this.setBounds = jest.fn(); this.setInterval = jest.fn(); @@ -45,7 +45,8 @@ function getTimeseriesChartPropsMock() { showForecast: true, showModelBounds: true, svgWidth: 1600, - timefilter: {} + timefilter: {}, + skipRefresh: false }; } diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js index 6b0babf5c3019..caef7be64d6e0 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js @@ -66,7 +66,7 @@ import { mlResultsService } from '../services/results_service'; import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service'; import { getIndexPatterns } from '../util/index_utils'; -import { getBoundsRoundedToInterval } from '../util/ml_time_buckets'; +import { getBoundsRoundedToInterval } from '../util/time_buckets'; import { APP_STATE_ACTION, CHARTS_POINT_TARGET, TIME_FIELD_NAME } from './timeseriesexplorer_constants'; import { mlTimeSeriesSearchService } from './timeseries_search_service'; diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js index 7a50b52c191a3..ac5d829ded3a9 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js @@ -29,7 +29,7 @@ import { import { ml } from '../services/ml_api_service'; import { mlForecastService } from '../services/forecast_service'; import { mlResultsService } from '../services/results_service'; -import { MlTimeBuckets, getBoundsRoundedToInterval } from '../util/ml_time_buckets'; +import { TimeBuckets, getBoundsRoundedToInterval } from '../util/time_buckets'; import { mlTimeSeriesSearchService } from './timeseries_search_service'; @@ -461,7 +461,7 @@ export function calculateAggregationInterval( const barTarget = (bucketsTarget !== undefined ? bucketsTarget : 100); // Use a maxBars of 10% greater than the target. const maxBars = Math.floor(1.1 * barTarget); - const buckets = new MlTimeBuckets(); + const buckets = new TimeBuckets(); buckets.setInterval('auto'); buckets.setBounds(bounds); buckets.setBarTarget(Math.floor(barTarget)); @@ -551,7 +551,7 @@ export function getAutoZoomDuration(jobs, selectedJob) { // Use a maxBars of 10% greater than the target. const maxBars = Math.floor(1.1 * CHARTS_POINT_TARGET); - const buckets = new MlTimeBuckets(); + const buckets = new TimeBuckets(); buckets.setInterval('auto'); buckets.setBarTarget(Math.floor(CHARTS_POINT_TARGET)); buckets.setMaxBars(maxBars); diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/calc_auto_interval.js b/x-pack/legacy/plugins/ml/public/util/__tests__/calc_auto_interval.js new file mode 100644 index 0000000000000..f89bb6aaffc48 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/__tests__/calc_auto_interval.js @@ -0,0 +1,145 @@ +/* + * 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 expect from '@kbn/expect'; +import moment from 'moment'; + +import { timeBucketsCalcAutoIntervalProvider } from '../calc_auto_interval'; + + +describe('ML - calc auto intervals', () => { + + const calcAuto = timeBucketsCalcAutoIntervalProvider(); + + describe('near interval', () => { + it('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.near(0, undefined); + expect(interval.asMilliseconds()).to.be(0); + }); + + it('returns 1000ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).to.be(1000); + }); + + it('returns 5m buckets for 8h / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).to.be(5); + }); + + it('returns 15m buckets for 1d / 100 bars', () => { + const interval = calcAuto.near(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).to.be(15); + }); + + it('returns 1h buckets for 20d / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(20, 'd')); + expect(interval.asHours()).to.be(1); + }); + + it('returns 6h buckets for 100d / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(100, 'd')); + expect(interval.asHours()).to.be(6); + }); + + it('returns 24h buckets for 1y / 500 bars', () => { + const interval = calcAuto.near(500, moment.duration(1, 'y')); + expect(interval.asHours()).to.be(24); + }); + + it('returns 12h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.near(1000, moment.duration(1, 'y')); + expect(interval.asHours()).to.be(12); + }); + }); + + describe('lessThan interval', () => { + it('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.lessThan(0, undefined); + expect(interval.asMilliseconds()).to.be(0); + }); + + it('returns 500ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).to.be(500); + }); + + it('returns 5m buckets for 8h / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).to.be(5); + }); + + it('returns 30m buckets for 1d / 100 bars', () => { + const interval = calcAuto.lessThan(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).to.be(30); + }); + + it('returns 1h buckets for 20d / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(20, 'd')); + expect(interval.asHours()).to.be(1); + }); + + it('returns 6h buckets for 100d / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(100, 'd')); + expect(interval.asHours()).to.be(6); + }); + + it('returns 24h buckets for 1y / 500 bars', () => { + const interval = calcAuto.lessThan(500, moment.duration(1, 'y')); + expect(interval.asHours()).to.be(24); + }); + + it('returns 12h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.lessThan(1000, moment.duration(1, 'y')); + expect(interval.asHours()).to.be(12); + }); + }); + + describe('atLeast interval', () => { + it('returns 0ms buckets for undefined / 0 bars', () => { + const interval = calcAuto.atLeast(0, undefined); + expect(interval.asMilliseconds()).to.be(0); + }); + + it('returns 100ms buckets for 60s / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(60, 's')); + expect(interval.asMilliseconds()).to.be(100); + }); + + it('returns 1m buckets for 8h / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(8, 'h')); + expect(interval.asMinutes()).to.be(1); + }); + + it('returns 10m buckets for 1d / 100 bars', () => { + const interval = calcAuto.atLeast(100, moment.duration(1, 'd')); + expect(interval.asMinutes()).to.be(10); + }); + + it('returns 30m buckets for 20d / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(20, 'd')); + expect(interval.asMinutes()).to.be(30); + }); + + it('returns 4h buckets for 100d / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(100, 'd')); + expect(interval.asHours()).to.be(4); + }); + + it('returns 12h buckets for 1y / 500 bars', () => { + const interval = calcAuto.atLeast(500, moment.duration(1, 'y')); + expect(interval.asHours()).to.be(12); + }); + + it('returns 8h buckets for 1y / 1000 bars', () => { + const interval = calcAuto.atLeast(1000, moment.duration(1, 'y')); + expect(interval.asHours()).to.be(8); + }); + }); + +}); diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/ml_time_buckets.js b/x-pack/legacy/plugins/ml/public/util/__tests__/ml_time_buckets.js index a9b7e775b6704..de3ebf27517ff 100644 --- a/x-pack/legacy/plugins/ml/public/util/__tests__/ml_time_buckets.js +++ b/x-pack/legacy/plugins/ml/public/util/__tests__/ml_time_buckets.js @@ -10,9 +10,9 @@ import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import moment from 'moment'; import { - MlTimeBuckets, + TimeBuckets, getBoundsRoundedToInterval, - calcEsInterval } from '../ml_time_buckets'; + calcEsInterval } from '../time_buckets'; describe('ML - time buckets', () => { @@ -23,10 +23,10 @@ describe('ML - time buckets', () => { ngMock.module('kibana'); ngMock.inject(() => { - autoBuckets = new MlTimeBuckets(); + autoBuckets = new TimeBuckets(); autoBuckets.setInterval('auto'); - customBuckets = new MlTimeBuckets(); + customBuckets = new TimeBuckets(); customBuckets.setInterval('auto'); customBuckets.setBarTarget(500); customBuckets.setMaxBars(550); diff --git a/x-pack/legacy/plugins/ml/public/util/ml_calc_auto_interval.js b/x-pack/legacy/plugins/ml/public/util/calc_auto_interval.js similarity index 98% rename from x-pack/legacy/plugins/ml/public/util/ml_calc_auto_interval.js rename to x-pack/legacy/plugins/ml/public/util/calc_auto_interval.js index a5db592cafc3b..4206b900ec677 100644 --- a/x-pack/legacy/plugins/ml/public/util/ml_calc_auto_interval.js +++ b/x-pack/legacy/plugins/ml/public/util/calc_auto_interval.js @@ -6,7 +6,7 @@ -// Based on Kibana ui/time_buckets/calc_auto_interval.js but with +// Based on the original Kibana ui/time_buckets/calc_auto_interval.js but with // a few modifications: // - edit to the near rule, so that it returns either the // upper or lower rule bound, depending on which is closest to the diff --git a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.js b/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.js deleted file mode 100644 index 3af986e3ca5da..0000000000000 --- a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.js +++ /dev/null @@ -1,185 +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. - */ - - - -// custom TimeBuckets which inherits from the standard kibana TimeBuckets -// this adds the ability to override the barTarget and maxBars settings -// allowing for a more granular visualization interval without having to -// modify the global settings stored in the kibana config - -import _ from 'lodash'; -import moment from 'moment'; -import dateMath from '@elastic/datemath'; -import chrome from 'ui/chrome'; - -import { timeBucketsCalcAutoIntervalProvider } from './ml_calc_auto_interval'; -import { inherits } from './inherits'; - -const unitsDesc = dateMath.unitsDesc; -const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals. - -import { TimeBuckets } from 'ui/time_buckets'; - -const config = chrome.getUiSettingsClient(); - -const calcAuto = timeBucketsCalcAutoIntervalProvider(); -inherits(MlTimeBuckets, TimeBuckets); - -export function MlTimeBuckets() { - this.barTarget = config.get('histogram:barTarget'); - this.maxBars = config.get('histogram:maxBars'); - - // return MlTimeBuckets.Super.call(this); -} - -MlTimeBuckets.prototype.setBarTarget = function (bt) { - this.barTarget = bt; -}; - -MlTimeBuckets.prototype.setMaxBars = function (mb) { - this.maxBars = mb; -}; - -MlTimeBuckets.prototype.getInterval = function () { - const self = this; - const duration = self.getDuration(); - return decorateInterval(maybeScaleInterval(readInterval()), duration); - - // either pull the interval from state or calculate the auto-interval - function readInterval() { - const interval = self._i; - if (moment.isDuration(interval)) return interval; - return calcAuto.near(self.barTarget, duration); - } - - // check to see if the interval should be scaled, and scale it if so - function maybeScaleInterval(interval) { - if (!self.hasBounds()) return interval; - - const maxLength = self.maxBars; - const approxLen = duration / interval; - let scaled; - - // If the number of buckets we got back from using the barTarget is less than - // maxBars, than use the lessThan rule to try and get closer to maxBars. - if (approxLen > maxLength) { - scaled = calcAuto.lessThan(maxLength, duration); - } else { - return interval; - } - - if (+scaled === +interval) return interval; - - decorateInterval(interval, duration); - return _.assign(scaled, { - preScaled: interval, - scale: interval / scaled, - scaled: true - }); - } - -}; - -// Returns an interval which in the last step of calculation is rounded to -// the closest multiple of the supplied divisor (in seconds). -MlTimeBuckets.prototype.getIntervalToNearestMultiple = function (divisorSecs) { - const interval = this.getInterval(); - const intervalSecs = interval.asSeconds(); - - const remainder = intervalSecs % divisorSecs; - if (remainder === 0) { - return interval; - } - - // Create a new interval which is a multiple of the supplied divisor (not zero). - let nearestMultiple = remainder > (divisorSecs / 2) ? - intervalSecs + divisorSecs - remainder : intervalSecs - remainder; - nearestMultiple = nearestMultiple === 0 ? divisorSecs : nearestMultiple; - const nearestMultipleInt = moment.duration(nearestMultiple, 'seconds'); - decorateInterval(nearestMultipleInt, this.getDuration()); - - // Check to see if the new interval is scaled compared to the original. - const preScaled = _.get(interval, 'preScaled'); - if (preScaled !== undefined && preScaled < nearestMultipleInt) { - nearestMultipleInt.preScaled = preScaled; - nearestMultipleInt.scale = preScaled / nearestMultipleInt; - nearestMultipleInt.scaled = true; - } - - return nearestMultipleInt; -}; - -// Appends some MlTimeBuckets specific properties to the momentjs duration interval. -// Uses the originalDuration from which the time bucket was created to calculate the overflow -// property (i.e. difference between the supplied duration and the calculated bucket interval). -function decorateInterval(interval, originalDuration) { - const esInterval = calcEsInterval(interval); - interval.esValue = esInterval.value; - interval.esUnit = esInterval.unit; - interval.expression = esInterval.expression; - interval.overflow = originalDuration > interval ? moment.duration(interval - originalDuration) : false; - - const prettyUnits = moment.normalizeUnits(esInterval.unit); - if (esInterval.value === 1) { - interval.description = prettyUnits; - } else { - interval.description = `${esInterval.value} ${prettyUnits}s`; - } - - return interval; -} - -export function getBoundsRoundedToInterval(bounds, interval, inclusiveEnd = false) { - // Returns new bounds, created by flooring the min of the provided bounds to the start of - // the specified interval (a moment duration), and rounded upwards (Math.ceil) to 1ms before - // the start of the next interval (Kibana dashboards search >= bounds min, and <= bounds max, - // so we subtract 1ms off the max to avoid querying start of the new Elasticsearch aggregation bucket). - const intervalMs = interval.asMilliseconds(); - const adjustedMinMs = (Math.floor(bounds.min.valueOf() / intervalMs)) * intervalMs; - let adjustedMaxMs = (Math.ceil(bounds.max.valueOf() / intervalMs)) * intervalMs; - - // Don't include the start ms of the next bucket unless specified.. - if (inclusiveEnd === false) { - adjustedMaxMs = adjustedMaxMs - 1; - } - return { min: moment(adjustedMinMs), max: moment(adjustedMaxMs) }; -} - -export function calcEsInterval(duration) { - // Converts a moment.duration into an Elasticsearch compatible interval expression, - // and provides associated metadata. - - // Note this is a copy of Kibana's ui/time_buckets/calc_es_interval, - // but with the definition of a 'large' unit changed from 'M' to 'w', - // bringing it into line with the time units supported by Elasticsearch - for (let i = 0; i < unitsDesc.length; i++) { - const unit = unitsDesc[i]; - const val = duration.as(unit); - // find a unit that rounds neatly - if (val >= 1 && Math.floor(val) === val) { - - // if the unit is "large", like years, but isn't set to 1, ES will throw an error. - // So keep going until we get out of the "large" units. - if (i <= largeMax && val !== 1) { - continue; - } - - return { - value: val, - unit: unit, - expression: val + unit - }; - } - } - - const ms = duration.as('ms'); - return { - value: ms, - unit: 'ms', - expression: ms + 'ms' - }; -} diff --git a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts b/x-pack/legacy/plugins/ml/public/util/time_buckets.d.ts similarity index 95% rename from x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts rename to x-pack/legacy/plugins/ml/public/util/time_buckets.d.ts index b860fdeeec8e2..17773b66e7456 100644 --- a/x-pack/legacy/plugins/ml/public/util/ml_time_buckets.d.ts +++ b/x-pack/legacy/plugins/ml/public/util/time_buckets.d.ts @@ -11,7 +11,7 @@ declare interface TimeFilterBounds { max: Moment; } -export class MlTimeBuckets { +export class TimeBuckets { setBarTarget: (barTarget: number) => void; setMaxBars: (maxBars: number) => void; setInterval: (interval: string) => void; diff --git a/x-pack/legacy/plugins/ml/public/util/time_buckets.js b/x-pack/legacy/plugins/ml/public/util/time_buckets.js new file mode 100644 index 0000000000000..2a4c7023dbb02 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/time_buckets.js @@ -0,0 +1,404 @@ +/* + * 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 _ from 'lodash'; +import moment from 'moment'; +import dateMath from '@elastic/datemath'; +import chrome from 'ui/chrome'; +import { fieldFormats } from 'ui/registry/field_formats'; + +import { timeBucketsCalcAutoIntervalProvider } from './calc_auto_interval'; +import { parseInterval } from '../../common/util/parse_interval'; + +const unitsDesc = dateMath.unitsDesc; +const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals. + +const config = chrome.getUiSettingsClient(); + +const getConfig = (...args) => config.get(...args); + +const calcAuto = timeBucketsCalcAutoIntervalProvider(); + +/** + * Helper object for wrapping the concept of an "Interval", which + * describes a timespan that will separate buckets of time, + * for example the interval between points on a time series chart. + */ +export function TimeBuckets() { + this.barTarget = config.get('histogram:barTarget'); + this.maxBars = config.get('histogram:maxBars'); +} + +/** + * Set the target number of bars. + * + * @param {number} bt - target number of bars (buckets). + * + * @returns {undefined} + */ +TimeBuckets.prototype.setBarTarget = function (bt) { + this.barTarget = bt; +}; + +/** + * Set the maximum number of bars. + * + * @param {number} mb - maximum number of bars (buckets). + * + * @returns {undefined} + */ +TimeBuckets.prototype.setMaxBars = function (mb) { + this.maxBars = mb; +}; + +/** + * Set the bounds that these buckets are expected to cover. + * This is required to support interval "auto" as well + * as interval scaling. + * + * @param {object} input - an object with properties min and max, + * representing the edges for the time span + * we should cover + * + * @returns {undefined} + */ +TimeBuckets.prototype.setBounds = function (input) { + if (!input) return this.clearBounds(); + + let bounds; + if (_.isPlainObject(input)) { + // accept the response from timefilter.getActiveBounds() + bounds = [input.min, input.max]; + } else { + bounds = Array.isArray(input) ? input : []; + } + + const moments = _(bounds) + .map(_.ary(moment, 1)) + .sortBy(Number); + + const valid = moments.size() === 2 && moments.every(isValidMoment); + if (!valid) { + this.clearBounds(); + throw new Error('invalid bounds set: ' + input); + } + + this._lb = moments.shift(); + this._ub = moments.pop(); + if (this.getDuration().asSeconds() < 0) { + throw new TypeError('Intervals must be positive'); + } +}; + +/** + * Clear the stored bounds + * + * @return {undefined} + */ +TimeBuckets.prototype.clearBounds = function () { + this._lb = this._ub = null; +}; + +/** + * Check to see if we have received bounds yet + * + * @return {Boolean} + */ +TimeBuckets.prototype.hasBounds = function () { + return isValidMoment(this._ub) && isValidMoment(this._lb); +}; + +/** + * Return the current bounds, if we have any. + * + * Note that this does not clone the bounds, so editing them may have unexpected side-effects. + * Always call bounds.min.clone() before editing. + * + * @return {object|undefined} - If bounds are not defined, this + * returns undefined, else it returns the bounds + * for these buckets. This object has two props, + * min and max. Each property will be a moment() + * object + */ +TimeBuckets.prototype.getBounds = function () { + if (!this.hasBounds()) return; + return { + min: this._lb, + max: this._ub + }; +}; + +/** + * Get a moment duration object representing + * the distance between the bounds, if the bounds + * are set. + * + * @return {moment.duration|undefined} + */ +TimeBuckets.prototype.getDuration = function () { + if (!this.hasBounds()) return; + return moment.duration(this._ub - this._lb, 'ms'); +}; + +/** + * Update the interval at which buckets should be + * generated. + * + * Input can be one of the following: + * - Any object from src/legacy/ui/agg_types/buckets/_interval_options.js + * - "auto" + * - Pass a valid moment unit + * - a moment.duration object. + * + * @param {object|string|moment.duration} input - see desc + */ +TimeBuckets.prototype.setInterval = function (input) { + // Preserve the original units because they're lost when the interval is converted to a + // moment duration object. + this.originalInterval = input; + + let interval = input; + + // selection object -> val + if (_.isObject(input)) { + interval = input.val; + } + + if (!interval || interval === 'auto') { + this._i = 'auto'; + return; + } + + if (_.isString(interval)) { + input = interval; + interval = parseInterval(interval); + if (+interval === 0) { + interval = null; + } + } + + // If the value wasn't converted to a duration, and isn't already a duration, we have a problem + if (!moment.isDuration(interval)) { + throw new TypeError('"' + input + '" is not a valid interval.'); + } + + this._i = interval; +}; + +/** + * Get the interval for the buckets. If the + * number of buckets created by the interval set + * is larger than config:histogram:maxBars then the + * interval will be scaled up. If the number of buckets + * created is less than one, the interval is scaled back. + * + * The interval object returned is a moment.duration + * object that has been decorated with the following + * properties. + * + * interval.description: a text description of the interval. + * designed to be used list "field per {{ desc }}". + * - "minute" + * - "10 days" + * - "3 years" + * + * interval.expr: the elasticsearch expression that creates this + * interval. If the interval does not properly form an elasticsearch + * expression it will be forced into one. + * + * interval.scaled: the interval was adjusted to + * accommodate the maxBars setting. + * + * interval.scale: the number that y-values should be + * multiplied by + * + * interval.scaleDescription: a description that reflects + * the values which will be produced by using the + * interval.scale. + * + * + * @return {[type]} [description] + */ +TimeBuckets.prototype.getInterval = function () { + const self = this; + const duration = self.getDuration(); + return decorateInterval(maybeScaleInterval(readInterval()), duration); + + // either pull the interval from state or calculate the auto-interval + function readInterval() { + const interval = self._i; + if (moment.isDuration(interval)) return interval; + return calcAuto.near(self.barTarget, duration); + } + + // check to see if the interval should be scaled, and scale it if so + function maybeScaleInterval(interval) { + if (!self.hasBounds()) return interval; + + const maxLength = self.maxBars; + const approxLen = duration / interval; + let scaled; + + // If the number of buckets we got back from using the barTarget is less than + // maxBars, than use the lessThan rule to try and get closer to maxBars. + if (approxLen > maxLength) { + scaled = calcAuto.lessThan(maxLength, duration); + } else { + return interval; + } + + if (+scaled === +interval) return interval; + + decorateInterval(interval, duration); + return _.assign(scaled, { + preScaled: interval, + scale: interval / scaled, + scaled: true + }); + } + +}; + +/** + * Returns an interval which in the last step of calculation is rounded to + * the closest multiple of the supplied divisor (in seconds). + * + * @return {moment.duration|undefined} + */ +TimeBuckets.prototype.getIntervalToNearestMultiple = function (divisorSecs) { + const interval = this.getInterval(); + const intervalSecs = interval.asSeconds(); + + const remainder = intervalSecs % divisorSecs; + if (remainder === 0) { + return interval; + } + + // Create a new interval which is a multiple of the supplied divisor (not zero). + let nearestMultiple = remainder > (divisorSecs / 2) ? + intervalSecs + divisorSecs - remainder : intervalSecs - remainder; + nearestMultiple = nearestMultiple === 0 ? divisorSecs : nearestMultiple; + const nearestMultipleInt = moment.duration(nearestMultiple, 'seconds'); + decorateInterval(nearestMultipleInt, this.getDuration()); + + // Check to see if the new interval is scaled compared to the original. + const preScaled = _.get(interval, 'preScaled'); + if (preScaled !== undefined && preScaled < nearestMultipleInt) { + nearestMultipleInt.preScaled = preScaled; + nearestMultipleInt.scale = preScaled / nearestMultipleInt; + nearestMultipleInt.scaled = true; + } + + return nearestMultipleInt; +}; + +/** + * Get a date format string that will represent dates that + * progress at our interval. + * + * Since our interval can be as small as 1ms, the default + * date format is usually way too much. with `dateFormat:scaled` + * users can modify how dates are formatted within series + * produced by TimeBuckets + * + * @return {string} + */ +TimeBuckets.prototype.getScaledDateFormat = function () { + const interval = this.getInterval(); + const rules = config.get('dateFormat:scaled'); + + for (let i = rules.length - 1; i >= 0; i--) { + const rule = rules[i]; + if (!rule[0] || interval >= moment.duration(rule[0])) { + return rule[1]; + } + } + + return config.get('dateFormat'); +}; + +TimeBuckets.prototype.getScaledDateFormatter = function () { + const DateFieldFormat = fieldFormats.getType('date'); + return new DateFieldFormat({ + pattern: this.getScaledDateFormat() + }, getConfig); +}; + +// Appends some TimeBuckets specific properties to the moment.js duration interval. +// Uses the originalDuration from which the time bucket was created to calculate the overflow +// property (i.e. difference between the supplied duration and the calculated bucket interval). +function decorateInterval(interval, originalDuration) { + const esInterval = calcEsInterval(interval); + interval.esValue = esInterval.value; + interval.esUnit = esInterval.unit; + interval.expression = esInterval.expression; + interval.overflow = originalDuration > interval ? moment.duration(interval - originalDuration) : false; + + const prettyUnits = moment.normalizeUnits(esInterval.unit); + if (esInterval.value === 1) { + interval.description = prettyUnits; + } else { + interval.description = `${esInterval.value} ${prettyUnits}s`; + } + + return interval; +} + +function isValidMoment(m) { + return m && ('isValid' in m) && m.isValid(); +} + +export function getBoundsRoundedToInterval(bounds, interval, inclusiveEnd = false) { + // Returns new bounds, created by flooring the min of the provided bounds to the start of + // the specified interval (a moment duration), and rounded upwards (Math.ceil) to 1ms before + // the start of the next interval (Kibana dashboards search >= bounds min, and <= bounds max, + // so we subtract 1ms off the max to avoid querying start of the new Elasticsearch aggregation bucket). + const intervalMs = interval.asMilliseconds(); + const adjustedMinMs = (Math.floor(bounds.min.valueOf() / intervalMs)) * intervalMs; + let adjustedMaxMs = (Math.ceil(bounds.max.valueOf() / intervalMs)) * intervalMs; + + // Don't include the start ms of the next bucket unless specified.. + if (inclusiveEnd === false) { + adjustedMaxMs = adjustedMaxMs - 1; + } + return { min: moment(adjustedMinMs), max: moment(adjustedMaxMs) }; +} + +export function calcEsInterval(duration) { + // Converts a moment.duration into an Elasticsearch compatible interval expression, + // and provides associated metadata. + + // Note this was a copy of Kibana's original ui/time_buckets/calc_es_interval, + // but with the definition of a 'large' unit changed from 'M' to 'w', + // bringing it into line with the time units supported by Elasticsearch + for (let i = 0; i < unitsDesc.length; i++) { + const unit = unitsDesc[i]; + const val = duration.as(unit); + // find a unit that rounds neatly + if (val >= 1 && Math.floor(val) === val) { + + // if the unit is "large", like years, but isn't set to 1, ES will throw an error. + // So keep going until we get out of the "large" units. + if (i <= largeMax && val !== 1) { + continue; + } + + return { + value: val, + unit: unit, + expression: val + unit + }; + } + } + + const ms = duration.as('ms'); + return { + value: ms, + unit: 'ms', + expression: ms + 'ms' + }; +}