Преглед на файлове

10587 script pagination (#15343)

* 10587 temp commit

* 10587 temp commit

* 10587 fix migrations

* 10587 pagination

* 10587 pagination

* 10587 pagination

* 10587 review changes
Arthur Hanson преди 1 година
родител
ревизия
663bd32464

+ 0 - 3
netbox/extras/migrations/0108_convert_reports_to_scripts.py

@@ -25,7 +25,4 @@ class Migration(migrations.Migration):
         migrations.DeleteModel(
             name='Report',
         ),
-        migrations.DeleteModel(
-            name='ReportModule',
-        ),
     ]

+ 12 - 4
netbox/extras/migrations/0109_script_model.py

@@ -82,10 +82,12 @@ def update_scripts(apps, schema_editor):
     ContentType = apps.get_model('contenttypes', 'ContentType')
     Script = apps.get_model('extras', 'Script')
     ScriptModule = apps.get_model('extras', 'ScriptModule')
+    ReportModule = apps.get_model('extras', 'ReportModule')
     Job = apps.get_model('core', 'Job')
 
-    script_ct = ContentType.objects.get_for_model(Script)
-    scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule)
+    script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
+    scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule, for_concrete_model=False)
+    reportmodule_ct = ContentType.objects.get_for_model(ReportModule, for_concrete_model=False)
 
     for module in ScriptModule.objects.all():
         for script_name in get_module_scripts(module):
@@ -96,10 +98,16 @@ def update_scripts(apps, schema_editor):
 
             # Update all Jobs associated with this ScriptModule & script name to point to the new Script object
             Job.objects.filter(
-                object_type=scriptmodule_ct,
+                object_type_id=scriptmodule_ct.id,
+                object_id=module.pk,
+                name=script_name
+            ).update(object_type_id=script_ct.id, object_id=script.pk)
+            # Update all Jobs associated with this ScriptModule & script name to point to the new Script object
+            Job.objects.filter(
+                object_type_id=reportmodule_ct.id,
                 object_id=module.pk,
                 name=script_name
-            ).update(object_type=script_ct, object_id=script.pk)
+            ).update(object_type_id=script_ct.id, object_id=script.pk)
 
 
 def update_event_rules(apps, schema_editor):

+ 3 - 0
netbox/extras/migrations/0110_remove_eventrule_action_parameters.py

@@ -12,4 +12,7 @@ class Migration(migrations.Migration):
             model_name='eventrule',
             name='action_parameters',
         ),
+        migrations.DeleteModel(
+            name='ReportModule',
+        ),
     ]

+ 61 - 1
netbox/extras/tables/tables.py

@@ -5,7 +5,7 @@ from django.conf import settings
 from django.utils.translation import gettext_lazy as _
 
 from extras.models import *
-from netbox.tables import NetBoxTable, columns
+from netbox.tables import BaseTable, NetBoxTable, columns
 from .template_code import *
 
 __all__ = (
@@ -21,6 +21,8 @@ __all__ = (
     'JournalEntryTable',
     'ObjectChangeTable',
     'SavedFilterTable',
+    'ReportResultsTable',
+    'ScriptResultsTable',
     'TaggedItemTable',
     'TagTable',
     'WebhookTable',
@@ -507,3 +509,61 @@ class JournalEntryTable(NetBoxTable):
         default_columns = (
             'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
         )
+
+
+class ScriptResultsTable(BaseTable):
+    index = tables.Column(
+        verbose_name=_('Line')
+    )
+    time = tables.Column(
+        verbose_name=_('Time')
+    )
+    status = tables.TemplateColumn(
+        template_code="""{% load log_levels %}{% log_level record.status %}""",
+        verbose_name=_('Level')
+    )
+    message = tables.Column(
+        verbose_name=_('Message')
+    )
+
+    class Meta(BaseTable.Meta):
+        empty_text = _('No results found')
+        fields = (
+            'index', 'time', 'status', 'message',
+        )
+
+
+class ReportResultsTable(BaseTable):
+    index = tables.Column(
+        verbose_name=_('Line')
+    )
+    method = tables.Column(
+        verbose_name=_('Method')
+    )
+    time = tables.Column(
+        verbose_name=_('Time')
+    )
+    status = tables.Column(
+        empty_values=(),
+        verbose_name=_('Level')
+    )
+    status = tables.TemplateColumn(
+        template_code="""{% load log_levels %}{% log_level record.status %}""",
+        verbose_name=_('Level')
+    )
+
+    object = tables.Column(
+        verbose_name=_('Object')
+    )
+    url = tables.Column(
+        verbose_name=_('URL')
+    )
+    message = tables.Column(
+        verbose_name=_('Message')
+    )
+
+    class Meta(BaseTable.Meta):
+        empty_text = _('No results found')
+        fields = (
+            'index', 'method', 'time', 'status', 'object', 'url', 'message',
+        )

+ 56 - 1
netbox/extras/views.py

@@ -17,6 +17,7 @@ from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
 from extras.dashboard.utils import get_widget_class
 from netbox.constants import DEFAULT_ACTION_PERMISSIONS
 from netbox.views import generic
+from netbox.views.generic.mixins import TableMixin
 from utilities.forms import ConfirmationForm, get_field_value
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.rqworker import get_workers_for_queue
@@ -26,6 +27,7 @@ from utilities.views import ContentTypePermissionRequiredMixin, register_model_v
 from . import filtersets, forms, tables
 from .models import *
 from .scripts import run_script
+from .tables import ReportResultsTable, ScriptResultsTable
 
 
 #
@@ -1143,19 +1145,72 @@ class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View):
         return redirect(f'{url}{path}')
 
 
-class ScriptResultView(generic.ObjectView):
+class ScriptResultView(TableMixin, generic.ObjectView):
     queryset = Job.objects.all()
 
     def get_required_permission(self):
         return 'extras.view_script'
 
+    def get_table(self, job, request, bulk_actions=True):
+        data = []
+        tests = None
+        table = None
+        index = 0
+        if job.data:
+            if 'log' in job.data:
+                if 'tests' in job.data:
+                    tests = job.data['tests']
+
+                for log in job.data['log']:
+                    index += 1
+                    result = {
+                        'index': index,
+                        'time': log.get('time'),
+                        'status': log.get('status'),
+                        'message': log.get('message'),
+                    }
+                    data.append(result)
+
+                table = ScriptResultsTable(data, user=request.user)
+                table.configure(request)
+            else:
+                # for legacy reports
+                tests = job.data
+
+        if tests:
+            for method, test_data in tests.items():
+                if 'log' in test_data:
+                    for time, status, obj, url, message in test_data['log']:
+                        index += 1
+                        result = {
+                            'index': index,
+                            'method': method,
+                            'time': time,
+                            'status': status,
+                            'object': obj,
+                            'url': url,
+                            'message': message,
+                        }
+                        data.append(result)
+
+            table = ReportResultsTable(data, user=request.user)
+            table.configure(request)
+
+        return table
+
     def get(self, request, **kwargs):
+        table = None
         job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk'))
 
+        if job.completed:
+            table = self.get_table(job, request, bulk_actions=False)
+
         context = {
             'script': job.object,
             'job': job,
+            'table': table,
         }
+
         if job.data and 'log' in job.data:
             # Script
             context['tests'] = job.data.get('tests', {})

+ 49 - 110
netbox/templates/extras/htmx/script_result.html

@@ -3,124 +3,63 @@
 {% load log_levels %}
 {% load i18n %}
 
-<p>
-  {% if job.started %}
-    {% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
-  {% elif job.scheduled %}
-    {% trans "Scheduled for" %}: <strong>{{ job.scheduled|annotated_date }}</strong> ({{ job.scheduled|naturaltime }})
-  {% else %}
-    {% trans "Created" %}: <strong>{{ job.created|annotated_date }}</strong>
-  {% endif %}
+<div class="htmx-container">
+  <p>
+    {% if job.started %}
+      {% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
+    {% elif job.scheduled %}
+      {% trans "Scheduled for" %}: <strong>{{ job.scheduled|annotated_date }}</strong> ({{ job.scheduled|naturaltime }})
+    {% else %}
+      {% trans "Created" %}: <strong>{{ job.created|annotated_date }}</strong>
+    {% endif %}
+    {% if job.completed %}
+      {% trans "Duration" %}: <strong>{{ job.duration }}</strong>
+    {% endif %}
+    <span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
+  </p>
   {% if job.completed %}
-    {% trans "Duration" %}: <strong>{{ job.duration }}</strong>
-  {% endif %}
-  <span id="pending-result-label">{% badge job.get_status_display job.get_status_color %}</span>
-</p>
-{% if job.completed %}
-
-  {# Script log. Legacy reports will not have this. #}
-  {% if 'log' in job.data %}
-    <div class="card mb-3">
-      <h5 class="card-header">{% trans "Log" %}</h5>
-      {% if job.data.log %}
-        <table class="table table-hover panel-body">
-          <tr>
-            <th>{% trans "Line" %}</th>
-            <th>{% trans "Time" %}</th>
-            <th>{% trans "Level" %}</th>
-            <th>{% trans "Message" %}</th>
-          </tr>
-          {% for log in job.data.log %}
+    {% if tests %}
+      {# Summary of test methods #}
+      <div class="card">
+        <h5 class="card-header">{% trans "Test Summary" %}</h5>
+        <table class="table table-hover">
+          {% for test, data in tests.items %}
             <tr>
-              <td>{{ forloop.counter }}</td>
-              <td>{{ log.time|placeholder }}</td>
-              <td>{% log_level log.status %}</td>
-              <td>{{ log.message|markdown }}</td>
+              <td class="font-monospace"><a href="#{{ test }}">{{ test }}</a></td>
+              <td class="text-end report-stats">
+                <span class="badge text-bg-success">{{ data.success }}</span>
+                <span class="badge text-bg-info">{{ data.info }}</span>
+                <span class="badge text-bg-warning">{{ data.warning }}</span>
+                <span class="badge text-bg-danger">{{ data.failure }}</span>
+              </td>
             </tr>
           {% endfor %}
         </table>
-      {% else %}
-        <div class="card-body text-muted">{% trans "None" %}</div>
-      {% endif %}
-    </div>
-  {% endif %}
-
-  {# Script output. Legacy reports will not have this. #}
-  {% if 'output' in job.data %}
-    <div class="card mb-3">
-    <h5 class="card-header">{% trans "Output" %}</h5>
-      {% if job.data.output %}
-        <pre class="card-body font-monospace">{{ job.data.output }}</pre>
-      {% else %}
-        <div class="card-body text-muted">{% trans "None" %}</div>
-      {% endif %}
-    </div>
-  {% endif %}
-
-  {# Test method logs (for legacy Reports) #}
-  {% if tests %}
+      </div>
+    {% endif %}
 
-    {# Summary of test methods #}
+    {% if table %}
     <div class="card">
-      <h5 class="card-header">{% trans "Test Summary" %}</h5>
-      <table class="table table-hover">
-        {% for test, data in tests.items %}
-          <tr>
-            <td class="font-monospace"><a href="#{{ test }}">{{ test }}</a></td>
-            <td class="text-end report-stats">
-              <span class="badge text-bg-success">{{ data.success }}</span>
-              <span class="badge text-bg-info">{{ data.info }}</span>
-              <span class="badge text-bg-warning">{{ data.warning }}</span>
-              <span class="badge text-bg-danger">{{ data.failure }}</span>
-            </td>
-          </tr>
-        {% endfor %}
-      </table>
+      <div class="table-responsive" id="object_list">
+        <h5 class="card-header">{% trans "Log" %}</h5>
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+    {% endif %}
 
-    {# Detailed results for individual tests #}
-    <div class="card">
-      <h5 class="card-header">{% trans "Test Details" %}</h5>
-      <table class="table table-hover report">
-        <thead>
-          <tr class="table-headings">
-            <th>{% trans "Time" %}</th>
-            <th>{% trans "Level" %}</th>
-            <th>{% trans "Object" %}</th>
-            <th>{% trans "Message" %}</th>
-          </tr>
-        </thead>
-        <tbody>
-          {% for test, data in tests.items %}
-            <tr>
-              <th colspan="4" style="font-family: monospace">
-                <a name="{{ test }}"></a>{{ test }}
-              </th>
-            </tr>
-            {% for time, level, obj, url, message in data.log %}
-              <tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
-                <td>{{ time }}</td>
-                <td>
-                  <label class="badge text-bg-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
-                </td>
-                <td>
-                  {% if obj and url %}
-                    <a href="{{ url }}">{{ obj }}</a>
-                  {% elif obj %}
-                    {{ obj }}
-                  {% else %}
-                    {{ ''|placeholder }}
-                  {% endif %}
-                </td>
-                <td class="rendered-markdown">{{ message|markdown }}</td>
-              </tr>
-            {% endfor %}
-          {% endfor %}
-        </tbody>
-      </table>
-    </div>
+    {# Script output. Legacy reports will not have this. #}
+    {% if 'output' in job.data %}
+      <div class="card mb-3">
+      <h5 class="card-header">{% trans "Output" %}</h5>
+        {% if job.data.output %}
+          <pre class="card-body font-monospace">{{ job.data.output }}</pre>
+        {% else %}
+          <div class="card-body text-muted">{% trans "None" %}</div>
+        {% endif %}
+      </div>
+    {% endif %}
 
+  {% elif job.started %}
+    {% include 'extras/inc/result_pending.html' %}
   {% endif %}
-{% elif job.started %}
-  {% include 'extras/inc/result_pending.html' %}
-{% endif %}
+</div>

+ 60 - 14
netbox/templates/extras/script_result.html

@@ -32,28 +32,74 @@
 {% block tabs %}
   <ul class="nav nav-tabs" role="tablist">
     <li class="nav-item" role="presentation">
-      <a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">{% trans "Log" %}</a>
-    </li>
-    <li class="nav-item" role="presentation">
-      <a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">{% trans "Source" %}</a>
+      <a href="#results" role="tab" data-bs-toggle="tab" class="nav-link active">{% trans "Results" %}</a>
     </li>
   </ul>
 {% endblock %}
 
 {% block content %}
-  <div role="tabpanel" class="tab-pane active" id="log">
-    <div class="row">
-      <div class="col col-md-12"{% if not job.completed %} hx-get="{% url 'extras:script_result' job_pk=job.pk %}" hx-trigger="load delay:0.5s, every 5s"{% endif %}>
-        {% include 'extras/htmx/script_result.html' %}
+    {# Object list tab #}
+    <div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab">
+
+      {# Object table controls #}
+      <div class="row mb-3">
+        <div class="col-auto ms-auto d-print-none">
+          {% if request.user.is_authenticated %}
+            <div class="table-configure input-group">
+              <button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#ObjectTable_config"
+                class="btn">
+                <i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
+              </button>
+            </div>
+          {% endif %}
+        </div>
       </div>
+
+      <form method="post" class="form form-horizontal">
+        {% csrf_token %}
+        {# "Select all" form #}
+        {% if table.paginator.num_pages > 1 %}
+          <div id="select-all-box" class="d-none card d-print-none">
+            <div class="form col-md-12">
+              <div class="card-body">
+                <div class="form-check">
+                  <input type="checkbox" id="select-all" name="_all" class="form-check-input" />
+                  <label for="select-all" class="form-check-label">
+                    {% blocktrans trimmed with count=table.rows|length object_type_plural=table.data.verbose_name_plural %}
+                      Select <strong>all {{ count }} {{ object_type_plural }}</strong> matching query
+                    {% endblocktrans %}
+                  </label>
+                </div>
+              </div>
+            </div>
+          </div>
+        {% endif %}
+
+        <div class="form form-horizontal">
+          {% csrf_token %}
+          <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
+
+          {# Objects table #}
+            <div class="col col-md-12"{% if not job.completed %} hx-get="{% url 'extras:script_result' job_pk=job.pk %}" hx-trigger="load delay:0.5s, every 5s"{% endif %}>
+              {% include 'extras/htmx/script_result.html' %}
+            </div>
+          {# /Objects table #}
+
+        </div>
+      </form>
     </div>
-  </div>
-  <div role="tabpanel" class="tab-pane" id="source">
-    <p><code>{{ script.filename }}</code></p>
-    <pre class="block">{{ script.source }}</pre>
-  </div>
+    {# /Object list tab #}
+
+    {# Filters tab #}
+    {% if filter_form %}
+      <div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
+        {% include 'inc/filter_list.html' %}
+      </div>
+    {% endif %}
+    {# /Filters tab #}
+
 {% endblock content %}
 
 {% block modals %}
-  {% include 'inc/htmx_modal.html' %}
+  {% table_config_form table table_name="ObjectTable" %}
 {% endblock modals %}