Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/




export const VALIDATION_STATUS = {
ERROR: 'error',
INFO: 'info',
SUCCESS: 'success',
WARNING: 'warning'
};
export enum VALIDATION_STATUS {
ERROR = 'error',
INFO = 'info',
SUCCESS = 'success',
WARNING = 'warning',
}

export const SKIP_BUCKET_SPAN_ESTIMATION = true;

Expand Down
16 changes: 16 additions & 0 deletions x-pack/legacy/plugins/ml/common/util/job_utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/

export interface ValidationMessage {
id: string;
}
export interface ValidationResults {
messages: ValidationMessage[];
valid: boolean;
contains: (id: string) => boolean;
find: (id: string) => ValidationMessage | undefined;
}
export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: number): number;

// TODO - use real types for job. Job interface first needs to move to a common location
export function isTimeSeriesViewJob(job: any): boolean;
export function basicJobValidation(
job: any,
fields: any[] | undefined,
limits: any,
skipMmlCheck?: boolean
): ValidationResults;

export const ML_MEDIAN_PERCENTS: number;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export { JobValidator, Validation, BasicValidations, ValidationSummary } from './job_validator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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 { basicJobValidation } from '../../../../../common/util/job_utils';
import { newJobLimits } from '../../../new_job/utils/new_job_defaults';
import { JobCreator } from '../job_creator';
import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util';
import { ExistingJobsAndGroups } from '../../../../services/job_service';

// delay start of validation to allow the user to make changes
// e.g. if they are typing in a new value, try not to validate
// after every keystroke
const VALIDATION_DELAY_MS = 500;

export interface ValidationSummary {
basic: boolean;
advanced: boolean;
}

export interface Validation {
valid: boolean;
message?: string;
}

export interface BasicValidations {
jobId: Validation;
groupIds: Validation;
modelMemoryLimit: Validation;
bucketSpan: Validation;
duplicateDetectors: Validation;
}

export class JobValidator {
private _jobCreator: JobCreator;
private _validationSummary: ValidationSummary;
private _lastJobConfig: string;
private _validateTimeout: NodeJS.Timeout;
private _existingJobsAndGroups: ExistingJobsAndGroups;
private _basicValidations: BasicValidations = {
jobId: { valid: true },
groupIds: { valid: true },
modelMemoryLimit: { valid: true },
bucketSpan: { valid: true },
duplicateDetectors: { valid: true },
};

constructor(jobCreator: JobCreator, existingJobsAndGroups: ExistingJobsAndGroups) {
this._jobCreator = jobCreator;
this._lastJobConfig = this._jobCreator.formattedJobJson;
this._validationSummary = {
basic: false,
advanced: false,
};
this._validateTimeout = setTimeout(() => {}, 0);
this._existingJobsAndGroups = existingJobsAndGroups;
}

public validate() {
const formattedJobConfig = this._jobCreator.formattedJobJson;
return new Promise((resolve: () => void) => {
// only validate if the config has changed
if (formattedJobConfig !== this._lastJobConfig) {
clearTimeout(this._validateTimeout);
this._lastJobConfig = formattedJobConfig;
this._validateTimeout = setTimeout(() => {
this._runBasicValidation();
resolve();
}, VALIDATION_DELAY_MS);
} else {
resolve();
}
});
}

private _resetBasicValidations() {
this._validationSummary.basic = true;
Object.values(this._basicValidations).forEach(v => {
v.valid = true;
delete v.message;
});
}

private _runBasicValidation() {
this._resetBasicValidations();

const jobConfig = this._jobCreator.jobConfig;
const limits = newJobLimits();

// run standard basic validation
const basicResults = basicJobValidation(jobConfig, undefined, limits);
populateValidationMessages(basicResults, this._basicValidations, jobConfig);

// run addition job and group id validation
const idResults = checkForExistingJobAndGroupIds(
this._jobCreator.jobId,
this._jobCreator.groups,
this._existingJobsAndGroups
);
populateValidationMessages(idResults, this._basicValidations, jobConfig);

this._validationSummary.basic = this._isOverallBasicValid();
}

private _isOverallBasicValid() {
return Object.values(this._basicValidations).some(v => v.valid === false) === false;
}

public get validationSummary(): ValidationSummary {
return this._validationSummary;
}

public get bucketSpan(): Validation {
return this._basicValidations.bucketSpan;
}

public get duplicateDetectors(): Validation {
return this._basicValidations.duplicateDetectors;
}

public get jobId(): Validation {
return this._basicValidations.jobId;
}

public get groupIds(): Validation {
return this._basicValidations.groupIds;
}

public get modelMemoryLimit(): Validation {
return this._basicValidations.modelMemoryLimit;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';
import { BasicValidations } from './job_validator';
import { Job } from '../job_creator/configs';
import { ALLOWED_DATA_UNITS } from '../../../../../common/constants/validation';
import { newJobLimits } from '../../../new_job/utils/new_job_defaults';
import { ValidationResults, ValidationMessage } from '../../../../../common/util/job_utils';
import { ExistingJobsAndGroups } from '../../../../services/job_service';

export function populateValidationMessages(
validationResults: ValidationResults,
basicValidations: BasicValidations,
jobConfig: Job
) {
const limits = newJobLimits();

if (validationResults.contains('job_id_empty')) {
basicValidations.jobId.valid = false;
} else if (validationResults.contains('job_id_invalid')) {
basicValidations.jobId.valid = false;
const msg = i18n.translate(
'xpack.ml.newJob.wizard.validateJob.jobNameAllowedCharactersDescription',
{
defaultMessage:
'Job name can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' +
'must start and end with an alphanumeric character',
}
);
basicValidations.jobId.message = msg;
} else if (validationResults.contains('job_id_already_exists')) {
basicValidations.jobId.valid = false;
const msg = i18n.translate('xpack.ml.newJob.wizard.validateJob.jobNameAlreadyExists', {
defaultMessage:
'Job ID already exists. A job ID cannot be the same as an existing job or group.',
});
basicValidations.jobId.message = msg;
}

if (validationResults.contains('job_group_id_invalid')) {
basicValidations.groupIds.valid = false;
const msg = i18n.translate(
'xpack.ml.newJob.wizard.validateJob.jobGroupAllowedCharactersDescription',
{
defaultMessage:
'Job group names can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' +
'must start and end with an alphanumeric character',
}
);
basicValidations.groupIds.message = msg;
} else if (validationResults.contains('job_group_id_already_exists')) {
basicValidations.groupIds.valid = false;
const msg = i18n.translate('xpack.ml.newJob.wizard.validateJob.groupNameAlreadyExists', {
defaultMessage:
'Group ID already exists. A group ID cannot be the same as an existing job or group.',
});
basicValidations.groupIds.message = msg;
}

if (validationResults.contains('model_memory_limit_units_invalid')) {
basicValidations.modelMemoryLimit.valid = false;
const str = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join(', ')} or ${[
...ALLOWED_DATA_UNITS,
].pop()}`;
const msg = i18n.translate(
'xpack.ml.newJob.wizard.validateJob.modelMemoryLimitUnitsInvalidErrorMessage',
{
defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}',
values: { str },
}
);
basicValidations.modelMemoryLimit.message = msg;
}

if (validationResults.contains('model_memory_limit_invalid')) {
basicValidations.modelMemoryLimit.valid = false;
const msg = i18n.translate(
'xpack.ml.newJob.wizard.validateJob.modelMemoryLimitRangeInvalidErrorMessage',
{
defaultMessage:
'Model memory limit cannot be higher than the maximum value of {maxModelMemoryLimit}',
values: { maxModelMemoryLimit: limits.max_model_memory_limit.toUpperCase() },
}
);
basicValidations.modelMemoryLimit.message = msg;
}

if (validationResults.contains('detectors_duplicates')) {
basicValidations.duplicateDetectors.valid = false;
const msg = i18n.translate(
'xpack.ml.newJob.wizard.validateJob.duplicatedDetectorsErrorMessage',
{
defaultMessage: 'Duplicate detectors were found.',
}
);
basicValidations.duplicateDetectors.message = msg;
}

if (validationResults.contains('bucket_span_empty')) {
basicValidations.bucketSpan.valid = false;
const msg = i18n.translate(
'xpack.ml.newJob.wizard.validateJob.bucketSpanMustBeSetErrorMessage',
{
defaultMessage: 'Bucket span must be set',
}
);

basicValidations.bucketSpan.message = msg;
} else if (validationResults.contains('bucket_span_invalid')) {
basicValidations.bucketSpan.valid = false;
const msg = i18n.translate(
'xpack.ml.newJob.wizard.validateJob.bucketSpanInvalidTimeIntervalFormatErrorMessage',
{
defaultMessage:
'{bucketSpan} is not a valid time interval format e.g. {tenMinutes}, {oneHour}. It also needs to be higher than zero.',
values: {
bucketSpan: jobConfig.analysis_config.bucket_span,
tenMinutes: '10m',
Comment thread
peteharverson marked this conversation as resolved.
Outdated
oneHour: '1h',
},
}
);

basicValidations.bucketSpan.message = msg;
}
}

export function checkForExistingJobAndGroupIds(
jobId: string,
groupIds: string[],
existingJobsAndGroups: ExistingJobsAndGroups
): ValidationResults {
const messages: ValidationMessage[] = [];

// check that job id does not already exist as a job or group or a newly created group
if (
existingJobsAndGroups.jobIds.includes(jobId) ||
existingJobsAndGroups.groupIds.includes(jobId) ||
groupIds.includes(jobId)
) {
messages.push({ id: 'job_id_already_exists' });
}

// check that groups that have been newly added in this job do not already exist as job ids
const newGroups = groupIds.filter(g => !existingJobsAndGroups.groupIds.includes(g));
if (existingJobsAndGroups.jobIds.some(g => newGroups.includes(g))) {
messages.push({ id: 'job_group_id_already_exists' });
}

return {
messages,
valid: messages.length === 0,
contains: (id: string) => messages.some(m => id === m.id),
find: (id: string) => messages.find(m => id === m.id),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ import {
} from '../../common/job_creator';
import { ChartLoader } from '../../common/chart_loader';
import { ResultsLoader } from '../../common/results_loader';

export interface ExistingJobsAndGroups {
jobs: string[];
groups: string[];
}
import { JobValidator } from '../../common/job_validator';
import { ExistingJobsAndGroups } from '../../../../services/job_service';

export interface JobCreatorContextValue {
jobCreatorUpdated: number;
Expand All @@ -27,6 +24,8 @@ export interface JobCreatorContextValue {
chartLoader: ChartLoader;
resultsLoader: ResultsLoader;
chartInterval: MlTimeBuckets;
jobValidator: JobValidator;
jobValidatorUpdated: number;
fields: Field[];
aggs: Aggregation[];
existingJobsAndGroups: ExistingJobsAndGroups;
Expand All @@ -39,6 +38,8 @@ export const JobCreatorContext = createContext<JobCreatorContextValue>({
chartLoader: {} as ChartLoader,
resultsLoader: {} as ResultsLoader,
chartInterval: {} as MlTimeBuckets,
jobValidator: {} as JobValidator,
jobValidatorUpdated: 0,
fields: [],
aggs: [],
existingJobsAndGroups: {} as ExistingJobsAndGroups,
Expand Down
Loading