1
0
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:
checktheroads
2021-04-18 14:17:13 -07:00
parent 5c07a968fe
commit 4b0d5815c0
7 changed files with 321 additions and 73 deletions

128
netbox/project-static/dist/jobs.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -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": {

View File

@@ -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>;
}

View 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));
}
}

View File

@@ -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>

View File

@@ -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 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>
<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 %}
<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 %}