diff --git a/netbox/extras/views.py b/netbox/extras/views.py index ab9e3ba52..d5d36d364 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -10,6 +10,7 @@ from rq import Worker from netbox.views import generic from utilities.forms import ConfirmationForm +from utilities.htmx import is_htmx from utilities.tables import paginate_table from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin @@ -693,16 +694,26 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View): def get(self, request, job_result_pk): report_content_type = ContentType.objects.get(app_label='extras', model='report') - jobresult = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type) + result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type) # Retrieve the Report and attach the JobResult to it - module, report_name = jobresult.name.split('.') + module, report_name = result.name.split('.') report = get_report(module, report_name) - report.result = jobresult + report.result = result + + # If this is an HTMX request, return only the result HTML + if is_htmx(request): + response = render(request, 'extras/htmx/report_result.html', { + 'report': report, + 'result': result, + }) + if result.completed: + response.status_code = 286 + return response return render(request, 'extras/report_result.html', { 'report': report, - 'result': jobresult, + 'result': result, }) @@ -820,6 +831,16 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View) script = self._get_script(result.name) + # If this is an HTMX request, return only the result HTML + if is_htmx(request): + response = render(request, 'extras/htmx/script_result.html', { + 'script': script, + 'result': result, + }) + if result.completed: + response.status_code = 286 + return response + return render(request, 'extras/script_result.html', { 'script': script, 'result': result, diff --git a/netbox/project-static/bundle.js b/netbox/project-static/bundle.js index 100b70ac8..76a1581ad 100644 --- a/netbox/project-static/bundle.js +++ b/netbox/project-static/bundle.js @@ -40,7 +40,6 @@ async function bundleGraphIQL() { async function bundleNetBox() { const entryPoints = { netbox: 'src/index.ts', - jobs: 'src/jobs.ts', lldp: 'src/device/lldp.ts', config: 'src/device/config.ts', status: 'src/device/status.ts', diff --git a/netbox/project-static/dist/jobs.js b/netbox/project-static/dist/jobs.js deleted file mode 100644 index 2aedf1219..000000000 Binary files a/netbox/project-static/dist/jobs.js and /dev/null differ diff --git a/netbox/project-static/dist/jobs.js.map b/netbox/project-static/dist/jobs.js.map deleted file mode 100644 index d7c1dbcbf..000000000 Binary files a/netbox/project-static/dist/jobs.js.map and /dev/null differ diff --git a/netbox/project-static/src/global.d.ts b/netbox/project-static/src/global.d.ts index bad12c795..89c106e9c 100644 --- a/netbox/project-static/src/global.d.ts +++ b/netbox/project-static/src/global.d.ts @@ -98,38 +98,6 @@ 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; - }; -}; - type APIUserConfig = { tables: { [k: string]: { columns: string[]; available_columns: string[] } }; [k: string]: unknown; diff --git a/netbox/project-static/src/jobs.ts b/netbox/project-static/src/jobs.ts deleted file mode 100644 index dedf0706d..000000000 --- a/netbox/project-static/src/jobs.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { createToast } from './bs'; -import { apiGetBase, hasError, getNetboxData } from './util'; - -let timeout: number = 1000; - -interface JobInfo { - url: 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 complete = false; - - // Determine the API URL for the job status - const url = getNetboxData('data-job-url'); - - // 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 jobComplete = getNetboxData('data-job-complete'); - if (typeof jobComplete === 'string' && jobComplete.toLowerCase() !== 'none') { - complete = true; - } - return { url, 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'; - break; - case 'running': - labelClass = 'warning'; - break; - case 'completed': - labelClass = 'success'; - break; - } - element.setAttribute('class', `badge bg-${labelClass}`); - element.innerText = status.label; - } -} - -/** - * Recursively check the job's status. - * @param url API URL for job result - */ -async function checkJobStatus(url: string) { - const res = await apiGetBase(url); - 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(url), asyncTimeout(timeout)]); - } - } -} - -function initJobs() { - const { url, complete } = getJobInfo(); - - if (url !== null && !complete) { - // If there is a job ID and it is not completed, check for the job's status. - Promise.resolve(checkJobStatus(url)); - } -} - -if (document.readyState !== 'loading') { - initJobs(); -} else { - document.addEventListener('DOMContentLoaded', initJobs); -} diff --git a/netbox/templates/extras/htmx/report_result.html b/netbox/templates/extras/htmx/report_result.html new file mode 100644 index 000000000..b04f0c78c --- /dev/null +++ b/netbox/templates/extras/htmx/report_result.html @@ -0,0 +1,73 @@ +{% load helpers %} + +

+ Initiated: {{ result.created|annotated_date }} + {% if result.completed %} + Duration: {{ result.duration }} + {% endif %} + {% include 'extras/inc/job_label.html' %} +

+{% if result.completed %} +
+
Report Methods
+
+ + {% for method, data in result.data.items %} + + + + + {% endfor %} +
{{ method }} + {{ data.success }} + {{ data.info }} + {{ data.warning }} + {{ data.failure }} +
+
+
+
+
Report Results
+
+ + + + + + + + + + + {% for method, data in result.data.items %} + + + + {% for time, level, obj, url, message in data.log %} + + + + + + + {% endfor %} + {% endfor %} + +
TimeLevelObjectMessage
+ {{ method }} +
{{ time }} + + + {% if obj and url %} + {{ obj }} + {% elif obj %} + {{ obj }} + {% else %} + + {% endif %} + {{ message|render_markdown }}
+
+
+{% else %} + {% include 'extras/inc/result_pending.html' %} +{% endif %} diff --git a/netbox/templates/extras/htmx/script_result.html b/netbox/templates/extras/htmx/script_result.html new file mode 100644 index 000000000..0336bdfaa --- /dev/null +++ b/netbox/templates/extras/htmx/script_result.html @@ -0,0 +1,50 @@ +{% load helpers %} +{% load log_levels %} + +

+ Initiated: {{ result.created|annotated_date }} + {% if result.completed %} + Duration: {{ result.duration }} + {% endif %} + {% include 'extras/inc/job_label.html' %} +

+{% if result.completed %} +
+
Script Log
+
+ + + + + + + {% 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 %} + + {% endif %} +
+

Output

+ {% if result.data.output %} +
{{ result.data.output }}
+ {% else %} +

None

+ {% endif %} +{% else %} + {% include 'extras/inc/result_pending.html' %} +{% endif %} diff --git a/netbox/templates/extras/inc/result_pending.html b/netbox/templates/extras/inc/result_pending.html new file mode 100644 index 000000000..7d053ec2d --- /dev/null +++ b/netbox/templates/extras/inc/result_pending.html @@ -0,0 +1,6 @@ +{# Indicates that a job result is still pending; used for HTMX requests #} +
+ Loading... +
+

Results pending...

+Last updated {% now "H:i:s" %} diff --git a/netbox/templates/extras/report_result.html b/netbox/templates/extras/report_result.html index 90726d287..3a23d705d 100644 --- a/netbox/templates/extras/report_result.html +++ b/netbox/templates/extras/report_result.html @@ -1,99 +1,9 @@ {% extends 'extras/report.html' %} -{% load helpers %} -{% load static %} - -{% block head %} - -{% endblock %} {% block content-wrapper %} -
-
-

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

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

- {% if result.completed %} -
-
- Report Methods -
-
- - {% for method, data in result.data.items %} - - - - - {% endfor %} -
{{ method }} - {{ data.success }} - {{ data.info }} - {{ data.warning }} - {{ data.failure }} -
-
-
-
-
- Report Results -
-
- - - - - - - - - - - {% for method, data in result.data.items %} - - - - {% for time, level, obj, url, message in data.log %} - - - - - - - {% endfor %} - {% endfor %} - -
TimeLevelObjectMessage
- {{ method }} -
{{ time }} - - - {% if obj and url %} - {{ obj }} - {% elif obj %} - {{ obj }} - {% else %} - - {% endif %} - {{ message|render_markdown }}
-
-
- {% else %} -
Pending results
- {% endif %} +
+
+ {% include 'extras/htmx/report_result.html' %}
-
-{% endblock %} - -{% block data %} - - +
{% endblock %} diff --git a/netbox/templates/extras/script_result.html b/netbox/templates/extras/script_result.html index 3cbd0c611..41368ecad 100644 --- a/netbox/templates/extras/script_result.html +++ b/netbox/templates/extras/script_result.html @@ -1,117 +1,48 @@ {% extends 'base/layout.html' %} {% load helpers %} -{% load form_helpers %} -{% load log_levels %} -{% load static %} - -{% block head %} - -{% endblock %} {% block title %}{{ script }}{% endblock %} {% block subtitle %} {{ script.Meta.description|render_markdown }} - {% include 'extras/inc/job_label.html' with result=result %} {% endblock %} {% block header %}
-
- -
+
+ +
{{ block.super }} {% endblock header %} {% block content-wrapper %}
-

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

- Loading... -
- {% endif %} -

-
- {% if result.completed %} -
-
-
-
- Script Log -
-
- - - - - - - {% 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 %} - - {% endif %} -
-
-
- {% else %} -
-
-
Pending Results
-
-
- {% endif %} -
-
-
{{ result.data.output }}
-
-
-

{{ script.filename }}

-
{{ script.source }}
+
+
+
+ {% include 'extras/htmx/script_result.html' %} +
+
+
+

{{ script.filename }}

+
{{ script.source }}
+
{% endblock content-wrapper %} - -{% block data %} - - -{% endblock %}