diff --git a/netbox/project-static/dist/jobs.js b/netbox/project-static/dist/jobs.js new file mode 100644 index 000000000..fb8621ff6 Binary files /dev/null and b/netbox/project-static/dist/jobs.js differ diff --git a/netbox/project-static/dist/jobs.js.map b/netbox/project-static/dist/jobs.js.map new file mode 100644 index 000000000..668867504 Binary files /dev/null and b/netbox/project-static/dist/jobs.js.map differ diff --git a/netbox/project-static/package.json b/netbox/project-static/package.json index 580b7f560..e7c79c5fc 100644 --- a/netbox/project-static/package.json +++ b/netbox/project-static/package.json @@ -5,7 +5,7 @@ "license": "Apache2", "scripts": { "bundle:css": "parcel build --public-url /static -o netbox.css main.scss && parcel build --public-url /static -o rack_elevation.css rack_elevation.scss", - "bundle:js": "parcel build -o netbox.js src/index.ts", + "bundle:js": "parcel build -o netbox.js src/index.ts && parcel build -o jobs.js src/jobs.ts", "bundle": "yarn bundle:css && yarn bundle:js" }, "dependencies": { diff --git a/netbox/project-static/src/global.d.ts b/netbox/project-static/src/global.d.ts index 05fda72b0..0ebd614a2 100644 --- a/netbox/project-static/src/global.d.ts +++ b/netbox/project-static/src/global.d.ts @@ -59,6 +59,38 @@ type APISecret = { url: string; }; +type JobResultLog = { + message: string; + status: 'success' | 'warning' | 'danger' | 'info'; +}; + +type JobStatus = { + label: string; + value: 'completed' | 'failed' | 'errored' | 'running'; +}; + +type APIJobResult = { + completed: string; + created: string; + data: { + log: JobResultLog[]; + output: string; + }; + display: string; + id: number; + job_id: string; + name: string; + obj_type: string; + status: JobStatus; + url: string; + user: { + display: string; + username: string; + id: number; + url: string; + }; +}; + interface ObjectWithGroup extends APIObjectBase { group: Nullable; } diff --git a/netbox/project-static/src/jobs.ts b/netbox/project-static/src/jobs.ts new file mode 100644 index 000000000..886bcb003 --- /dev/null +++ b/netbox/project-static/src/jobs.ts @@ -0,0 +1,98 @@ +import { createToast } from './toast'; +import { apiGetBase, hasError } from './util'; + +let timeout: number = 1000; + +interface JobInfo { + id: Nullable; + complete: boolean; +} + +/** + * Mimic the behavior of setTimeout() in an async function. + */ +function asyncTimeout(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Job ID & Completion state are only from Django context, which can only be used from the HTML + * template. Hidden elements are present in the template to provide access to these values from + * JavaScript. + */ +function getJobInfo(): JobInfo { + let id: Nullable = null; + let complete = false; + + // Determine the Job ID, if present. + const jobIdElement = document.getElementById('jobId'); + if (jobIdElement !== null && jobIdElement.getAttribute('data-value')) { + id = jobIdElement.getAttribute('data-value'); + } + + // Determine the job completion status, if present. If the job is not complete, the value will be + // "None". Otherwise, it will be a stringified date. + const jobCompleteElement = document.getElementById('jobComplete'); + if (jobCompleteElement !== null && jobCompleteElement.getAttribute('data-value') !== 'None') { + complete = true; + } + return { id, complete }; +} + +/** + * Update the job status label element based on the API response. + */ +function updateLabel(status: JobStatus) { + const element = document.querySelector('#pending-result-label > span.badge'); + if (element !== null) { + let labelClass = 'secondary'; + switch (status.value) { + case 'failed' || 'errored': + labelClass = 'danger'; + case 'running': + labelClass = 'warning'; + case 'completed': + labelClass = 'success'; + } + element.setAttribute('class', `badge bg-${labelClass}`); + element.innerText = status.label; + } +} + +/** + * Recursively check the job's status. + * @param id Job ID + */ +async function checkJobStatus(id: string) { + const res = await apiGetBase(`/api/extras/job-results/${id}/`); + if (hasError(res)) { + // If the response is an API error, display an error message and stop checking for job status. + const toast = createToast('danger', 'Error', res.error); + toast.show(); + return; + } else { + // Update the job status label. + updateLabel(res.status); + + // If the job is complete, reload the page. + if (['completed', 'failed', 'errored'].includes(res.status.value)) { + location.reload(); + return; + } else { + // Otherwise, keep checking the job's status, backing off 1 second each time, until a 10 + // second interval is reached. + if (timeout < 10000) { + timeout += 1000; + } + await Promise.all([checkJobStatus(id), asyncTimeout(timeout)]); + } + } +} + +if (document !== null) { + const { id, complete } = getJobInfo(); + if (id !== null && !complete) { + // If there is a job ID and it is not completed, check for the job's status. + Promise.resolve(checkJobStatus(id)); + } +} diff --git a/netbox/templates/extras/script.html b/netbox/templates/extras/script.html index 5fbbedd52..2ef75133c 100644 --- a/netbox/templates/extras/script.html +++ b/netbox/templates/extras/script.html @@ -26,10 +26,10 @@ Source -
+
-
+
{% if not perms.extras.run_script %}
@@ -38,25 +38,20 @@ {% endif %}
{% csrf_token %} +
{% if form.requires_input %} -
-
- Script Data -
-
- {% render_form form %} -
-
+

Script Data

{% else %} -
+
This script does not require any input to run.
- {% render_form form %} {% endif %} + {% render_form form %} +
- Cancel - + Cancel +
diff --git a/netbox/templates/extras/script_result.html b/netbox/templates/extras/script_result.html index 76e0613b7..1dc42cacc 100644 --- a/netbox/templates/extras/script_result.html +++ b/netbox/templates/extras/script_result.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'layout.html' %} {% load helpers %} {% load form_helpers %} {% load log_levels %} @@ -7,36 +7,41 @@ {% block title %}{{ script }} - {{ result.get_status_display }}{% endblock %} {% block content %} + +
- +
-

{{ script }}

-

{{ script.Meta.description|render_markdown }}

+

{{ script.Meta.description|render_markdown }}

-
+

Run: {{ result.created }} {% if result.completed %} Duration: {{ result.duration }} {% else %} - +

+ Loading... +
{% endif %} {% include 'extras/inc/job_label.html' with result=result %}

@@ -44,33 +49,35 @@ {% if result.completed %}
-
-
- Script Log +
+
+ Script Log +
+
+ + + + + + + {% for log in result.data.log %} + + + + + + {% empty %} + + + + {% endfor %} +
LineLevelMessage
{{ forloop.counter }}{% log_level log.status %}{{ log.message|render_markdown }}
+ No log output +
- - - - - - - {% for log in result.data.log %} - - - - - - {% empty %} - - - - {% endfor %} -
LineLevelMessage
{{ forloop.counter }}{% log_level log.status %}{{ log.message|render_markdown }}
- No log output -
{% if execution_time %} - @@ -79,7 +86,7 @@ {% else %}
-
Pending results
+
Pending Results
{% endif %} @@ -94,20 +101,7 @@
{% endblock %} - {% block javascript %} - - + {% endblock %}