Features: - Modern web interface with table view of all artifacts - Display all metadata: filename, type, size, test info, tags - Upload form with full metadata support - Query/filter interface - Detail modal for viewing full artifact information - Download and delete actions - Integrated seed data generation via UI - Responsive design with gradient theme Technical: - Pure HTML/CSS/JavaScript (no frameworks) - FastAPI serves static files - Seed data API endpoint for easy testing - Pagination support - Real-time deployment mode display The UI is now accessible at http://localhost:8000/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
463 lines
15 KiB
JavaScript
463 lines
15 KiB
JavaScript
// API Base URL
|
|
const API_BASE = '/api/v1';
|
|
|
|
// Pagination
|
|
let currentPage = 1;
|
|
let pageSize = 25;
|
|
let totalArtifacts = 0;
|
|
|
|
// Load API info on page load
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
loadApiInfo();
|
|
loadArtifacts();
|
|
});
|
|
|
|
// Load API information
|
|
async function loadApiInfo() {
|
|
try {
|
|
const response = await fetch('/api');
|
|
const data = await response.json();
|
|
|
|
document.getElementById('deployment-mode').textContent = `Mode: ${data.deployment_mode}`;
|
|
document.getElementById('storage-backend').textContent = `Storage: ${data.storage_backend}`;
|
|
} catch (error) {
|
|
console.error('Error loading API info:', error);
|
|
}
|
|
}
|
|
|
|
// Load artifacts
|
|
async function loadArtifacts(limit = pageSize, offset = 0) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/artifacts/?limit=${limit}&offset=${offset}`);
|
|
const artifacts = await response.json();
|
|
|
|
displayArtifacts(artifacts);
|
|
updatePagination(artifacts.length);
|
|
} catch (error) {
|
|
console.error('Error loading artifacts:', error);
|
|
document.getElementById('artifacts-tbody').innerHTML = `
|
|
<tr><td colspan="10" class="loading" style="color: #ef4444;">
|
|
Error loading artifacts: ${error.message}
|
|
</td></tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Display artifacts in table
|
|
function displayArtifacts(artifacts) {
|
|
const tbody = document.getElementById('artifacts-tbody');
|
|
|
|
if (artifacts.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="10" class="loading">No artifacts found. Upload some files to get started!</td></tr>';
|
|
document.getElementById('artifact-count').textContent = '0 artifacts';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = artifacts.map(artifact => `
|
|
<tr>
|
|
<td><strong>${artifact.id}</strong></td>
|
|
<td>
|
|
<a href="#" onclick="showDetail(${artifact.id}); return false;" style="color: #667eea; text-decoration: none;">
|
|
${escapeHtml(artifact.filename)}
|
|
</a>
|
|
</td>
|
|
<td><span class="file-type-badge">${artifact.file_type}</span></td>
|
|
<td>${formatBytes(artifact.file_size)}</td>
|
|
<td>${artifact.test_name || '-'}</td>
|
|
<td>${artifact.test_suite || '-'}</td>
|
|
<td>${formatResult(artifact.test_result)}</td>
|
|
<td>${formatTags(artifact.tags)}</td>
|
|
<td><small>${formatDate(artifact.created_at)}</small></td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="icon-btn" onclick="downloadArtifact(${artifact.id}, '${escapeHtml(artifact.filename)}')" title="Download">
|
|
💾
|
|
</button>
|
|
<button class="icon-btn" onclick="deleteArtifact(${artifact.id})" title="Delete">
|
|
🗑️
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
document.getElementById('artifact-count').textContent = `${artifacts.length} artifacts`;
|
|
}
|
|
|
|
// Format result badge
|
|
function formatResult(result) {
|
|
if (!result) return '-';
|
|
const className = `result-badge result-${result}`;
|
|
return `<span class="${className}">${result}</span>`;
|
|
}
|
|
|
|
// Format tags
|
|
function formatTags(tags) {
|
|
if (!tags || tags.length === 0) return '-';
|
|
return tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join(' ');
|
|
}
|
|
|
|
// Format bytes
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
// Format date
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
// Escape HTML
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Show artifact detail
|
|
async function showDetail(id) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/artifacts/${id}`);
|
|
const artifact = await response.json();
|
|
|
|
const detailContent = document.getElementById('detail-content');
|
|
detailContent.innerHTML = `
|
|
<div class="detail-row">
|
|
<div class="detail-label">ID</div>
|
|
<div class="detail-value">${artifact.id}</div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">Filename</div>
|
|
<div class="detail-value">${escapeHtml(artifact.filename)}</div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">File Type</div>
|
|
<div class="detail-value"><span class="file-type-badge">${artifact.file_type}</span></div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">Size</div>
|
|
<div class="detail-value">${formatBytes(artifact.file_size)}</div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">Storage Path</div>
|
|
<div class="detail-value"><code>${artifact.storage_path}</code></div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">Test Name</div>
|
|
<div class="detail-value">${artifact.test_name || '-'}</div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">Test Suite</div>
|
|
<div class="detail-value">${artifact.test_suite || '-'}</div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">Test Result</div>
|
|
<div class="detail-value">${formatResult(artifact.test_result)}</div>
|
|
</div>
|
|
${artifact.test_config ? `
|
|
<div class="detail-row">
|
|
<div class="detail-label">Test Config</div>
|
|
<div class="detail-value"><pre>${JSON.stringify(artifact.test_config, null, 2)}</pre></div>
|
|
</div>
|
|
` : ''}
|
|
${artifact.custom_metadata ? `
|
|
<div class="detail-row">
|
|
<div class="detail-label">Custom Metadata</div>
|
|
<div class="detail-value"><pre>${JSON.stringify(artifact.custom_metadata, null, 2)}</pre></div>
|
|
</div>
|
|
` : ''}
|
|
${artifact.description ? `
|
|
<div class="detail-row">
|
|
<div class="detail-label">Description</div>
|
|
<div class="detail-value">${escapeHtml(artifact.description)}</div>
|
|
</div>
|
|
` : ''}
|
|
${artifact.tags && artifact.tags.length > 0 ? `
|
|
<div class="detail-row">
|
|
<div class="detail-label">Tags</div>
|
|
<div class="detail-value">${formatTags(artifact.tags)}</div>
|
|
</div>
|
|
` : ''}
|
|
<div class="detail-row">
|
|
<div class="detail-label">Version</div>
|
|
<div class="detail-value">${artifact.version || '-'}</div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">Created</div>
|
|
<div class="detail-value">${formatDate(artifact.created_at)}</div>
|
|
</div>
|
|
<div class="detail-row">
|
|
<div class="detail-label">Updated</div>
|
|
<div class="detail-value">${formatDate(artifact.updated_at)}</div>
|
|
</div>
|
|
<div style="margin-top: 20px; display: flex; gap: 10px;">
|
|
<button onclick="downloadArtifact(${artifact.id}, '${escapeHtml(artifact.filename)}')" class="btn btn-primary">
|
|
💾 Download
|
|
</button>
|
|
<button onclick="deleteArtifact(${artifact.id}); closeDetailModal();" class="btn btn-danger">
|
|
🗑️ Delete
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('detail-modal').classList.add('active');
|
|
} catch (error) {
|
|
alert('Error loading artifact details: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Close detail modal
|
|
function closeDetailModal() {
|
|
document.getElementById('detail-modal').classList.remove('active');
|
|
}
|
|
|
|
// Download artifact
|
|
async function downloadArtifact(id, filename) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/artifacts/${id}/download`);
|
|
const blob = await response.blob();
|
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
} catch (error) {
|
|
alert('Error downloading artifact: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Delete artifact
|
|
async function deleteArtifact(id) {
|
|
if (!confirm('Are you sure you want to delete this artifact? This cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/artifacts/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
loadArtifacts((currentPage - 1) * pageSize, pageSize);
|
|
alert('Artifact deleted successfully');
|
|
} else {
|
|
throw new Error('Failed to delete artifact');
|
|
}
|
|
} catch (error) {
|
|
alert('Error deleting artifact: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Upload artifact
|
|
async function uploadArtifact(event) {
|
|
event.preventDefault();
|
|
|
|
const form = event.target;
|
|
const formData = new FormData();
|
|
|
|
// Add file
|
|
const fileInput = document.getElementById('file');
|
|
formData.append('file', fileInput.files[0]);
|
|
|
|
// Add optional fields
|
|
const fields = ['test_name', 'test_suite', 'test_result', 'version', 'description'];
|
|
fields.forEach(field => {
|
|
const value = form.elements[field].value;
|
|
if (value) formData.append(field, value);
|
|
});
|
|
|
|
// Add tags (convert comma-separated to JSON array)
|
|
const tags = document.getElementById('tags').value;
|
|
if (tags) {
|
|
const tagsArray = tags.split(',').map(t => t.trim()).filter(t => t);
|
|
formData.append('tags', JSON.stringify(tagsArray));
|
|
}
|
|
|
|
// Add JSON fields
|
|
const testConfig = document.getElementById('test-config').value;
|
|
if (testConfig) {
|
|
try {
|
|
JSON.parse(testConfig); // Validate
|
|
formData.append('test_config', testConfig);
|
|
} catch (e) {
|
|
showUploadStatus('Invalid Test Config JSON', false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const customMetadata = document.getElementById('custom-metadata').value;
|
|
if (customMetadata) {
|
|
try {
|
|
JSON.parse(customMetadata); // Validate
|
|
formData.append('custom_metadata', customMetadata);
|
|
} catch (e) {
|
|
showUploadStatus('Invalid Custom Metadata JSON', false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/artifacts/upload`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
const artifact = await response.json();
|
|
showUploadStatus(`Successfully uploaded: ${artifact.filename}`, true);
|
|
form.reset();
|
|
loadArtifacts();
|
|
} else {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Upload failed');
|
|
}
|
|
} catch (error) {
|
|
showUploadStatus('Upload failed: ' + error.message, false);
|
|
}
|
|
}
|
|
|
|
// Show upload status
|
|
function showUploadStatus(message, success) {
|
|
const status = document.getElementById('upload-status');
|
|
status.textContent = message;
|
|
status.className = success ? 'success' : 'error';
|
|
|
|
setTimeout(() => {
|
|
status.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
|
|
// Query artifacts
|
|
async function queryArtifacts(event) {
|
|
event.preventDefault();
|
|
|
|
const query = {};
|
|
|
|
const filename = document.getElementById('q-filename').value;
|
|
if (filename) query.filename = filename;
|
|
|
|
const fileType = document.getElementById('q-type').value;
|
|
if (fileType) query.file_type = fileType;
|
|
|
|
const testName = document.getElementById('q-test-name').value;
|
|
if (testName) query.test_name = testName;
|
|
|
|
const suite = document.getElementById('q-suite').value;
|
|
if (suite) query.test_suite = suite;
|
|
|
|
const result = document.getElementById('q-result').value;
|
|
if (result) query.test_result = result;
|
|
|
|
const tags = document.getElementById('q-tags').value;
|
|
if (tags) {
|
|
query.tags = tags.split(',').map(t => t.trim()).filter(t => t);
|
|
}
|
|
|
|
const startDate = document.getElementById('q-start-date').value;
|
|
if (startDate) query.start_date = new Date(startDate).toISOString();
|
|
|
|
const endDate = document.getElementById('q-end-date').value;
|
|
if (endDate) query.end_date = new Date(endDate).toISOString();
|
|
|
|
query.limit = 100;
|
|
query.offset = 0;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/artifacts/query`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(query)
|
|
});
|
|
|
|
const artifacts = await response.json();
|
|
|
|
// Switch to artifacts tab and display results
|
|
showTab('artifacts');
|
|
displayArtifacts(artifacts);
|
|
} catch (error) {
|
|
alert('Query failed: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Clear query form
|
|
function clearQuery() {
|
|
document.getElementById('query-form').reset();
|
|
}
|
|
|
|
// Generate seed data
|
|
async function generateSeedData() {
|
|
const count = prompt('How many artifacts to generate? (1-100)', '10');
|
|
if (!count) return;
|
|
|
|
const num = parseInt(count);
|
|
if (isNaN(num) || num < 1 || num > 100) {
|
|
alert('Please enter a number between 1 and 100');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/seed/generate/${num}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
alert(result.message);
|
|
loadArtifacts();
|
|
} else {
|
|
throw new Error(result.detail || 'Generation failed');
|
|
}
|
|
} catch (error) {
|
|
alert('Error generating seed data: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Tab navigation
|
|
function showTab(tabName) {
|
|
// Hide all tabs
|
|
document.querySelectorAll('.tab-content').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.tab-button').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
// Show selected tab
|
|
document.getElementById(tabName + '-tab').classList.add('active');
|
|
event.target.classList.add('active');
|
|
}
|
|
|
|
// Pagination
|
|
function updatePagination(count) {
|
|
const pageInfo = document.getElementById('page-info');
|
|
pageInfo.textContent = `Page ${currentPage}`;
|
|
|
|
document.getElementById('prev-btn').disabled = currentPage === 1;
|
|
document.getElementById('next-btn').disabled = count < pageSize;
|
|
}
|
|
|
|
function previousPage() {
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
loadArtifacts(pageSize, (currentPage - 1) * pageSize);
|
|
}
|
|
}
|
|
|
|
function nextPage() {
|
|
currentPage++;
|
|
loadArtifacts(pageSize, (currentPage - 1) * pageSize);
|
|
}
|