mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
migrate script job checking to typescript & update templates for bootstrap 5
This commit is contained in:
128
netbox/project-static/dist/jobs.js
vendored
Normal file
128
netbox/project-static/dist/jobs.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
netbox/project-static/dist/jobs.js.map
vendored
Normal file
1
netbox/project-static/dist/jobs.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -5,7 +5,7 @@
|
|||||||
"license": "Apache2",
|
"license": "Apache2",
|
||||||
"scripts": {
|
"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: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"
|
"bundle": "yarn bundle:css && yarn bundle:js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
32
netbox/project-static/src/global.d.ts
vendored
32
netbox/project-static/src/global.d.ts
vendored
@ -59,6 +59,38 @@ type APISecret = {
|
|||||||
url: string;
|
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 {
|
interface ObjectWithGroup extends APIObjectBase {
|
||||||
group: Nullable<APIReference>;
|
group: Nullable<APIReference>;
|
||||||
}
|
}
|
||||||
|
98
netbox/project-static/src/jobs.ts
Normal file
98
netbox/project-static/src/jobs.ts
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -26,10 +26,10 @@
|
|||||||
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
|
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content py-3">
|
||||||
<div role="tabpanel" class="tab-pane active" id="run">
|
<div role="tabpanel" class="tab-pane active" id="run">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6">
|
||||||
{% if not perms.extras.run_script %}
|
{% if not perms.extras.run_script %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<i class="mdi mdi-alert"></i>
|
<i class="mdi mdi-alert"></i>
|
||||||
@ -38,25 +38,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
|
<form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<div class="field-group mb-3">
|
||||||
{% if form.requires_input %}
|
{% if form.requires_input %}
|
||||||
<div class="card my-3">
|
<h4>Script Data</h4>
|
||||||
<h5 class="card-header">
|
|
||||||
Script Data
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% render_form form %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<i class="mdi mdi-information"></i>
|
<i class="mdi mdi-information"></i>
|
||||||
This script does not require any input to run.
|
This script does not require any input to run.
|
||||||
</div>
|
</div>
|
||||||
{% render_form form %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% render_form form %}
|
||||||
|
</div>
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
<a href="{% url 'extras:script_list' %}" class="btn btn-outline-danger btn-sm">Cancel</a>
|
<a href="{% url 'extras:script_list' %}" class="btn btn-outline-danger">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>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
{% load log_levels %}
|
{% load log_levels %}
|
||||||
@ -7,36 +7,41 @@
|
|||||||
{% block title %}{{ script }} - {{ result.get_status_display }}{% endblock %}
|
{% block title %}{{ script }} - {{ result.get_status_display }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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="row noprint">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<ol class="breadcrumb">
|
<nav class="breadcrumb-container" aria-label="breadcrumb">
|
||||||
<li><a href="{% url 'extras:script_list' %}">Scripts</a></li>
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
|
||||||
<li><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
|
||||||
<li>{{ result.created }}</li>
|
<li class="breadcrumb-item"><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
|
||||||
</ol>
|
<li class="breadcrumb-item">{{ result.created }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="title">{{ script }}</h1>
|
<p class="text-muted">{{ script.Meta.description|render_markdown }}</p>
|
||||||
<p>{{ script.Meta.description|render_markdown }}</p>
|
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<li role="presentation" class="active">
|
<li class="nav-item" role="presentation">
|
||||||
<a href="#log" role="tab" data-toggle="tab" class="active">Log</a>
|
<a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">Log</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a href="#output" role="tab" data-toggle="tab">Output</a>
|
<a href="#output" role="tab" data-bs-toggle="tab" class="nav-link">Output</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a href="#source" role="tab" data-toggle="tab">Source</a>
|
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content my-3">
|
||||||
<p>
|
<p>
|
||||||
Run: <strong>{{ result.created }}</strong>
|
Run: <strong>{{ result.created }}</strong>
|
||||||
{% if result.completed %}
|
{% if result.completed %}
|
||||||
Duration: <strong>{{ result.duration }}</strong>
|
Duration: <strong>{{ result.duration }}</strong>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
|
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
|
||||||
</p>
|
</p>
|
||||||
@ -44,33 +49,35 @@
|
|||||||
{% if result.completed %}
|
{% if result.completed %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<h5 class="card-header">
|
||||||
<strong>Script Log</strong>
|
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>
|
</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 %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="3" class="text-center text-muted">
|
|
||||||
No log output
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% if execution_time %}
|
{% if execution_time %}
|
||||||
<div class="panel-footer text-right text-muted">
|
<div class="card-footer text-end text-muted">
|
||||||
<small>Exec time: {{ execution_time|floatformat:3 }}s</small>
|
<small>Exec Time: {{ execution_time|floatformat:3 }}s</small>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +86,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="well">Pending results</div>
|
<div class="well">Pending Results</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -94,20 +101,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script type="text/javascript">
|
<script src="{% static 'jobs.js' %}?v{{ settings.VERSION }}"
|
||||||
{% if not result.completed %}
|
onerror="window.location='{% url 'media_failure' %}?filename=jobs.js'"></script>
|
||||||
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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Reference in New Issue
Block a user