feat: enhance job statistics display with interactive filtering and improved styling
This commit is contained in:
parent
57963274e1
commit
2e1c33c182
@ -60,6 +60,23 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jobs-stat {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.jobs-stat:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.jobs-stat.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-glow), var(--shadow);
|
||||||
|
}
|
||||||
|
.jobs-stat.active .jobs-stat-lbl {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -80,27 +97,27 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<!-- Summary chips -->
|
<!-- Summary chips -->
|
||||||
<div class="jobs-summary">
|
<div class="jobs-summary">
|
||||||
<div class="jobs-stat">
|
<div class="jobs-stat active" data-filter="all">
|
||||||
<div class="jobs-stat-val">{{ jobs|length }}</div>
|
<div class="jobs-stat-val">{{ jobs|length }}</div>
|
||||||
<div class="jobs-stat-lbl">Total</div>
|
<div class="jobs-stat-lbl">Total</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jobs-stat">
|
<div class="jobs-stat" data-filter="running">
|
||||||
<div class="jobs-stat-val" style="color:var(--accent)">{{ jobs|selectattr('status','equalto','running')|list|length }}</div>
|
<div class="jobs-stat-val" style="color:var(--accent)">{{ jobs|selectattr('status','equalto','running')|list|length }}</div>
|
||||||
<div class="jobs-stat-lbl">Running</div>
|
<div class="jobs-stat-lbl">Running</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jobs-stat">
|
<div class="jobs-stat" data-filter="finished">
|
||||||
<div class="jobs-stat-val" style="color:var(--success)">{{ jobs|selectattr('status','equalto','finished')|list|length }}</div>
|
<div class="jobs-stat-val" style="color:var(--success)">{{ jobs|selectattr('status','equalto','finished')|list|length }}</div>
|
||||||
<div class="jobs-stat-lbl">Finished</div>
|
<div class="jobs-stat-lbl">Finished</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jobs-stat">
|
<div class="jobs-stat" data-filter="queued">
|
||||||
<div class="jobs-stat-val" style="color:var(--warning)">{{ jobs|selectattr('status','equalto','queued')|list|length }}</div>
|
<div class="jobs-stat-val" style="color:var(--warning)">{{ jobs|selectattr('status','equalto','queued')|list|length }}</div>
|
||||||
<div class="jobs-stat-lbl">Queued</div>
|
<div class="jobs-stat-lbl">Queued</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jobs-stat">
|
<div class="jobs-stat" data-filter="failed">
|
||||||
<div class="jobs-stat-val" style="color:var(--danger)">{% set fcnt = namespace(n=0) %}{% for j in jobs %}{% if j.status.startswith('failed') %}{% set fcnt.n = fcnt.n + 1 %}{% endif %}{% endfor %}{{ fcnt.n }}</div>
|
<div class="jobs-stat-val" style="color:var(--danger)">{% set fcnt = namespace(n=0) %}{% for j in jobs %}{% if j.status.startswith('failed') %}{% set fcnt.n = fcnt.n + 1 %}{% endif %}{% endfor %}{{ fcnt.n }}</div>
|
||||||
<div class="jobs-stat-lbl">Failed</div>
|
<div class="jobs-stat-lbl">Failed</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jobs-stat">
|
<div class="jobs-stat" data-filter="scheduled">
|
||||||
<div class="jobs-stat-val" style="color:var(--accent-2)">{{ scheduled_count }}</div>
|
<div class="jobs-stat-val" style="color:var(--accent-2)">{{ scheduled_count }}</div>
|
||||||
<div class="jobs-stat-lbl">Scheduled</div>
|
<div class="jobs-stat-lbl">Scheduled</div>
|
||||||
</div>
|
</div>
|
||||||
@ -121,7 +138,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<tr>
|
<tr data-status="{{ job.status }}" data-scheduled="{{ '1' if job.schedule_id else '0' }}">
|
||||||
<td>
|
<td>
|
||||||
<div style="font-weight:600;font-size:13px;">
|
<div style="font-weight:600;font-size:13px;">
|
||||||
{{ job.label or ('Job #' + job.id[:8]) }}
|
{{ job.label or ('Job #' + job.id[:8]) }}
|
||||||
@ -239,5 +256,57 @@
|
|||||||
if (hasRunning) {
|
if (hasRunning) {
|
||||||
setTimeout(() => location.reload(), 8000);
|
setTimeout(() => location.reload(), 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
const statCards = document.querySelectorAll('.jobs-stat[data-filter]');
|
||||||
|
const tableRows = document.querySelectorAll('tbody tr[data-status]');
|
||||||
|
let activeFilter = localStorage.getItem('jobsFilter') || 'all';
|
||||||
|
|
||||||
|
function applyFilter(filter) {
|
||||||
|
activeFilter = filter;
|
||||||
|
localStorage.setItem('jobsFilter', filter);
|
||||||
|
statCards.forEach(card => {
|
||||||
|
card.classList.toggle('active', card.dataset.filter === filter);
|
||||||
|
});
|
||||||
|
let visibleCount = 0;
|
||||||
|
tableRows.forEach(row => {
|
||||||
|
let show = false;
|
||||||
|
if (filter === 'all') {
|
||||||
|
show = true;
|
||||||
|
} else if (filter === 'failed') {
|
||||||
|
show = row.dataset.status.startsWith('failed');
|
||||||
|
} else if (filter === 'scheduled') {
|
||||||
|
show = row.dataset.scheduled === '1';
|
||||||
|
} else {
|
||||||
|
show = row.dataset.status === filter;
|
||||||
|
}
|
||||||
|
row.style.display = show ? '' : 'none';
|
||||||
|
if (show) visibleCount++;
|
||||||
|
});
|
||||||
|
// Show "no matches" message if filter yields zero results
|
||||||
|
let noMatch = document.getElementById('no-filter-match');
|
||||||
|
if (!noMatch && visibleCount === 0) {
|
||||||
|
const tbody = document.querySelector('tbody');
|
||||||
|
noMatch = document.createElement('tr');
|
||||||
|
noMatch.id = 'no-filter-match';
|
||||||
|
noMatch.innerHTML = '<td colspan="6" style="text-align:center;padding:32px;color:var(--text-muted);">No jobs match this filter.</td>';
|
||||||
|
tbody.appendChild(noMatch);
|
||||||
|
} else if (noMatch && visibleCount > 0) {
|
||||||
|
noMatch.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statCards.forEach(card => {
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
const filter = card.dataset.filter;
|
||||||
|
// Toggle off if clicking the already-active filter
|
||||||
|
applyFilter(filter === activeFilter ? 'all' : filter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply persisted filter on load
|
||||||
|
if (activeFilter !== 'all') {
|
||||||
|
applyFilter(activeFilter);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user