mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #8130 from netbox-community/8114-htmx-jobs
Closes #8114: Use HTMX to update report/script results
This commit is contained in:
commit
9ffd791ae4
@ -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,
|
||||
|
@ -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',
|
||||
|
BIN
netbox/project-static/dist/jobs.js
vendored
BIN
netbox/project-static/dist/jobs.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js.map
vendored
BIN
netbox/project-static/dist/jobs.js.map
vendored
Binary file not shown.
32
netbox/project-static/src/global.d.ts
vendored
32
netbox/project-static/src/global.d.ts
vendored
@ -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;
|
||||
|
@ -1,104 +0,0 @@
|
||||
import { createToast } from './bs';
|
||||
import { apiGetBase, hasError, getNetboxData } from './util';
|
||||
|
||||
let timeout: number = 1000;
|
||||
|
||||
interface JobInfo {
|
||||
url: 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 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<HTMLSpanElement>('#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<APIJobResult>(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);
|
||||
}
|
73
netbox/templates/extras/htmx/report_result.html
Normal file
73
netbox/templates/extras/htmx/report_result.html
Normal file
@ -0,0 +1,73 @@
|
||||
{% load helpers %}
|
||||
|
||||
<p>
|
||||
Initiated: <strong>{{ result.created|annotated_date }}</strong>
|
||||
{% if result.completed %}
|
||||
Duration: <strong>{{ result.duration }}</strong>
|
||||
{% endif %}
|
||||
<span id="pending-result-label">{% include 'extras/inc/job_label.html' %}</span>
|
||||
</p>
|
||||
{% if result.completed %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Report Methods</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
{% for method, data in result.data.items %}
|
||||
<tr>
|
||||
<td class="font-monospace"><a href="#{{ method }}">{{ method }}</a></td>
|
||||
<td class="text-end report-stats">
|
||||
<span class="badge bg-success">{{ data.success }}</span>
|
||||
<span class="badge bg-info">{{ data.info }}</span>
|
||||
<span class="badge bg-warning">{{ data.warning }}</span>
|
||||
<span class="badge bg-danger">{{ data.failure }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">Report Results</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover report">
|
||||
<thead>
|
||||
<tr class="table-headings">
|
||||
<th>Time</th>
|
||||
<th>Level</th>
|
||||
<th>Object</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for method, data in result.data.items %}
|
||||
<tr>
|
||||
<th colspan="4" style="font-family: monospace">
|
||||
<a name="{{ method }}"></a>{{ method }}
|
||||
</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 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 %}
|
||||
<span class="muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="rendered-markdown">{{ message|render_markdown }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% include 'extras/inc/result_pending.html' %}
|
||||
{% endif %}
|
50
netbox/templates/extras/htmx/script_result.html
Normal file
50
netbox/templates/extras/htmx/script_result.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% load helpers %}
|
||||
{% load log_levels %}
|
||||
|
||||
<p>
|
||||
Initiated: <strong>{{ result.created|annotated_date }}</strong>
|
||||
{% if result.completed %}
|
||||
Duration: <strong>{{ result.duration }}</strong>
|
||||
{% endif %}
|
||||
<span id="pending-result-label">{% include 'extras/inc/job_label.html' %}</span>
|
||||
</p>
|
||||
{% if result.completed %}
|
||||
<div class="card mb-3">
|
||||
<h5 class="card-header">Script Log</h5>
|
||||
<div class="card-body">
|
||||
<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 %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
No log output
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% if execution_time %}
|
||||
<div class="card-footer text-end text-muted">
|
||||
<small>Exec Time: {{ execution_time|floatformat:3 }}s</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h4>Output</h4>
|
||||
{% if result.data.output %}
|
||||
<pre class="block">{{ result.data.output }}</pre>
|
||||
{% else %}
|
||||
<p class="text-muted">None</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% include 'extras/inc/result_pending.html' %}
|
||||
{% endif %}
|
6
netbox/templates/extras/inc/result_pending.html
Normal file
6
netbox/templates/extras/inc/result_pending.html
Normal file
@ -0,0 +1,6 @@
|
||||
{# Indicates that a job result is still pending; used for HTMX requests #}
|
||||
<div class="spinner-border float-start me-1" id="spinner" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h3>Results pending...</h3>
|
||||
<small class="text-muted">Last updated {% now "H:i:s" %}</small>
|
@ -1,99 +1,9 @@
|
||||
{% extends 'extras/report.html' %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'jobs.js' %}?v{{ settings.VERSION }}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=jobs.js'"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<div class="row px-3">
|
||||
<div class="col col-md-12">
|
||||
<p>
|
||||
Run: <strong>{{ result.created|annotated_date }}</strong>
|
||||
{% if result.completed %}
|
||||
Duration: <strong>{{ result.duration }}</strong>
|
||||
{% else %}
|
||||
<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>
|
||||
{% if result.completed %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Report Methods
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
{% for method, data in result.data.items %}
|
||||
<tr>
|
||||
<td class="font-monospace"><a href="#{{ method }}">{{ method }}</a></td>
|
||||
<td class="text-end report-stats">
|
||||
<span class="badge bg-success">{{ data.success }}</span>
|
||||
<span class="badge bg-info">{{ data.info }}</span>
|
||||
<span class="badge bg-warning">{{ data.warning }}</span>
|
||||
<span class="badge bg-danger">{{ data.failure }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Report Results
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover report">
|
||||
<thead>
|
||||
<tr class="table-headings">
|
||||
<th>Time</th>
|
||||
<th>Level</th>
|
||||
<th>Object</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for method, data in result.data.items %}
|
||||
<tr>
|
||||
<th colspan="4" style="font-family: monospace">
|
||||
<a name="{{ method }}"></a>{{ method }}
|
||||
</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 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 %}
|
||||
<span class="muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="rendered-markdown">{{ message|render_markdown }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="well">Pending results</div>
|
||||
{% endif %}
|
||||
<div class="row px-3">
|
||||
<div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:report_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
|
||||
{% include 'extras/htmx/report_result.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block data %}
|
||||
<span data-job-url="{% url 'extras-api:jobresult-detail' pk=result.pk %}"></span>
|
||||
<span data-job-complete="{{ result.completed }}"></span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,117 +1,48 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
{% load log_levels %}
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'jobs.js' %}?v{{ settings.VERSION }}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=jobs.js'"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ script }}{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{{ script.Meta.description|render_markdown }}
|
||||
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col col-md-12">
|
||||
<nav class="breadcrumb-container px-3" 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|annotated_date }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<nav class="breadcrumb-container px-3" 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|annotated_date }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock header %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<ul class="nav nav-tabs px-3" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">Log</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="#output" role="tab" data-bs-toggle="tab" class="nav-link">Output</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">Log</a>
|
||||
</li>
|
||||
<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 mb-3">
|
||||
<p>
|
||||
Run: <strong>{{ result.created|annotated_date }}</strong>
|
||||
{% if result.completed %}
|
||||
Duration: <strong>{{ result.duration }}</strong>
|
||||
{% else %}
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</p>
|
||||
<div role="tabpanel" class="tab-pane active" id="log">
|
||||
{% if result.completed %}
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Script Log
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<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 %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
No log output
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% if execution_time %}
|
||||
<div class="card-footer text-end text-muted">
|
||||
<small>Exec Time: {{ execution_time|floatformat:3 }}s</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="well">Pending Results</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="output">
|
||||
<pre class="block">{{ result.data.output }}</pre>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="source">
|
||||
<p><code>{{ script.filename }}</code></p>
|
||||
<pre class="block">{{ script.source }}</pre>
|
||||
<div role="tabpanel" class="tab-pane active" id="log">
|
||||
<div class="row">
|
||||
<div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:script_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
|
||||
{% include 'extras/htmx/script_result.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="source">
|
||||
<p><code>{{ script.filename }}</code></p>
|
||||
<pre class="block">{{ script.source }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content-wrapper %}
|
||||
|
||||
{% block data %}
|
||||
<span data-job-url="{% url 'extras-api:jobresult-detail' pk=result.pk %}"></span>
|
||||
<span data-job-complete="{{ result.completed }}"></span>
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user