From 4de2396c98b17ec42c9adac2e2805b5a5b6701a7 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Sat, 24 Feb 2024 00:18:15 -0500 Subject: [PATCH] adding logic to handle jobs requiring approvals (#718) * adding logic to handle jobs requiring approvals * typo * Update run_job.js --------- Co-authored-by: Ken Celenza --- nautobot_golden_config/static/run_job.js | 271 ++++++++++++----------- 1 file changed, 144 insertions(+), 127 deletions(-) diff --git a/nautobot_golden_config/static/run_job.js b/nautobot_golden_config/static/run_job.js index 938f4920..de978300 100644 --- a/nautobot_golden_config/static/run_job.js +++ b/nautobot_golden_config/static/run_job.js @@ -9,49 +9,66 @@ * @param {string} callBack - The promise function to return the success or failure message, leave `undefined` no callback is required. */ function startJob(jobClass, data, redirectUrlTemplate, callBack) { - var jobApi = `/api/extras/jobs/${jobClass}/run/`; + var jobApi = `/api/extras/jobs/${jobClass}/run/`; - if (typeof callBack === "undefined") { - var callBack = getMessage; - } + if (typeof callBack === "undefined") { + var callBack = getMessage; + } - $.ajax({ - type: 'POST', - url: jobApi, - contentType: "application/json", - data: JSON.stringify(data), - dataType: 'json', - headers: { - 'X-CSRFToken': nautobot_csrf_token - }, - beforeSend: function() { - // Normalize to base as much as you can. - $('#jobStatus').html("Pending").show(); - $('#loaderImg').show(); - $('#jobResults').hide(); - $('#redirectLink').hide(); - $('#detailMessages').hide(); - }, - success: function(jobData) { - $('#jobStatus').html("Started").show(); - var jobResultUrl = "/extras/job-results/" + jobData.job_result.id + "/"; - $('#jobResults').html(iconLink(jobResultUrl, "mdi-open-in-new", "Job Details")).show(); - pollJobStatus(jobData.job_result.url, callBack); - if (typeof redirectUrlTemplate !== "undefined") { - var redirectUrl = _renderTemplate(redirectUrlTemplate, jobData); - $('#redirectLink').html(iconLink(redirectUrl, "mdi-open-in-new", "Info")); - } - }, - error: function(e) { - $("#loaderImg").hide(); - console.log("There was an error with your request..."); - console.log("error: " + JSON.stringify(e)); - $('#jobStatus').html("Failed").show(); - $('#detailMessages').show(); - $('#detailMessages').attr('class', 'alert alert-danger text-center'); - $('#detailMessages').html("Error: " + e.responseText); - } - }); + $.ajax({ + type: 'POST', + url: jobApi, + contentType: "application/json", + data: JSON.stringify(data), + dataType: 'json', + headers: { + 'X-CSRFToken': nautobot_csrf_token + }, + beforeSend: function () { + // Normalize to base as much as you can. + $('#jobStatus').html("Pending").show(); + $('#loaderImg').show(); + $('#jobResults').hide(); + $('#redirectLink').hide(); + $('#detailMessages').hide(); + }, + success: function (jobData) { + if (jobData.job_result) { + // if you get a 200 response and job_result data is there, you know it was a good run + $('#jobStatus').html("Started").show(); + var jobResultUrl = "/extras/job-results/" + jobData.job_result.id + "/"; + $('#jobResults').html(iconLink(jobResultUrl, "mdi-open-in-new", "Job Details")).show(); + pollJobStatus(jobData.job_result.url, callBack); + if (typeof redirectUrlTemplate !== "undefined") { + var redirectUrl = _renderTemplate(redirectUrlTemplate, jobData); + $('#redirectLink').html(iconLink(redirectUrl, "mdi-open-in-new", "Info")); + } + } else if (jobData.scheduled_job && jobData.scheduled_job.approval_required) { + // Job approvals have this relevant data structure { "scheduled_job": {"approval_required": true }} + $("#loaderImg").hide(); + $('#jobStatus').html("Pending Approval").show(); + $('#detailMessages').show(); + $('#detailMessages').html(`

Job is pending approval, please review Job Approval

`); + } else { + // Response was 200 but did not return a job_result OR a scheduled_job pending approval, this condition should not be possible at the time of development but was added to prevent the modal going into a hung state + $("#loaderImg").hide(); + $('#jobStatus').html("Unknown").show(); + $('#detailMessages').show(); + $('#detailMessages').attr('class', 'alert alert-danger text-center'); + $('#detailMessages').html("

The call to launch the job was successful but an unknown error has occurred, response has been logged to the browser console.

"); + console.log("error: " + JSON.stringify(jobData)); + } + }, + error: function (e) { + $("#loaderImg").hide(); + console.log("There was an error with your request..."); + console.log("error: " + JSON.stringify(e)); + $('#jobStatus').html("Failed").show(); + $('#detailMessages').show(); + $('#detailMessages').attr('class', 'alert alert-danger text-center'); + $('#detailMessages').html("Error: " + e.responseText); + } + }); } /** @@ -70,50 +87,50 @@ function startJob(jobClass, data, redirectUrlTemplate, callBack) { * @returns {void} */ function pollJobStatus(jobId, callBack) { - $.ajax({ - url: jobId, - type: "GET", - async: false, - dataType: "json", - headers: { - 'X-CSRFToken': nautobot_csrf_token - }, - success: function(data) { - $('#jobStatus').html(data.status.value.charAt(0).toUpperCase() + data.status.value.slice(1)).show(); - if (["FAILURE", "REVOKED"].includes(data.status.value)) { - $("#loaderImg").hide(); - $('#detailMessages').show(); - $('#detailMessages').attr('class', 'alert alert-warning text-center'); - $('#detailMessages').html("Job started but failed during the Job run. This job may have partially completed. See Job Results for more details on the errors."); - } else if (["PENDING", "RECEIVED", "RETRY", "STARTED"].includes(data.status.value)) { - // Job is still processing, continue polling - setTimeout(function() { - pollJobStatus(jobId, callBack); - }, 1000); // Poll every 1 seconds - } else if (["SUCCESS"].includes(data.status.value)) { - $("#loaderImg").hide(); - $('#detailMessages').show(); - callBack(data.id) - .then((message) => { - $('#redirectLink').show(); - $('#detailMessages').attr('class', 'alert alert-success text-center'); - $('#detailMessages').html(message) - }) - .catch((message) => { - $('#detailMessages').attr('class', 'alert alert-warning text-center'); - $('#detailMessages').html(message) - }) - } - }, - error: function(e) { - $("#loaderImg").hide(); - console.log("There was an error with your request..."); - console.log("error: " + JSON.stringify(e)); - $('#detailMessages').show(); - $('#detailMessages').attr('class', 'alert alert-danger text-center'); - $('#detailMessages').html("Error: " + e.responseText); - } - }) + $.ajax({ + url: jobId, + type: "GET", + async: false, + dataType: "json", + headers: { + 'X-CSRFToken': nautobot_csrf_token + }, + success: function (data) { + $('#jobStatus').html(data.status.value.charAt(0).toUpperCase() + data.status.value.slice(1)).show(); + if (["FAILURE", "REVOKED"].includes(data.status.value)) { + $("#loaderImg").hide(); + $('#detailMessages').show(); + $('#detailMessages').attr('class', 'alert alert-warning text-center'); + $('#detailMessages').html("Job started but failed during the Job run. This job may have partially completed. See Job Results for more details on the errors."); + } else if (["PENDING", "RECEIVED", "RETRY", "STARTED"].includes(data.status.value)) { + // Job is still processing, continue polling + setTimeout(function () { + pollJobStatus(jobId, callBack); + }, 1000); // Poll every 1 seconds + } else if (["SUCCESS"].includes(data.status.value)) { + $("#loaderImg").hide(); + $('#detailMessages').show(); + callBack(data.id) + .then((message) => { + $('#redirectLink').show(); + $('#detailMessages').attr('class', 'alert alert-success text-center'); + $('#detailMessages').html(message) + }) + .catch((message) => { + $('#detailMessages').attr('class', 'alert alert-warning text-center'); + $('#detailMessages').html(message) + }) + } + }, + error: function (e) { + $("#loaderImg").hide(); + console.log("There was an error with your request..."); + console.log("error: " + JSON.stringify(e)); + $('#detailMessages').show(); + $('#detailMessages').attr('class', 'alert alert-danger text-center'); + $('#detailMessages').html("Error: " + e.responseText); + } + }) }; /** * Converts a list of form data objects to a dictionary. @@ -123,25 +140,25 @@ function pollJobStatus(jobId, callBack) { * @returns {Object} - The dictionary representation of the form data. */ function formDataToDictionary(formData, listKeys) { - const dict = {}; + const dict = {}; - formData.forEach(item => { - const { - name, - value - } = item; - if (listKeys.includes(name)) { - if (!dict[name]) { - dict[name] = [value]; - } else { - dict[name].push(value); - } - } else { - dict[name] = value; - } - }); + formData.forEach(item => { + const { + name, + value + } = item; + if (listKeys.includes(name)) { + if (!dict[name]) { + dict[name] = [value]; + } else { + dict[name].push(value); + } + } else { + dict[name] = value; + } + }); - return dict; + return dict; } /** @@ -154,12 +171,12 @@ function formDataToDictionary(formData, listKeys) { */ function iconLink(url, icon, title) { - const linkUrl = `` + - ` ` + - ` ` + - ` ` + - `` - return linkUrl + const linkUrl = `` + + ` ` + + ` ` + + ` ` + + `` + return linkUrl } /** @@ -170,28 +187,28 @@ function iconLink(url, icon, title) { * @returns {string} - The rendered string with placeholders replaced by actual values from jobData. */ function _renderTemplate(templateString, data) { - // Create a regular expression to match placeholders in the template - const placeholderRegex = /\{jobData\.([^\}]+)\}/g; + // Create a regular expression to match placeholders in the template + const placeholderRegex = /\{jobData\.([^\}]+)\}/g; - // Replace placeholders with corresponding values from jobData - const renderedString = templateString.replace(placeholderRegex, (match, key) => { - const keys = key.split("."); - let value = data; - for (const k of keys) { - if (value.hasOwnProperty(k)) { - value = value[k]; - } else { - return match; // If the key is not found, keep the original placeholder - } - } - return value; - }); + // Replace placeholders with corresponding values from jobData + const renderedString = templateString.replace(placeholderRegex, (match, key) => { + const keys = key.split("."); + let value = data; + for (const k of keys) { + if (value.hasOwnProperty(k)) { + value = value[k]; + } else { + return match; // If the key is not found, keep the original placeholder + } + } + return value; + }); - return renderedString; + return renderedString; } function getMessage(jobResultId) { - return new Promise((resolve) => { - resolve("Job Completed Successfully."); - }); + return new Promise((resolve) => { + resolve("Job Completed Successfully."); + }); } \ No newline at end of file