Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Add option to Advanced Settings to set default time range filter for AD jobs #76347

Merged
merged 16 commits into from
Sep 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message either needs changing to remove for this job or else you need to pass in the number of jobs selected in the view and change to for these jobs if more than one is selected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the for this job part to make it more concise and I think it reads okay. Updated here 1118b3e

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the "for this job" part...

Should it be removed from the UI text on the Advanced Settings page too?

},
})
);
}
}

// 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