Skip to content

Commit

Permalink
[ML] Add option to Advanced Settings to set default time range filter…
Browse files Browse the repository at this point in the history
… for AD jobs (#76347)
  • Loading branch information
qn895 authored Sep 4, 2020
1 parent 8654200 commit ee3c8d0
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 39 deletions.
8 changes: 8 additions & 0 deletions x-pack/plugins/ml/common/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@
*/

export const FILE_DATA_VISUALIZER_MAX_FILE_SIZE = 'ml:fileDataVisualizerMaxFileSize';
export const ANOMALY_DETECTION_ENABLE_TIME_RANGE = 'ml:anomalyDetection:results:enableTimeDefaults';
export const ANOMALY_DETECTION_DEFAULT_TIME_RANGE = 'ml:anomalyDetection:results:timeDefaults';

export const DEFAULT_AD_RESULTS_TIME_FILTER = {
from: 'now-15m',
to: 'now',
};
export const DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useCallback } from 'react';
import { useMlKibana, useUiSettings } from '../../contexts/kibana';
import {
ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
ANOMALY_DETECTION_ENABLE_TIME_RANGE,
} from '../../../../common/constants/settings';
import { mlJobService } from '../../services/job_service';

export const useCreateADLinks = () => {
const {
services: {
http: { basePath },
},
} = useMlKibana();

const useUserTimeSettings = useUiSettings().get(ANOMALY_DETECTION_ENABLE_TIME_RANGE);
const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE);
const createLinkWithUserDefaults = useCallback(
(location, jobList) => {
const resultsPageUrl = mlJobService.createResultsUrlForJobs(
jobList,
location,
useUserTimeSettings === true && userTimeSettings !== undefined
? userTimeSettings
: undefined
);
return `${basePath.get()}/app/ml${resultsPageUrl}`;
},
[basePath]
);
return { createLinkWithUserDefaults };
};
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export const JobSelectorFlyout: FC<JobSelectorFlyoutProps> = ({
<EuiFlexItem grow={false}>
<EuiSwitch
label={i18n.translate('xpack.ml.jobSelector.applyTimerangeSwitchLabel', {
defaultMessage: 'Apply timerange',
defaultMessage: 'Apply time range',
})}
checked={applyTimeRange}
onChange={toggleTimerangeSwitch}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export function JobSelectorTable({
id: 'checkbox',
isCheckbox: true,
textOnly: false,
width: '24px',
width: '32px',
},
{
label: 'group ID',
Expand Down
18 changes: 17 additions & 1 deletion x-pack/plugins/ml/public/application/explorer/explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { ExplorerChartsContainer } from './explorer_charts/explorer_charts_conta
import { AnomaliesTable } from '../components/anomalies_table/anomalies_table';

import { getTimefilter, getToastNotifications } from '../util/dependency_cache';
import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings';

const ExplorerPage = ({
children,
Expand Down Expand Up @@ -145,6 +146,22 @@ export class Explorer extends React.Component {
state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG };
htmlIdGen = htmlIdGenerator();

componentDidMount() {
const { invalidTimeRangeError } = this.props;
if (invalidTimeRangeError) {
const toastNotifications = getToastNotifications();
toastNotifications.addWarning(
i18n.translate('xpack.ml.explorer.invalidTimeRangeInUrlCallout', {
defaultMessage:
'The time filter was changed to the full range due to an invalid default time filter. Check the advanced settings for {field}.',
values: {
field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
},
})
);
}
}

// Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes
// and will cause a syntax error when called with getKqlQueryValues
applyFilter = (fieldName, fieldValue, action) => {
Expand Down Expand Up @@ -298,7 +315,6 @@ export class Explorer extends React.Component {

<div className={mainColumnClasses}>
<EuiSpacer size="m" />

{stoppedPartitions && (
<EuiCallOut
size={'s'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,8 @@ import PropTypes from 'prop-types';
import React from 'react';

import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';

import { mlJobService } from '../../../../services/job_service';
import { i18n } from '@kbn/i18n';
import { getBasePath } from '../../../../util/dependency_cache';

export function getLink(location, jobs) {
const basePath = getBasePath();
const resultsPageUrl = mlJobService.createResultsUrlForJobs(jobs, location);
return `${basePath.get()}/app/ml${resultsPageUrl}`;
}
import { useCreateADLinks } from '../../../../components/custom_hooks/use_create_ad_links';

export function ResultLinks({ jobs }) {
const openJobsInSingleMetricViewerText = i18n.translate(
Expand All @@ -44,13 +36,13 @@ export function ResultLinks({ jobs }) {
const singleMetricVisible = jobs.length < 2;
const singleMetricEnabled = jobs.length === 1 && jobs[0].isSingleMetricViewerJob;
const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true;

const { createLinkWithUserDefaults } = useCreateADLinks();
return (
<React.Fragment>
{singleMetricVisible && (
<EuiToolTip position="bottom" content={openJobsInSingleMetricViewerText}>
<EuiButtonIcon
href={getLink('timeseriesexplorer', jobs)}
href={createLinkWithUserDefaults('timeseriesexplorer', jobs)}
iconType="visLine"
aria-label={openJobsInSingleMetricViewerText}
className="results-button"
Expand All @@ -61,7 +53,7 @@ export function ResultLinks({ jobs }) {
)}
<EuiToolTip position="bottom" content={openJobsInAnomalyExplorerText}>
<EuiButtonIcon
href={getLink('explorer', jobs)}
href={createLinkWithUserDefaults('explorer', jobs)}
iconType="visTable"
aria-label={openJobsInAnomalyExplorerText}
className="results-button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
import React, { FC } from 'react';
import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
// @ts-ignore no module file
import { getLink } from '../../../jobs/jobs_list/components/job_actions/results';
import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs';
import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links';

interface Props {
jobsList: MlSummaryJobs;
Expand All @@ -23,13 +22,14 @@ export const ExplorerLink: FC<Props> = ({ jobsList }) => {
values: { jobsCount: jobsList.length, jobId: jobsList[0] && jobsList[0].id },
}
);
const { createLinkWithUserDefaults } = useCreateADLinks();

return (
<EuiToolTip position="bottom" content={openJobsInAnomalyExplorerText}>
<EuiButtonEmpty
color="text"
size="xs"
href={getLink('explorer', jobsList)}
href={createLinkWithUserDefaults('explorer', jobsList)}
iconType="visTable"
aria-label={openJobsInAnomalyExplorerText}
className="results-button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
const [globalState, setGlobalState] = useUrlState('_g');
const [lastRefresh, setLastRefresh] = useState(0);
const [stoppedPartitions, setStoppedPartitions] = useState<string[] | undefined>();

const [invalidTimeRangeError, setInValidTimeRangeError] = useState<boolean>(false);
const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true });

const { jobIds } = useJobSelection(jobsWithTimeRange);
Expand All @@ -99,6 +99,9 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
// `timefilter.getBounds()` to update `bounds` in this component's state.
useEffect(() => {
if (globalState?.time !== undefined) {
if (globalState.time.mode === 'invalid') {
setInValidTimeRangeError(true);
}
timefilter.setTime({
from: globalState.time.from,
to: globalState.time.to,
Expand Down Expand Up @@ -236,6 +239,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
showCharts,
severity: tableSeverity.val,
stoppedPartitions,
invalidTimeRangeError,
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
const previousRefresh = usePrevious(lastRefresh);
const [selectedJobId, setSelectedJobId] = useState<string>();
const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true });
const [invalidTimeRangeError, setInValidTimeRangeError] = useState<boolean>(false);

const refresh = useRefresh();
useEffect(() => {
Expand All @@ -114,6 +115,9 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
const [bounds, setBounds] = useState<TimeRangeBounds | undefined>(undefined);
useEffect(() => {
if (globalState?.time !== undefined) {
if (globalState.time.mode === 'invalid') {
setInValidTimeRangeError(true);
}
timefilter.setTime({
from: globalState.time.from,
to: globalState.time.to,
Expand Down Expand Up @@ -300,6 +304,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
tableSeverity: tableSeverity.val,
timefilter,
zoom: zoomProp,
invalidTimeRangeError,
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { SearchResponse } from 'elasticsearch';
import { TimeRange } from 'src/plugins/data/common/query/timefilter/types';
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import { Calendar } from '../../../common/types/calendars';

Expand All @@ -15,7 +16,7 @@ export interface ExistingJobsAndGroups {

declare interface JobService {
jobs: CombinedJob[];
createResultsUrlForJobs: (jobs: any[], target: string) => string;
createResultsUrlForJobs: (jobs: any[], target: string, timeRange?: TimeRange) => string;
tempJobCloningObjects: {
job: any;
skipTimeRangeStep: boolean;
Expand Down
68 changes: 51 additions & 17 deletions x-pack/plugins/ml/public/application/services/job_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils';
import { TIME_FORMAT } from '../../../common/constants/time_format';
import { parseInterval } from '../../../common/util/parse_interval';
import { toastNotificationServiceProvider } from '../services/toast_notification_service';

import { validateTimeRange } from '../util/date_utils';
const msgs = mlMessageBarService;
let jobs = [];
let datafeedIds = {};
Expand Down Expand Up @@ -790,8 +790,8 @@ class JobService {
return groups;
}

createResultsUrlForJobs(jobsList, resultsPage) {
return createResultsUrlForJobs(jobsList, resultsPage);
createResultsUrlForJobs(jobsList, resultsPage, timeRange) {
return createResultsUrlForJobs(jobsList, resultsPage, timeRange);
}

createResultsUrl(jobIds, from, to, resultsPage) {
Expand Down Expand Up @@ -932,41 +932,75 @@ function createJobStats(jobsList, jobStats) {
jobStats.activeNodes.value = Object.keys(mlNodes).length;
}

function createResultsUrlForJobs(jobsList, resultsPage) {
function createResultsUrlForJobs(jobsList, resultsPage, userTimeRange) {
let from = undefined;
let to = undefined;
if (jobsList.length === 1) {
from = jobsList[0].earliestTimestampMs;
to = jobsList[0].latestResultsTimestampMs; // Will be max(latest source data, latest bucket results)
let mode = 'absolute';
const jobIds = jobsList.map((j) => j.id);

// if the custom default time filter is set and enabled in advanced settings
// if time is either absolute date or proper datemath format
if (validateTimeRange(userTimeRange)) {
from = userTimeRange.from;
to = userTimeRange.to;
// if both pass datemath's checks but are not technically absolute dates, use 'quick'
// e.g. "now-15m" "now+1d"
const fromFieldAValidDate = moment(userTimeRange.from).isValid();
const toFieldAValidDate = moment(userTimeRange.to).isValid();
if (!fromFieldAValidDate && !toFieldAValidDate) {
return createResultsUrl(jobIds, from, to, resultsPage, 'quick');
}
} else {
const jobsWithData = jobsList.filter((j) => j.earliestTimestampMs !== undefined);
if (jobsWithData.length > 0) {
from = Math.min(...jobsWithData.map((j) => j.earliestTimestampMs));
to = Math.max(...jobsWithData.map((j) => j.latestResultsTimestampMs));
// if time range is specified but with incorrect format
// change back to the default time range but alert the user
// that the advanced setting config is invalid
if (userTimeRange) {
mode = 'invalid';
}

if (jobsList.length === 1) {
from = jobsList[0].earliestTimestampMs;
to = jobsList[0].latestResultsTimestampMs; // Will be max(latest source data, latest bucket results)
} else {
const jobsWithData = jobsList.filter((j) => j.earliestTimestampMs !== undefined);
if (jobsWithData.length > 0) {
from = Math.min(...jobsWithData.map((j) => j.earliestTimestampMs));
to = Math.max(...jobsWithData.map((j) => j.latestResultsTimestampMs));
}
}
}

const fromString = moment(from).format(TIME_FORMAT); // Defaults to 'now' if 'from' is undefined
const toString = moment(to).format(TIME_FORMAT); // Defaults to 'now' if 'to' is undefined

const jobIds = jobsList.map((j) => j.id);
return createResultsUrl(jobIds, fromString, toString, resultsPage);
return createResultsUrl(jobIds, fromString, toString, resultsPage, mode);
}

function createResultsUrl(jobIds, start, end, resultsPage) {
function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') {
const idString = jobIds.map((j) => `'${j}'`).join(',');
const from = moment(start).toISOString();
const to = moment(end).toISOString();
let from;
let to;
let path = '';

if (resultsPage !== undefined) {
path += '#/';
path += resultsPage;
}

if (mode === 'quick') {
from = start;
to = end;
} else {
from = moment(start).toISOString();
to = moment(end).toISOString();
}

path += `?_g=(ml:(jobIds:!(${idString}))`;
path += `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${from}'`;
path += `,mode:absolute,to:'${to}'`;
path += `,to:'${to}'`;
if (mode === 'invalid') {
path += `,mode:invalid`;
}
path += "))&_a=(query:(query_string:(analyze_wildcard:!t,query:'*')))";

return path;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
getFocusData,
} from './timeseriesexplorer_utils';
import { EMPTY_FIELD_VALUE_LABEL } from './components/entity_control/entity_control';
import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings';

// Used to indicate the chart is being plotted across
// all partition field values, where the cardinality of the field cannot be
Expand Down Expand Up @@ -833,6 +834,22 @@ export class TimeSeriesExplorer extends React.Component {
}

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) {
const toastNotifications = getToastNotifications();
toastNotifications.addWarning(
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: ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
},
})
);
}

// Required to redraw the time series chart when the container is resized.
this.resizeChecker = new ResizeChecker(this.resizeRef.current);
this.resizeChecker.on('resize', () => {
Expand Down
Loading

0 comments on commit ee3c8d0

Please sign in to comment.