- Overhaul CSS with comprehensive design tokens (shadows, transitions, radii) - Add hover effects and smooth transitions to cards, buttons, tables - Improve typography hierarchy and color consistency - Remove inline styles from 10 template files for better maintainability - Add global notification container to ensure toasts appear above modals - Update showNotification/showAlert functions to use centralized container - Add accessibility improvements (focus states, reduced motion support) - Improve responsive design and mobile styling - Add print styles
251 lines
8.9 KiB
HTML
251 lines
8.9 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Webhooks - SneakyScanner{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mt-4">
|
|
<div class="col-12 d-flex justify-content-between align-items-center mb-4">
|
|
<h1>Webhook Management</h1>
|
|
<a href="{{ url_for('webhooks.new_webhook') }}" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle"></i> Add Webhook
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading indicator -->
|
|
<div id="loading" class="text-center my-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Webhooks table -->
|
|
<div id="webhooks-container" style="display: none;">
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>URL</th>
|
|
<th>Alert Types</th>
|
|
<th>Severity Filter</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="webhooks-tbody">
|
|
<!-- Populated via JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<nav aria-label="Webhooks pagination" id="pagination-container">
|
|
<ul class="pagination justify-content-center" id="pagination">
|
|
<!-- Populated via JavaScript -->
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div id="empty-state" class="text-center my-5" style="display: none;">
|
|
<i class="bi bi-webhook" style="font-size: 4rem; color: #94a3b8;"></i>
|
|
<p class="text-muted mt-3">No webhooks configured yet.</p>
|
|
<a href="{{ url_for('webhooks.new_webhook') }}" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle"></i> Create Your First Webhook
|
|
</a>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let currentPage = 1;
|
|
const perPage = 20;
|
|
|
|
async function loadWebhooks(page = 1) {
|
|
try {
|
|
const response = await fetch(`/api/webhooks?page=${page}&per_page=${perPage}`);
|
|
const data = await response.json();
|
|
|
|
if (data.webhooks && data.webhooks.length > 0) {
|
|
renderWebhooks(data.webhooks);
|
|
renderPagination(data.page, data.pages, data.total);
|
|
document.getElementById('webhooks-container').style.display = 'block';
|
|
document.getElementById('empty-state').style.display = 'none';
|
|
} else {
|
|
document.getElementById('webhooks-container').style.display = 'none';
|
|
document.getElementById('empty-state').style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading webhooks:', error);
|
|
alert('Failed to load webhooks');
|
|
} finally {
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function renderWebhooks(webhooks) {
|
|
const tbody = document.getElementById('webhooks-tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
webhooks.forEach(webhook => {
|
|
const row = document.createElement('tr');
|
|
|
|
// Truncate URL for display
|
|
const truncatedUrl = webhook.url.length > 50 ?
|
|
webhook.url.substring(0, 47) + '...' : webhook.url;
|
|
|
|
// Format alert types
|
|
const alertTypes = webhook.alert_types && webhook.alert_types.length > 0 ?
|
|
webhook.alert_types.map(t => `<span class="badge bg-secondary me-1">${t}</span>`).join('') :
|
|
'<span class="text-muted">All</span>';
|
|
|
|
// Format severity filter
|
|
const severityFilter = webhook.severity_filter && webhook.severity_filter.length > 0 ?
|
|
webhook.severity_filter.map(s => `<span class="badge bg-${getSeverityColor(s)} me-1">${s}</span>`).join('') :
|
|
'<span class="text-muted">All</span>';
|
|
|
|
// Status badge
|
|
const statusBadge = webhook.enabled ?
|
|
'<span class="badge bg-success">Enabled</span>' :
|
|
'<span class="badge bg-secondary">Disabled</span>';
|
|
|
|
row.innerHTML = `
|
|
<td><strong>${escapeHtml(webhook.name)}</strong></td>
|
|
<td><code class="small">${escapeHtml(truncatedUrl)}</code></td>
|
|
<td>${alertTypes}</td>
|
|
<td>${severityFilter}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button class="btn btn-outline-primary" onclick="testWebhook(${webhook.id})" title="Test">
|
|
<i class="bi bi-send"></i>
|
|
</button>
|
|
<a href="/webhooks/${webhook.id}/edit" class="btn btn-outline-primary" title="Edit">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<a href="/webhooks/${webhook.id}/logs" class="btn btn-outline-info" title="Logs">
|
|
<i class="bi bi-list-ul"></i>
|
|
</a>
|
|
<button class="btn btn-outline-danger" onclick="deleteWebhook(${webhook.id}, '${escapeHtml(webhook.name)}')" title="Delete">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function renderPagination(currentPage, totalPages, totalItems) {
|
|
const pagination = document.getElementById('pagination');
|
|
pagination.innerHTML = '';
|
|
|
|
if (totalPages <= 1) {
|
|
document.getElementById('pagination-container').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('pagination-container').style.display = 'block';
|
|
|
|
// Previous button
|
|
const prevLi = document.createElement('li');
|
|
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
|
prevLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage - 1}); return false;">Previous</a>`;
|
|
pagination.appendChild(prevLi);
|
|
|
|
// Page numbers
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
|
|
const li = document.createElement('li');
|
|
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
|
|
li.innerHTML = `<a class="page-link" href="#" onclick="changePage(${i}); return false;">${i}</a>`;
|
|
pagination.appendChild(li);
|
|
} else if (i === currentPage - 3 || i === currentPage + 3) {
|
|
const li = document.createElement('li');
|
|
li.className = 'page-item disabled';
|
|
li.innerHTML = '<a class="page-link" href="#">...</a>';
|
|
pagination.appendChild(li);
|
|
}
|
|
}
|
|
|
|
// Next button
|
|
const nextLi = document.createElement('li');
|
|
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
|
nextLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage + 1}); return false;">Next</a>`;
|
|
pagination.appendChild(nextLi);
|
|
}
|
|
|
|
function changePage(page) {
|
|
currentPage = page;
|
|
loadWebhooks(page);
|
|
}
|
|
|
|
async function testWebhook(id) {
|
|
if (!confirm('Send a test payload to this webhook?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/webhooks/${id}/test`, { method: 'POST' });
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
alert(`Test successful!\nHTTP ${result.status_code}\n${result.message}`);
|
|
} else {
|
|
alert(`Test failed:\n${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error testing webhook:', error);
|
|
alert('Failed to test webhook');
|
|
}
|
|
}
|
|
|
|
async function deleteWebhook(id, name) {
|
|
if (!confirm(`Are you sure you want to delete webhook "${name}"?`)) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/webhooks/${id}`, { method: 'DELETE' });
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
alert('Webhook deleted successfully');
|
|
loadWebhooks(currentPage);
|
|
} else {
|
|
alert(`Failed to delete webhook: ${result.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting webhook:', error);
|
|
alert('Failed to delete webhook');
|
|
}
|
|
}
|
|
|
|
function getSeverityColor(severity) {
|
|
const colors = {
|
|
'critical': 'danger',
|
|
'warning': 'warning',
|
|
'info': 'info'
|
|
};
|
|
return colors[severity] || 'secondary';
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Load webhooks on page load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadWebhooks(1);
|
|
});
|
|
</script>
|
|
{% endblock %}
|