diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/jobs_export_service.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/jobs_export_service.ts index 9b073a87110d8a..52e9bb2e101c20 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/jobs_export_service.ts +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/jobs_export_service.ts @@ -30,7 +30,9 @@ export class JobsExportService { constructor(private _mlApiServices: MlApiServices) {} public async exportAnomalyDetectionJobs(jobIds: string[]) { - const configs = await Promise.all(jobIds.map(this._mlApiServices.jobs.jobForCloning)); + const configs = await Promise.all( + jobIds.map((id) => this._mlApiServices.jobs.jobForCloning(id, true)) + ); this._export(configs, 'anomaly-detector'); } diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 45e5c2f4b82dc7..0b79dfc2dd990b 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -67,8 +67,8 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }); }, - jobForCloning(jobId: string) { - const body = JSON.stringify({ jobId }); + jobForCloning(jobId: string, retainCreatedBy = false) { + const body = JSON.stringify({ jobId, retainCreatedBy }); return httpService.http<{ job?: Job; datafeed?: Datafeed } | undefined>({ path: `${ML_INTERNAL_BASE_PATH}/jobs/job_for_cloning`, method: 'POST', diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index be9d6961cd6baf..669b1d4b737d77 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -315,22 +315,30 @@ export function jobsProvider( return { jobs, jobsMap }; } - async function getJobForCloning(jobId: string) { - const [jobResults, datafeedResult] = await Promise.all([ + async function getJobForCloning(jobId: string, retainCreatedBy = false) { + const [jobResults, datafeedResult, fullJobResults] = await Promise.all([ mlClient.getJobs({ job_id: jobId, exclude_generated: true }), getDatafeedByJobId(jobId, true), + ...(retainCreatedBy ? [mlClient.getJobs({ job_id: jobId })] : []), ]); const result: { datafeed?: Datafeed; job?: Job } = { job: undefined, datafeed: undefined }; if (datafeedResult && datafeedResult.job_id === jobId) { result.datafeed = datafeedResult; } - if (jobResults && jobResults.jobs) { - const job = jobResults.jobs.find((j) => j.job_id === jobId); - if (job) { - removeUnClonableCustomSettings(job); - result.job = job; + if (jobResults?.jobs?.length > 0) { + const job = jobResults.jobs[0]; + removeUnClonableCustomSettings(job); + + // to retain the created by property we need to add it back in + // from the job which hasn't been loaded with exclude_generated: true + if (retainCreatedBy && fullJobResults?.jobs?.length > 0) { + const fullJob = fullJobResults.jobs[0]; + if (fullJob.custom_settings?.created_by) { + job.custom_settings.created_by = fullJob.custom_settings.created_by; + } } + result.job = job; } return result; } diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 18c57b90925f65..b7fad0ef66ce21 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -30,7 +30,7 @@ import { deleteJobsSchema, } from './schemas/job_service_schema'; -import { jobIdSchema } from './schemas/anomaly_detectors_schema'; +import { jobForCloningSchema, jobIdSchema } from './schemas/anomaly_detectors_schema'; import { jobServiceProvider } from '../models/job_service'; import { getAuthorizationHeader } from '../lib/request_authorization'; @@ -428,16 +428,16 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { version: '1', validate: { request: { - body: jobIdSchema, + body: jobForCloningSchema, }, }, }, routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { try { const { getJobForCloning } = jobServiceProvider(client, mlClient); - const { jobId } = request.body; + const { jobId, retainCreatedBy } = request.body; - const resp = await getJobForCloning(jobId); + const resp = await getJobForCloning(jobId, retainCreatedBy); return response.ok({ body: resp, }); diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 6d3940c8d3e1f0..d39ab40b7c5615 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -211,3 +211,10 @@ export const forceQuerySchema = schema.object({ /** force close */ force: schema.maybe(schema.boolean()), }); + +export const jobForCloningSchema = schema.object({ + /** Whether to retain the created_by custom setting. */ + retainCreatedBy: schema.maybe(schema.boolean()), + /** Job ID */ + jobId: schema.string(), +});