Răsfoiți Sursa

migrate script job checking to typescript & update templates for bootstrap 5

checktheroads 4 ani în urmă
părinte
comite
4b0d5815c0

Fișier diff suprimat deoarece este prea mare
+ 43 - 0
netbox/project-static/dist/jobs.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
netbox/project-static/dist/jobs.js.map


+ 1 - 1
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": {

+ 32 - 0
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<APIReference>;
 }

+ 98 - 0
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<string>;
+  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<string> = 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<HTMLSpanElement>('#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<APIJobResult>(`/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));
+  }
+}

+ 9 - 14
netbox/templates/extras/script.html

@@ -26,10 +26,10 @@
             <a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
         </li>
     </ul>
-    <div class="tab-content">
+    <div class="tab-content py-3">
         <div role="tabpanel" class="tab-pane active" id="run">
             <div class="row">
-                <div class="col-md-6 col-md-offset-3">
+                <div class="col-md-6">
                     {% if not perms.extras.run_script %}
                         <div class="alert alert-warning">
                             <i class="mdi mdi-alert"></i>
@@ -38,25 +38,20 @@
                     {% endif %}
                     <form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
                         {% csrf_token %}
+                        <div class="field-group mb-3">
                         {% if form.requires_input %}
-                            <div class="card my-3">
-                                <h5 class="card-header">
-                                    Script Data
-                                </h5>
-                                <div class="card-body">
-                                    {% render_form form %}
-                                </div>
-                            </div>
+                            <h4>Script Data</h4>
                         {% else %}
-                             <div class="alert alert-info">
+                            <div class="alert alert-info">
                                 <i class="mdi mdi-information"></i>
                                 This script does not require any input to run.
                             </div>
-                            {% render_form form %}
                         {% endif %}
+                        {% render_form form %}
+                        </div>
                         <div class="float-end">
-                            <a href="{% url 'extras:script_list' %}" class="btn btn-outline-danger btn-sm">Cancel</a>
-                            <button type="submit" name="_run" class="btn btn-primary btn-sm"{% if not perms.extras.run_script %} disabled="disabled"{% endif %}><i class="mdi mdi-play"></i> Run Script</button>
+                            <a href="{% url 'extras:script_list' %}" class="btn btn-outline-danger">Cancel</a>
+                            <button type="submit" name="_run" class="btn btn-primary"{% if not perms.extras.run_script %} disabled="disabled"{% endif %}><i class="mdi mdi-play"></i> Run Script</button>
                         </div>
                     </form>
                 </div>

+ 51 - 57
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 %}
+    <span id="jobId" data-value="{{ result.pk }}" style="display: none;"></span>
+    <span id="jobComplete" data-value="{{ result.completed }}" style="display: none;"></span>
     <div class="row noprint">
         <div class="col-md-12">
-            <ol class="breadcrumb">
-                <li><a href="{% url 'extras:script_list' %}">Scripts</a></li>
-                <li><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
-                <li><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
-                <li>{{ result.created }}</li>
-            </ol>
+            <nav class="breadcrumb-container" aria-label="breadcrumb">
+                <ol class="breadcrumb">
+                    <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
+                    <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
+                    <li class="breadcrumb-item"><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
+                    <li class="breadcrumb-item">{{ result.created }}</li>
+                </ol>
+            </nav>
         </div>
     </div>
-    <h1 class="title">{{ script }}</h1>
-    <p>{{ script.Meta.description|render_markdown }}</p>
+    <p class="text-muted">{{ script.Meta.description|render_markdown }}</p>
     <ul class="nav nav-tabs" role="tablist">
-        <li role="presentation" class="active">
-            <a href="#log" role="tab" data-toggle="tab" class="active">Log</a>
+        <li class="nav-item" role="presentation">
+            <a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">Log</a>
         </li>
-        <li role="presentation">
-            <a href="#output" role="tab" data-toggle="tab">Output</a>
+        <li class="nav-item" role="presentation">
+            <a href="#output" role="tab" data-bs-toggle="tab" class="nav-link">Output</a>
         </li>
-        <li role="presentation">
-            <a href="#source" role="tab" data-toggle="tab">Source</a>
+        <li class="nav-item" role="presentation">
+            <a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
         </li>
     </ul>
-    <div class="tab-content">
+    <div class="tab-content my-3">
         <p>
             Run: <strong>{{ result.created }}</strong>
             {% if result.completed %}
                 Duration: <strong>{{ result.duration }}</strong>
             {% else %}
-                <img id="pending-result-loader" src="{% static 'img/ajax-loader.gif' %}" />
+                <div class="spinner-border" role="status">
+                    <span class="visually-hidden">Loading...</span>
+                </div>
             {% endif %}
             <span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
         </p>
@@ -44,33 +49,35 @@
             {% if result.completed %}
                 <div class="row">
                     <div class="col-md-12">
-                        <div class="panel panel-default">
-                            <div class="panel-heading">
-                                <strong>Script Log</strong>
-                            </div>
-                            <table class="table table-hover panel-body">
-                                <tr>
-                                    <th>Line</th>
-                                    <th>Level</th>
-                                    <th>Message</th>
-                                </tr>
-                                {% for log in result.data.log %}
-                                    <tr>
-                                        <td>{{ forloop.counter }}</td>
-                                        <td>{% log_level log.status %}</td>
-                                        <td class="rendered-markdown">{{ log.message|render_markdown }}</td>
-                                    </tr>
-                                {% empty %}
+                        <div class="card">
+                            <h5 class="card-header">
+                                Script Log
+                            </h5>
+                            <div class="card-body">
+                                <table class="table table-hover panel-body">
                                     <tr>
-                                        <td colspan="3" class="text-center text-muted">
-                                            No log output
-                                        </td>
+                                        <th>Line</th>
+                                        <th>Level</th>
+                                        <th>Message</th>
                                     </tr>
-                                {% endfor %}
-                            </table>
+                                    {% for log in result.data.log %}
+                                        <tr>
+                                            <td>{{ forloop.counter }}</td>
+                                            <td>{% log_level log.status %}</td>
+                                            <td class="rendered-markdown">{{ log.message|render_markdown }}</td>
+                                        </tr>
+                                    {% empty %}
+                                        <tr>
+                                            <td colspan="3" class="text-center text-muted">
+                                                No log output
+                                            </td>
+                                        </tr>
+                                    {% endfor %}
+                                </table>
+                            </div>
                             {% if execution_time %}
-                                <div class="panel-footer text-right text-muted">
-                                    <small>Exec time: {{ execution_time|floatformat:3 }}s</small>
+                                <div class="card-footer text-end text-muted">
+                                    <small>Exec Time: {{ execution_time|floatformat:3 }}s</small>
                                 </div>
                             {% endif %}
                         </div>
@@ -79,7 +86,7 @@
             {% else %}
                 <div class="row">
                     <div class="col-md-12">
-                        <div class="well">Pending results</div>
+                        <div class="well">Pending Results</div>
                     </div>
                 </div>
             {% endif %}
@@ -94,20 +101,7 @@
     </div>
 {% endblock %}
 
-
 {% block javascript %}
-<script type="text/javascript">
-{% if not result.completed %}
-var pending_result_id = {{ result.pk }};
-{% else %}
-var pending_result_id = null;
-{% endif %}
-
-function jobTerminatedAction(){
-    refreshWindow()
-}
-
-</script>
-<script src="{% static 'js/job_result.js' %}?v{{ settings.VERSION }}"
-        onerror="window.location='{% url 'media_failure' %}?filename=js/job_result.js'"></script>
+<script src="{% static 'jobs.js' %}?v{{ settings.VERSION }}"
+        onerror="window.location='{% url 'media_failure' %}?filename=jobs.js'"></script>
 {% endblock %}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff