mirror of
https://github.com/ArchiveBox/ArchiveBox
synced 2024-11-29 15:40:21 +00:00
add jobs dashboard
This commit is contained in:
parent
fb82fdae16
commit
36d24cd8d7
3 changed files with 181 additions and 16 deletions
146
archivebox/actors/templates/jobs_dashboard.html
Normal file
146
archivebox/actors/templates/jobs_dashboard.html
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Job Dashboard</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.dashboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.scroll-area {
|
||||||
|
height: 800px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.job-item {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.job-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 7px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.badge-started {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.badge-queued {
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.badge-failed {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.date {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Job Dashboard <small><a href="?refresh=true">♻️ {{now}}</a></small></h1>
|
||||||
|
<div id="dashboard" class="dashboard"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function formatDate(dateString) {
|
||||||
|
// return new Date(dateString).toLocaleString();
|
||||||
|
return new Date(dateString).toISOString().split('T').at(-1).replace('Z', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createJobElement(job) {
|
||||||
|
const jobElement = document.createElement('div');
|
||||||
|
jobElement.className = 'job-item';
|
||||||
|
jobElement.innerHTML = `
|
||||||
|
<p><a href="/api/v1/core/any/${job.abid}?api_key={{api_token|default:'NONE PROVIDED BY VIEW'}}"><code>${job.abid}</code></a></p>
|
||||||
|
<p>
|
||||||
|
<span class="badge badge-${job.status}">${job.status}</span>
|
||||||
|
<span class="date">♻️ ${formatDate(job.retry_at)}</span>
|
||||||
|
</p>
|
||||||
|
<p style="font-size: 12px; color: #666;">${job.description}</p>
|
||||||
|
`;
|
||||||
|
return jobElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDashboard(data) {
|
||||||
|
const dashboard = document.getElementById('dashboard');
|
||||||
|
dashboard.innerHTML = '';
|
||||||
|
|
||||||
|
data.forEach(actor => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<h2>${actor.model}</h2>
|
||||||
|
<h3>Queue</h3>
|
||||||
|
<div class="scroll-area" id="queue-${actor.model}"></div>
|
||||||
|
<h3>Past Tasks</h3>
|
||||||
|
<div class="scroll-area" id="past-${actor.model}"></div>
|
||||||
|
`;
|
||||||
|
dashboard.appendChild(card);
|
||||||
|
|
||||||
|
const queueContainer = document.getElementById(`queue-${actor.model}`);
|
||||||
|
actor.queue.forEach(job => {
|
||||||
|
queueContainer.appendChild(createJobElement(job));
|
||||||
|
});
|
||||||
|
|
||||||
|
const pastContainer = document.getElementById(`past-${actor.model}`);
|
||||||
|
actor.past.forEach(job => {
|
||||||
|
pastContainer.appendChild(createJobElement(job));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchData() {
|
||||||
|
fetch('/api/v1/jobs/actors', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer {{api_token|default:'NONE PROVIDED BY VIEW'}}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => updateDashboard(data))
|
||||||
|
.catch(error => console.error('Error fetching data:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
setInterval(fetchData, 1000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,3 +1,20 @@
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
from django.views.generic import TemplateView
|
||||||
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
|
from django.utils import timezone
|
||||||
|
from api.auth import get_or_create_api_token
|
||||||
|
|
||||||
|
|
||||||
|
class JobsDashboardView(UserPassesTestMixin, TemplateView):
|
||||||
|
template_name = "jobs_dashboard.html"
|
||||||
|
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
return self.request.user and self.request.user.is_superuser
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
api_token = get_or_create_api_token(self.request.user)
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['api_token'] = api_token.token if api_token else 'UNABLE TO GENERATE API TOKEN'
|
||||||
|
context['now'] = timezone.now().strftime("%H:%M:%S")
|
||||||
|
return context
|
||||||
|
|
|
@ -10,6 +10,8 @@ from archivebox.misc.serve_static import serve_static
|
||||||
from core.admin_site import archivebox_admin
|
from core.admin_site import archivebox_admin
|
||||||
from core.views import HomepageView, SnapshotView, PublicIndexView, AddView, HealthCheckView
|
from core.views import HomepageView, SnapshotView, PublicIndexView, AddView, HealthCheckView
|
||||||
|
|
||||||
|
from actors.views import JobsDashboardView
|
||||||
|
|
||||||
# GLOBAL_CONTEXT doesn't work as-is, disabled for now: https://github.com/ArchiveBox/ArchiveBox/discussions/1306
|
# GLOBAL_CONTEXT doesn't work as-is, disabled for now: https://github.com/ArchiveBox/ArchiveBox/discussions/1306
|
||||||
# from archivebox.config import VERSION, VERSIONS_AVAILABLE, CAN_UPGRADE
|
# from archivebox.config import VERSION, VERSIONS_AVAILABLE, CAN_UPGRADE
|
||||||
# GLOBAL_CONTEXT = {'VERSION': VERSION, 'VERSIONS_AVAILABLE': VERSIONS_AVAILABLE, 'CAN_UPGRADE': CAN_UPGRADE}
|
# GLOBAL_CONTEXT = {'VERSION': VERSION, 'VERSIONS_AVAILABLE': VERSIONS_AVAILABLE, 'CAN_UPGRADE': CAN_UPGRADE}
|
||||||
|
@ -21,30 +23,30 @@ urlpatterns = [
|
||||||
re_path(r"^static/(?P<path>.*)$", serve_static),
|
re_path(r"^static/(?P<path>.*)$", serve_static),
|
||||||
# re_path(r"^media/(?P<path>.*)$", static.serve, {"document_root": settings.MEDIA_ROOT}),
|
# re_path(r"^media/(?P<path>.*)$", static.serve, {"document_root": settings.MEDIA_ROOT}),
|
||||||
|
|
||||||
|
path('health/', HealthCheckView.as_view(), name='healthcheck'),
|
||||||
|
path('error/', lambda *_: 1/0), # type: ignore
|
||||||
path('robots.txt', static.serve, {'document_root': settings.STATICFILES_DIRS[0], 'path': 'robots.txt'}),
|
path('robots.txt', static.serve, {'document_root': settings.STATICFILES_DIRS[0], 'path': 'robots.txt'}),
|
||||||
path('favicon.ico', static.serve, {'document_root': settings.STATICFILES_DIRS[0], 'path': 'favicon.ico'}),
|
path('favicon.ico', static.serve, {'document_root': settings.STATICFILES_DIRS[0], 'path': 'favicon.ico'}),
|
||||||
|
|
||||||
path('docs/', RedirectView.as_view(url='https://github.com/ArchiveBox/ArchiveBox/wiki'), name='Docs'),
|
|
||||||
|
|
||||||
path('public/', PublicIndexView.as_view(), name='public-index'),
|
|
||||||
|
|
||||||
path('archive/', RedirectView.as_view(url='/')),
|
|
||||||
path('archive/<path:path>', SnapshotView.as_view(), name='Snapshot'),
|
|
||||||
|
|
||||||
path('admin/core/snapshot/add/', RedirectView.as_view(url='/add/')),
|
|
||||||
path('add/', AddView.as_view(), name='add'),
|
|
||||||
|
|
||||||
path('accounts/login/', RedirectView.as_view(url='/admin/login/')),
|
path('accounts/login/', RedirectView.as_view(url='/admin/login/')),
|
||||||
path('accounts/logout/', RedirectView.as_view(url='/admin/logout/')),
|
path('accounts/logout/', RedirectView.as_view(url='/admin/logout/')),
|
||||||
|
|
||||||
|
path('admin/core/snapshot/add/', RedirectView.as_view(url='/add/')),
|
||||||
|
path('docs/', RedirectView.as_view(url='https://github.com/ArchiveBox/ArchiveBox/wiki'), name='Docs'),
|
||||||
|
path('archive/', RedirectView.as_view(url='/')),
|
||||||
|
|
||||||
path('accounts/', include('django.contrib.auth.urls')),
|
path('accounts/', include('django.contrib.auth.urls')),
|
||||||
path('admin/', archivebox_admin.urls),
|
path('admin/', archivebox_admin.urls),
|
||||||
|
|
||||||
path("api/", include('api.urls'), name='api'),
|
path("api/", include('api.urls'), name='api'),
|
||||||
|
|
||||||
path('health/', HealthCheckView.as_view(), name='healthcheck'),
|
path('public/', PublicIndexView.as_view(), name='public-index'),
|
||||||
path('error/', lambda *_: 1/0), # type: ignore
|
|
||||||
|
path('archive/<path:path>', SnapshotView.as_view(), name='Snapshot'),
|
||||||
|
|
||||||
|
path('add/', AddView.as_view(), name='add'),
|
||||||
|
|
||||||
|
path("jobs/", JobsDashboardView.as_view(), name='jobs_dashboard'),
|
||||||
|
|
||||||
|
|
||||||
# path('jet_api/', include('jet_django.urls')), Enable to use https://www.jetadmin.io/integrations/django
|
# path('jet_api/', include('jet_django.urls')), Enable to use https://www.jetadmin.io/integrations/django
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue