[ML] Improving existing job check in anomaly detection wizard (#87674)
* [ML] Improving existing job check in anomaly detection wizard * fixing job id validation * allow group ids to be reused * updating module exists endpoint * fixing issuse with job without group list * fixing test and translation ids * fixing validator when model plot is disabled * changes based on review * adding group id check to edit job flyout * small refactor and fixing edit job issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
701bd0998d
commit
686ece9aea
24
x-pack/plugins/ml/common/types/job_service.ts
Normal file
24
x-pack/plugins/ml/common/types/job_service.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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 { Job, JobStats } from './anomaly_detection_jobs';
|
||||||
|
|
||||||
|
export interface MlJobsResponse {
|
||||||
|
jobs: Job[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MlJobsStatsResponse {
|
||||||
|
jobs: JobStats[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JobsExistResponse {
|
||||||
|
[jobId: string]: {
|
||||||
|
exists: boolean;
|
||||||
|
isGroup: boolean;
|
||||||
|
};
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import { saveJob } from './edit_utils';
|
||||||
import { loadFullJob } from '../utils';
|
import { loadFullJob } from '../utils';
|
||||||
import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job';
|
import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job';
|
||||||
import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service';
|
import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service';
|
||||||
|
import { ml } from '../../../../services/ml_api_service';
|
||||||
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
|
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||||
import { collapseLiteralStrings } from '../../../../../../shared_imports';
|
import { collapseLiteralStrings } from '../../../../../../shared_imports';
|
||||||
import { DATAFEED_STATE } from '../../../../../../common/constants/states';
|
import { DATAFEED_STATE } from '../../../../../../common/constants/states';
|
||||||
|
@ -195,16 +196,24 @@ export class EditJobFlyoutUI extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jobDetails.jobGroups !== undefined) {
|
if (jobDetails.jobGroups !== undefined) {
|
||||||
if (jobDetails.jobGroups.some((j) => this.props.allJobIds.includes(j))) {
|
jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message;
|
||||||
jobGroupsValidationError = i18n.translate(
|
if (jobGroupsValidationError === '') {
|
||||||
'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage',
|
ml.jobs.jobsExist(jobDetails.jobGroups, true).then((resp) => {
|
||||||
{
|
const groups = Object.values(resp);
|
||||||
defaultMessage:
|
const valid = groups.some((g) => g.exists === true && g.isGroup === false) === false;
|
||||||
'A job with this ID already exists. Groups and jobs cannot use the same ID.',
|
if (valid === false) {
|
||||||
|
this.setState({
|
||||||
|
jobGroupsValidationError: i18n.translate(
|
||||||
|
'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'A job with this ID already exists. Groups and jobs cannot use the same ID.',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
isValidJobDetails: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
} else {
|
|
||||||
jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,15 @@ import {
|
||||||
} from '../../../../../../common/util/job_utils';
|
} from '../../../../../../common/util/job_utils';
|
||||||
import { getNewJobLimits } from '../../../../services/ml_server_info';
|
import { getNewJobLimits } from '../../../../services/ml_server_info';
|
||||||
import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_creator';
|
import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_creator';
|
||||||
import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util';
|
import { populateValidationMessages } from './util';
|
||||||
import { ExistingJobsAndGroups } from '../../../../services/job_service';
|
import {
|
||||||
import { cardinalityValidator, CardinalityValidatorResult } from './validators';
|
cardinalityValidator,
|
||||||
|
CardinalityValidatorResult,
|
||||||
|
jobIdValidator,
|
||||||
|
groupIdsValidator,
|
||||||
|
JobExistsResult,
|
||||||
|
GroupsExistResult,
|
||||||
|
} from './validators';
|
||||||
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../common/constants/categorization_job';
|
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../common/constants/categorization_job';
|
||||||
import { JOB_TYPE } from '../../../../../../common/constants/new_job';
|
import { JOB_TYPE } from '../../../../../../common/constants/new_job';
|
||||||
|
|
||||||
|
@ -25,7 +31,9 @@ import { JOB_TYPE } from '../../../../../../common/constants/new_job';
|
||||||
// after every keystroke
|
// after every keystroke
|
||||||
export const VALIDATION_DELAY_MS = 500;
|
export const VALIDATION_DELAY_MS = 500;
|
||||||
|
|
||||||
type AsyncValidatorsResult = Partial<CardinalityValidatorResult>;
|
type AsyncValidatorsResult = Partial<
|
||||||
|
CardinalityValidatorResult & JobExistsResult & GroupsExistResult
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Union of possible validation results.
|
* Union of possible validation results.
|
||||||
|
@ -69,7 +77,6 @@ export class JobValidator {
|
||||||
private _validateTimeout: ReturnType<typeof setTimeout> | null = null;
|
private _validateTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
private _asyncValidators$: Array<Observable<AsyncValidatorsResult>> = [];
|
private _asyncValidators$: Array<Observable<AsyncValidatorsResult>> = [];
|
||||||
private _asyncValidatorsResult$: Observable<AsyncValidatorsResult>;
|
private _asyncValidatorsResult$: Observable<AsyncValidatorsResult>;
|
||||||
private _existingJobsAndGroups: ExistingJobsAndGroups;
|
|
||||||
private _basicValidations: BasicValidations = {
|
private _basicValidations: BasicValidations = {
|
||||||
jobId: { valid: true },
|
jobId: { valid: true },
|
||||||
groupIds: { valid: true },
|
groupIds: { valid: true },
|
||||||
|
@ -97,7 +104,7 @@ export class JobValidator {
|
||||||
*/
|
*/
|
||||||
public validationResult$: Observable<JobValidationResult>;
|
public validationResult$: Observable<JobValidationResult>;
|
||||||
|
|
||||||
constructor(jobCreator: JobCreatorType, existingJobsAndGroups: ExistingJobsAndGroups) {
|
constructor(jobCreator: JobCreatorType) {
|
||||||
this._jobCreator = jobCreator;
|
this._jobCreator = jobCreator;
|
||||||
this._lastJobConfig = this._jobCreator.formattedJobJson;
|
this._lastJobConfig = this._jobCreator.formattedJobJson;
|
||||||
this._lastDatafeedConfig = this._jobCreator.formattedDatafeedJson;
|
this._lastDatafeedConfig = this._jobCreator.formattedDatafeedJson;
|
||||||
|
@ -105,9 +112,12 @@ export class JobValidator {
|
||||||
basic: false,
|
basic: false,
|
||||||
advanced: false,
|
advanced: false,
|
||||||
};
|
};
|
||||||
this._existingJobsAndGroups = existingJobsAndGroups;
|
|
||||||
|
|
||||||
this._asyncValidators$ = [cardinalityValidator(this._jobCreatorSubject$)];
|
this._asyncValidators$ = [
|
||||||
|
cardinalityValidator(this._jobCreatorSubject$),
|
||||||
|
jobIdValidator(this._jobCreatorSubject$),
|
||||||
|
groupIdsValidator(this._jobCreatorSubject$),
|
||||||
|
];
|
||||||
|
|
||||||
this._asyncValidatorsResult$ = combineLatest(this._asyncValidators$).pipe(
|
this._asyncValidatorsResult$ = combineLatest(this._asyncValidators$).pipe(
|
||||||
map((res) => {
|
map((res) => {
|
||||||
|
@ -208,14 +218,6 @@ export class JobValidator {
|
||||||
datafeedConfig
|
datafeedConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
// run addition job and group id validation
|
|
||||||
const idResults = checkForExistingJobAndGroupIds(
|
|
||||||
this._jobCreator.jobId,
|
|
||||||
this._jobCreator.groups,
|
|
||||||
this._existingJobsAndGroups
|
|
||||||
);
|
|
||||||
populateValidationMessages(idResults, this._basicValidations, jobConfig, datafeedConfig);
|
|
||||||
|
|
||||||
this._validationSummary.basic = this._isOverallBasicValid();
|
this._validationSummary.basic = this._isOverallBasicValid();
|
||||||
// Update validation results subject
|
// Update validation results subject
|
||||||
this._basicValidationResult$.next(this._basicValidations);
|
this._basicValidationResult$.next(this._basicValidations);
|
||||||
|
|
|
@ -13,8 +13,6 @@ import {
|
||||||
} from '../../../../../../common/constants/validation';
|
} from '../../../../../../common/constants/validation';
|
||||||
import { getNewJobLimits } from '../../../../services/ml_server_info';
|
import { getNewJobLimits } from '../../../../services/ml_server_info';
|
||||||
import { ValidationResults } from '../../../../../../common/util/job_utils';
|
import { ValidationResults } from '../../../../../../common/util/job_utils';
|
||||||
import { ExistingJobsAndGroups } from '../../../../services/job_service';
|
|
||||||
import { JobValidationMessage } from '../../../../../../common/constants/messages';
|
|
||||||
|
|
||||||
export function populateValidationMessages(
|
export function populateValidationMessages(
|
||||||
validationResults: ValidationResults,
|
validationResults: ValidationResults,
|
||||||
|
@ -204,36 +202,6 @@ export function populateValidationMessages(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkForExistingJobAndGroupIds(
|
|
||||||
jobId: string,
|
|
||||||
groupIds: string[],
|
|
||||||
existingJobsAndGroups: ExistingJobsAndGroups
|
|
||||||
): ValidationResults {
|
|
||||||
const messages: JobValidationMessage[] = [];
|
|
||||||
|
|
||||||
// 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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidTimeIntervalMessage(value: string | undefined) {
|
function invalidTimeIntervalMessage(value: string | undefined) {
|
||||||
return i18n.translate(
|
return i18n.translate(
|
||||||
'xpack.ml.newJob.wizard.validateJob.frequencyInvalidTimeIntervalFormatErrorMessage',
|
'xpack.ml.newJob.wizard.validateJob.frequencyInvalidTimeIntervalFormatErrorMessage',
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { distinctUntilChanged, filter, map, pluck, switchMap, startWith } from 'rxjs/operators';
|
||||||
|
import { combineLatest, Observable, Subject } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
CardinalityModelPlotHigh,
|
CardinalityModelPlotHigh,
|
||||||
CardinalityValidationResult,
|
CardinalityValidationResult,
|
||||||
|
@ -13,6 +14,7 @@ import {
|
||||||
} from '../../../../services/ml_api_service';
|
} from '../../../../services/ml_api_service';
|
||||||
import { JobCreator } from '../job_creator';
|
import { JobCreator } from '../job_creator';
|
||||||
import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs';
|
import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs';
|
||||||
|
import { BasicValidations } from './job_validator';
|
||||||
|
|
||||||
export enum VALIDATOR_SEVERITY {
|
export enum VALIDATOR_SEVERITY {
|
||||||
ERROR,
|
ERROR,
|
||||||
|
@ -26,8 +28,30 @@ export interface CardinalityValidatorError {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jobExistsErrorMessage = i18n.translate(
|
||||||
|
'xpack.ml.newJob.wizard.validateJob.asyncJobNameAlreadyExists',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'Job ID already exists. A job ID cannot be the same as an existing job or group.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const groupExistsErrorMessage = i18n.translate(
|
||||||
|
'xpack.ml.newJob.wizard.validateJob.asyncGroupNameAlreadyExists',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'Group ID already exists. A group ID cannot be the same as an existing group or job.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export type CardinalityValidatorResult = CardinalityValidatorError | null;
|
export type CardinalityValidatorResult = CardinalityValidatorError | null;
|
||||||
|
|
||||||
|
export type JobExistsResult = {
|
||||||
|
jobIdExists: BasicValidations['jobId'];
|
||||||
|
} | null;
|
||||||
|
export type GroupsExistResult = {
|
||||||
|
groupIdsExist: BasicValidations['groupIds'];
|
||||||
|
} | null;
|
||||||
|
|
||||||
export function isCardinalityModelPlotHigh(
|
export function isCardinalityModelPlotHigh(
|
||||||
cardinalityValidationResult: CardinalityValidationResult
|
cardinalityValidationResult: CardinalityValidationResult
|
||||||
): cardinalityValidationResult is CardinalityModelPlotHigh {
|
): cardinalityValidationResult is CardinalityModelPlotHigh {
|
||||||
|
@ -39,39 +63,95 @@ export function isCardinalityModelPlotHigh(
|
||||||
export function cardinalityValidator(
|
export function cardinalityValidator(
|
||||||
jobCreator$: Subject<JobCreator>
|
jobCreator$: Subject<JobCreator>
|
||||||
): Observable<CardinalityValidatorResult> {
|
): Observable<CardinalityValidatorResult> {
|
||||||
return jobCreator$.pipe(
|
return combineLatest([
|
||||||
// Perform a cardinality check only with enabled model plot.
|
jobCreator$.pipe(pluck('modelPlot')),
|
||||||
filter((jobCreator) => {
|
jobCreator$.pipe(
|
||||||
return jobCreator?.modelPlot;
|
filter((jobCreator) => {
|
||||||
}),
|
return jobCreator?.modelPlot;
|
||||||
map((jobCreator) => {
|
}),
|
||||||
return {
|
map((jobCreator) => {
|
||||||
jobCreator,
|
return {
|
||||||
analysisConfigString: JSON.stringify(jobCreator.jobConfig.analysis_config),
|
jobCreator,
|
||||||
};
|
analysisConfigString: JSON.stringify(jobCreator.jobConfig.analysis_config, null, 2),
|
||||||
}),
|
};
|
||||||
// No need to perform an API call if the analysis configuration hasn't been changed
|
}),
|
||||||
distinctUntilChanged((prev, curr) => {
|
distinctUntilChanged((prev, curr) => {
|
||||||
return prev.analysisConfigString === curr.analysisConfigString;
|
return prev.analysisConfigString === curr.analysisConfigString;
|
||||||
}),
|
}),
|
||||||
switchMap(({ jobCreator }) => {
|
switchMap(({ jobCreator }) => {
|
||||||
return ml.validateCardinality$({
|
// Perform a cardinality check only with enabled model plot.
|
||||||
...jobCreator.jobConfig,
|
return ml
|
||||||
datafeed_config: jobCreator.datafeedConfig,
|
.validateCardinality$({
|
||||||
} as CombinedJob);
|
...jobCreator.jobConfig,
|
||||||
}),
|
datafeed_config: jobCreator.datafeedConfig,
|
||||||
map((validationResults) => {
|
} as CombinedJob)
|
||||||
for (const validationResult of validationResults) {
|
.pipe(
|
||||||
if (isCardinalityModelPlotHigh(validationResult)) {
|
map((validationResults) => {
|
||||||
return {
|
for (const validationResult of validationResults) {
|
||||||
highCardinality: {
|
if (isCardinalityModelPlotHigh(validationResult)) {
|
||||||
value: validationResult.modelPlotCardinality,
|
return {
|
||||||
severity: VALIDATOR_SEVERITY.WARNING,
|
highCardinality: {
|
||||||
},
|
value: validationResult.modelPlotCardinality,
|
||||||
};
|
severity: VALIDATOR_SEVERITY.WARNING,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
return null;
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
startWith(null)
|
||||||
|
),
|
||||||
|
]).pipe(
|
||||||
|
map(([isModelPlotEnabled, cardinalityValidationResult]) => {
|
||||||
|
return isModelPlotEnabled ? cardinalityValidationResult : null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jobIdValidator(jobCreator$: Subject<JobCreator>): Observable<JobExistsResult> {
|
||||||
|
return jobCreator$.pipe(
|
||||||
|
map((jobCreator) => {
|
||||||
|
return jobCreator.jobId;
|
||||||
|
}),
|
||||||
|
// No need to perform an API call if the analysis configuration hasn't been changed
|
||||||
|
distinctUntilChanged((prevJobId, currJobId) => prevJobId === currJobId),
|
||||||
|
switchMap((jobId) => ml.jobs.jobsExist$([jobId], true)),
|
||||||
|
map((jobExistsResults) => {
|
||||||
|
const jobs = Object.values(jobExistsResults);
|
||||||
|
const valid = jobs?.[0].exists === false;
|
||||||
|
return {
|
||||||
|
jobIdExists: {
|
||||||
|
valid,
|
||||||
|
...(valid ? {} : { message: jobExistsErrorMessage }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupIdsValidator(jobCreator$: Subject<JobCreator>): Observable<GroupsExistResult> {
|
||||||
|
return jobCreator$.pipe(
|
||||||
|
map((jobCreator) => jobCreator.groups),
|
||||||
|
// No need to perform an API call if the analysis configuration hasn't been changed
|
||||||
|
distinctUntilChanged(
|
||||||
|
(prevGroups, currGroups) => JSON.stringify(prevGroups) === JSON.stringify(currGroups)
|
||||||
|
),
|
||||||
|
switchMap((groups) => {
|
||||||
|
return ml.jobs.jobsExist$(groups, true);
|
||||||
|
}),
|
||||||
|
map((jobExistsResults) => {
|
||||||
|
const groups = Object.values(jobExistsResults);
|
||||||
|
// only match jobs that exist but aren't groups.
|
||||||
|
// as we should allow existing groups to be reused.
|
||||||
|
const valid = groups.some((g) => g.exists === true && g.isGroup === false) === false;
|
||||||
|
return {
|
||||||
|
groupIdsExist: {
|
||||||
|
valid,
|
||||||
|
...(valid ? {} : { message: groupExistsErrorMessage }),
|
||||||
|
},
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useState, useContext, useEffect } from 'react';
|
import React, { FC, useState, useContext, useEffect, useMemo } from 'react';
|
||||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { JobCreatorContext } from '../../../job_creator_context';
|
import { JobCreatorContext } from '../../../job_creator_context';
|
||||||
|
@ -17,7 +17,19 @@ export const GroupsInput: FC = () => {
|
||||||
);
|
);
|
||||||
const { existingJobsAndGroups } = useContext(JobCreatorContext);
|
const { existingJobsAndGroups } = useContext(JobCreatorContext);
|
||||||
const [selectedGroups, setSelectedGroups] = useState(jobCreator.groups);
|
const [selectedGroups, setSelectedGroups] = useState(jobCreator.groups);
|
||||||
const [validation, setValidation] = useState(jobValidator.groupIds);
|
|
||||||
|
const validation = useMemo(() => {
|
||||||
|
const valid =
|
||||||
|
jobValidator.groupIds.valid === true &&
|
||||||
|
jobValidator.latestValidationResult.groupIdsExist?.valid === true;
|
||||||
|
const message =
|
||||||
|
jobValidator.groupIds.message ?? jobValidator.latestValidationResult.groupIdsExist?.message;
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}, [jobValidatorUpdated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
jobCreator.groups = selectedGroups;
|
jobCreator.groups = selectedGroups;
|
||||||
|
@ -61,10 +73,6 @@ export const GroupsInput: FC = () => {
|
||||||
setSelectedGroups([...selectedOptions, newGroup].map((g) => g.label));
|
setSelectedGroups([...selectedOptions, newGroup].map((g) => g.label));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValidation(jobValidator.groupIds);
|
|
||||||
}, [jobValidatorUpdated]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Description validation={validation}>
|
<Description validation={validation}>
|
||||||
<EuiComboBox
|
<EuiComboBox
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useState, useContext, useEffect } from 'react';
|
import React, { FC, useState, useContext, useEffect, useMemo } from 'react';
|
||||||
import { EuiFieldText } from '@elastic/eui';
|
import { EuiFieldText } from '@elastic/eui';
|
||||||
import { JobCreatorContext } from '../../../job_creator_context';
|
import { JobCreatorContext } from '../../../job_creator_context';
|
||||||
import { Description } from './description';
|
import { Description } from './description';
|
||||||
|
@ -14,21 +14,28 @@ export const JobIdInput: FC = () => {
|
||||||
JobCreatorContext
|
JobCreatorContext
|
||||||
);
|
);
|
||||||
const [jobId, setJobId] = useState(jobCreator.jobId);
|
const [jobId, setJobId] = useState(jobCreator.jobId);
|
||||||
const [validation, setValidation] = useState(jobValidator.jobId);
|
|
||||||
|
const validation = useMemo(() => {
|
||||||
|
const isEmptyId = jobId === '';
|
||||||
|
const valid =
|
||||||
|
isEmptyId === true ||
|
||||||
|
(jobValidator.jobId.valid === true &&
|
||||||
|
jobValidator.latestValidationResult.jobIdExists?.valid === true);
|
||||||
|
|
||||||
|
const message =
|
||||||
|
jobValidator.jobId.message ?? jobValidator.latestValidationResult.jobIdExists?.message;
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}, [jobValidatorUpdated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
jobCreator.jobId = jobId;
|
jobCreator.jobId = jobId;
|
||||||
jobCreatorUpdate();
|
jobCreatorUpdate();
|
||||||
}, [jobId]);
|
}, [jobId]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isEmptyId = jobId === '';
|
|
||||||
setValidation({
|
|
||||||
valid: isEmptyId === true || jobValidator.jobId.valid,
|
|
||||||
message: isEmptyId === false ? jobValidator.jobId.message : '',
|
|
||||||
});
|
|
||||||
}, [jobValidatorUpdated]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Description validation={validation}>
|
<Description validation={validation}>
|
||||||
<EuiFieldText
|
<EuiFieldText
|
||||||
|
|
|
@ -40,6 +40,8 @@ export const JobDetailsStep: FC<Props> = ({
|
||||||
jobValidator.jobId.valid &&
|
jobValidator.jobId.valid &&
|
||||||
jobValidator.modelMemoryLimit.valid &&
|
jobValidator.modelMemoryLimit.valid &&
|
||||||
jobValidator.groupIds.valid &&
|
jobValidator.groupIds.valid &&
|
||||||
|
jobValidator.latestValidationResult.jobIdExists?.valid === true &&
|
||||||
|
jobValidator.latestValidationResult.groupIdsExist?.valid === true &&
|
||||||
jobValidator.validating === false;
|
jobValidator.validating === false;
|
||||||
setNextActive(active);
|
setNextActive(active);
|
||||||
}, [jobValidatorUpdated]);
|
}, [jobValidatorUpdated]);
|
||||||
|
|
|
@ -182,7 +182,7 @@ export const Page: FC<PageProps> = ({ existingJobsAndGroups, jobType }) => {
|
||||||
|
|
||||||
const chartLoader = new ChartLoader(mlContext.currentIndexPattern, mlContext.combinedQuery);
|
const chartLoader = new ChartLoader(mlContext.currentIndexPattern, mlContext.combinedQuery);
|
||||||
|
|
||||||
const jobValidator = new JobValidator(jobCreator, existingJobsAndGroups);
|
const jobValidator = new JobValidator(jobCreator);
|
||||||
|
|
||||||
const resultsLoader = new ResultsLoader(jobCreator, chartInterval, chartLoader);
|
const resultsLoader = new ResultsLoader(jobCreator, chartInterval, chartLoader);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
import { HttpService } from '../http_service';
|
import { HttpService } from '../http_service';
|
||||||
|
|
||||||
import { basePath } from './index';
|
import { basePath } from './index';
|
||||||
|
@ -23,6 +24,7 @@ import {
|
||||||
} from '../../../../common/types/categories';
|
} from '../../../../common/types/categories';
|
||||||
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job';
|
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job';
|
||||||
import { Category } from '../../../../common/types/categories';
|
import { Category } from '../../../../common/types/categories';
|
||||||
|
import { JobsExistResponse } from '../../../../common/types/job_service';
|
||||||
|
|
||||||
export const jobsApiProvider = (httpService: HttpService) => ({
|
export const jobsApiProvider = (httpService: HttpService) => ({
|
||||||
jobsSummary(jobIds: string[]) {
|
jobsSummary(jobIds: string[]) {
|
||||||
|
@ -138,9 +140,18 @@ export const jobsApiProvider = (httpService: HttpService) => ({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
jobsExist(jobIds: string[]) {
|
jobsExist(jobIds: string[], allSpaces: boolean = false) {
|
||||||
const body = JSON.stringify({ jobIds });
|
const body = JSON.stringify({ jobIds, allSpaces });
|
||||||
return httpService.http<any>({
|
return httpService.http<JobsExistResponse>({
|
||||||
|
path: `${basePath()}/jobs/jobs_exist`,
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
jobsExist$(jobIds: string[], allSpaces: boolean = false): Observable<JobsExistResponse> {
|
||||||
|
const body = JSON.stringify({ jobIds, allSpaces });
|
||||||
|
return httpService.http$({
|
||||||
path: `${basePath()}/jobs/jobs_exist`,
|
path: `${basePath()}/jobs/jobs_exist`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
|
|
@ -43,7 +43,7 @@ import { fieldsServiceProvider } from '../fields_service';
|
||||||
import { jobServiceProvider } from '../job_service';
|
import { jobServiceProvider } from '../job_service';
|
||||||
import { resultsServiceProvider } from '../results_service';
|
import { resultsServiceProvider } from '../results_service';
|
||||||
import { JobExistResult, JobStat } from '../../../common/types/data_recognizer';
|
import { JobExistResult, JobStat } from '../../../common/types/data_recognizer';
|
||||||
import { MlJobsStatsResponse } from '../job_service/jobs';
|
import { MlJobsStatsResponse } from '../../../common/types/job_service';
|
||||||
import { JobSavedObjectService } from '../../saved_objects';
|
import { JobSavedObjectService } from '../../saved_objects';
|
||||||
|
|
||||||
const ML_DIR = 'ml';
|
const ML_DIR = 'ml';
|
||||||
|
@ -533,7 +533,7 @@ export class DataRecognizer {
|
||||||
const jobInfo = await this._jobsService.jobsExist(jobIds);
|
const jobInfo = await this._jobsService.jobsExist(jobIds);
|
||||||
|
|
||||||
// Check if the value for any of the jobs is false.
|
// Check if the value for any of the jobs is false.
|
||||||
const doJobsExist = Object.values(jobInfo).includes(false) === false;
|
const doJobsExist = Object.values(jobInfo).every((j) => j.exists === true);
|
||||||
results.jobsExist = doJobsExist;
|
results.jobsExist = doJobsExist;
|
||||||
|
|
||||||
if (doJobsExist === true) {
|
if (doJobsExist === true) {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { CalendarManager } from '../calendar';
|
import { CalendarManager } from '../calendar';
|
||||||
import { GLOBAL_CALENDAR } from '../../../common/constants/calendars';
|
import { GLOBAL_CALENDAR } from '../../../common/constants/calendars';
|
||||||
import { Job } from '../../../common/types/anomaly_detection_jobs';
|
import { Job } from '../../../common/types/anomaly_detection_jobs';
|
||||||
import { MlJobsResponse } from './jobs';
|
import { MlJobsResponse } from '../../../common/types/job_service';
|
||||||
import type { MlClient } from '../../lib/ml_client';
|
import type { MlClient } from '../../lib/ml_client';
|
||||||
|
|
||||||
interface Group {
|
interface Group {
|
||||||
|
|
|
@ -16,11 +16,14 @@ import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states';
|
||||||
import {
|
import {
|
||||||
MlSummaryJob,
|
MlSummaryJob,
|
||||||
AuditMessage,
|
AuditMessage,
|
||||||
Job,
|
|
||||||
JobStats,
|
|
||||||
DatafeedWithStats,
|
DatafeedWithStats,
|
||||||
CombinedJobWithStats,
|
CombinedJobWithStats,
|
||||||
} from '../../../common/types/anomaly_detection_jobs';
|
} from '../../../common/types/anomaly_detection_jobs';
|
||||||
|
import {
|
||||||
|
MlJobsResponse,
|
||||||
|
MlJobsStatsResponse,
|
||||||
|
JobsExistResponse,
|
||||||
|
} from '../../../common/types/job_service';
|
||||||
import { GLOBAL_CALENDAR } from '../../../common/constants/calendars';
|
import { GLOBAL_CALENDAR } from '../../../common/constants/calendars';
|
||||||
import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds';
|
import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds';
|
||||||
import { jobAuditMessagesProvider } from '../job_audit_messages';
|
import { jobAuditMessagesProvider } from '../job_audit_messages';
|
||||||
|
@ -34,16 +37,6 @@ import {
|
||||||
import { groupsProvider } from './groups';
|
import { groupsProvider } from './groups';
|
||||||
import type { MlClient } from '../../lib/ml_client';
|
import type { MlClient } from '../../lib/ml_client';
|
||||||
|
|
||||||
export interface MlJobsResponse {
|
|
||||||
jobs: Job[];
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MlJobsStatsResponse {
|
|
||||||
jobs: JobStats[];
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Results {
|
interface Results {
|
||||||
[id: string]: {
|
[id: string]: {
|
||||||
[status: string]: boolean;
|
[status: string]: boolean;
|
||||||
|
@ -420,10 +413,18 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
|
||||||
// Checks if each of the jobs in the specified list of IDs exist.
|
// Checks if each of the jobs in the specified list of IDs exist.
|
||||||
// Job IDs in supplied array may contain wildcard '*' characters
|
// Job IDs in supplied array may contain wildcard '*' characters
|
||||||
// e.g. *_low_request_rate_ecs
|
// e.g. *_low_request_rate_ecs
|
||||||
async function jobsExist(jobIds: string[] = [], allSpaces: boolean = false) {
|
async function jobsExist(
|
||||||
const results: { [id: string]: boolean } = {};
|
jobIds: string[] = [],
|
||||||
|
allSpaces: boolean = false
|
||||||
|
): Promise<JobsExistResponse> {
|
||||||
|
const results: JobsExistResponse = {};
|
||||||
for (const jobId of jobIds) {
|
for (const jobId of jobIds) {
|
||||||
try {
|
try {
|
||||||
|
if (jobId === '') {
|
||||||
|
results[jobId] = { exists: false, isGroup: false };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const { body } = allSpaces
|
const { body } = allSpaces
|
||||||
? await client.asInternalUser.ml.getJobs<MlJobsResponse>({
|
? await client.asInternalUser.ml.getJobs<MlJobsResponse>({
|
||||||
job_id: jobId,
|
job_id: jobId,
|
||||||
|
@ -431,13 +432,15 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
|
||||||
: await mlClient.getJobs<MlJobsResponse>({
|
: await mlClient.getJobs<MlJobsResponse>({
|
||||||
job_id: jobId,
|
job_id: jobId,
|
||||||
});
|
});
|
||||||
results[jobId] = body.count > 0;
|
|
||||||
|
const isGroup = body.jobs.some((j) => j.groups !== undefined && j.groups.includes(jobId));
|
||||||
|
results[jobId] = { exists: body.count > 0, isGroup };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if a non-wildcarded job id is supplied, the get jobs endpoint will 404
|
// if a non-wildcarded job id is supplied, the get jobs endpoint will 404
|
||||||
if (e.statusCode !== 404) {
|
if (e.statusCode !== 404) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
results[jobId] = false;
|
results[jobId] = { exists: false, isGroup: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from '../../../common/types/anomalies';
|
} from '../../../common/types/anomalies';
|
||||||
import { JOB_ID, PARTITION_FIELD_VALUE } from '../../../common/constants/anomalies';
|
import { JOB_ID, PARTITION_FIELD_VALUE } from '../../../common/constants/anomalies';
|
||||||
import { GetStoppedPartitionResult } from '../../../common/types/results';
|
import { GetStoppedPartitionResult } from '../../../common/types/results';
|
||||||
import { MlJobsResponse } from '../job_service/jobs';
|
import { MlJobsResponse } from '../../../common/types/job_service';
|
||||||
import type { MlClient } from '../../lib/ml_client';
|
import type { MlClient } from '../../lib/ml_client';
|
||||||
|
|
||||||
// Service for carrying out Elasticsearch queries to obtain data for the
|
// Service for carrying out Elasticsearch queries to obtain data for the
|
||||||
|
|
|
@ -17,6 +17,7 @@ export default ({ getService }: FtrProviderContext) => {
|
||||||
|
|
||||||
const jobIdSpace1 = 'fq_single_space1';
|
const jobIdSpace1 = 'fq_single_space1';
|
||||||
const jobIdSpace2 = 'fq_single_space2';
|
const jobIdSpace2 = 'fq_single_space2';
|
||||||
|
const groupSpace1 = 'farequote';
|
||||||
const idSpace1 = 'space1';
|
const idSpace1 = 'space1';
|
||||||
const idSpace2 = 'space2';
|
const idSpace2 = 'space2';
|
||||||
|
|
||||||
|
@ -57,17 +58,25 @@ export default ({ getService }: FtrProviderContext) => {
|
||||||
|
|
||||||
it('should find single job from same space', async () => {
|
it('should find single job from same space', async () => {
|
||||||
const body = await runRequest(idSpace1, 200, [jobIdSpace1]);
|
const body = await runRequest(idSpace1, 200, [jobIdSpace1]);
|
||||||
expect(body).to.eql({ [jobIdSpace1]: true });
|
expect(body).to.eql({ [jobIdSpace1]: { exists: true, isGroup: false } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find single job from same space', async () => {
|
||||||
|
const body = await runRequest(idSpace1, 200, [groupSpace1]);
|
||||||
|
expect(body).to.eql({ [groupSpace1]: { exists: true, isGroup: true } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not find single job from different space', async () => {
|
it('should not find single job from different space', async () => {
|
||||||
const body = await runRequest(idSpace2, 200, [jobIdSpace1]);
|
const body = await runRequest(idSpace2, 200, [jobIdSpace1]);
|
||||||
expect(body).to.eql({ [jobIdSpace1]: false });
|
expect(body).to.eql({ [jobIdSpace1]: { exists: false, isGroup: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only find job from same space when called with a list of jobs', async () => {
|
it('should only find job from same space when called with a list of jobs', async () => {
|
||||||
const body = await runRequest(idSpace1, 200, [jobIdSpace1, jobIdSpace2]);
|
const body = await runRequest(idSpace1, 200, [jobIdSpace1, jobIdSpace2]);
|
||||||
expect(body).to.eql({ [jobIdSpace1]: true, [jobIdSpace2]: false });
|
expect(body).to.eql({
|
||||||
|
[jobIdSpace1]: { exists: true, isGroup: false },
|
||||||
|
[jobIdSpace2]: { exists: false, isGroup: false },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue