diff --git a/Dockerfile b/Dockerfile index 2b3de52..ea95f18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,10 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY app/ ./app/ +COPY utils/ ./utils/ COPY alembic/ ./alembic/ COPY alembic.ini . +COPY static/ ./static/ # Create non-root user RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app diff --git a/app/api/seed.py b/app/api/seed.py new file mode 100644 index 0000000..c8777d6 --- /dev/null +++ b/app/api/seed.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, HTTPException +from app.database import SessionLocal +import asyncio +import sys +import os + +# Add parent directory to path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) + +from utils.seed_data import generate_seed_data as generate_data, clear_all_data + +router = APIRouter(prefix="/api/v1/seed", tags=["seed"]) + + +@router.post("/generate/{count}") +async def generate_seed_data(count: int = 10): + """Generate seed data""" + if count < 1 or count > 100: + raise HTTPException(status_code=400, detail="Count must be between 1 and 100") + + try: + artifact_ids = await generate_data(count) + return { + "message": f"Successfully generated {len(artifact_ids)} artifacts", + "artifact_ids": artifact_ids + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Seed generation failed: {str(e)}") + + +@router.delete("/clear") +async def clear_data(): + """Clear all data""" + try: + await clear_all_data() + return {"message": "All data cleared successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Clear failed: {str(e)}") diff --git a/app/main.py b/app/main.py index 09dc6d7..c5d3770 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,13 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse from app.api.artifacts import router as artifacts_router +from app.api.seed import router as seed_router from app.database import init_db from app.config import settings import logging +import os # Configure logging logging.basicConfig( @@ -33,6 +37,12 @@ app.add_middleware( # Include routers app.include_router(artifacts_router) +app.include_router(seed_router) + +# Mount static files +static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static") +if os.path.exists(static_dir): + app.mount("/static", StaticFiles(directory=static_dir), name="static") @app.on_event("startup") @@ -45,9 +55,9 @@ async def startup_event(): logger.info("Application started successfully") -@app.get("/") -async def root(): - """Root endpoint""" +@app.get("/api") +async def api_root(): + """API root endpoint""" return { "message": "Test Artifact Data Lake API", "version": "1.0.0", @@ -57,6 +67,23 @@ async def root(): } +@app.get("/") +async def ui_root(): + """Serve the UI""" + index_path = os.path.join(static_dir, "index.html") + if os.path.exists(index_path): + return FileResponse(index_path) + else: + return { + "message": "Test Artifact Data Lake API", + "version": "1.0.0", + "docs": "/docs", + "ui": "UI not found. Serving API only.", + "deployment_mode": settings.deployment_mode, + "storage_backend": settings.storage_backend + } + + @app.get("/health") async def health_check(): """Health check endpoint""" diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..2a73e1e --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,436 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1400px; + margin: 0 auto; + background: white; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 30px; + display: flex; + justify-content: space-between; + align-items: center; +} + +header h1 { + font-size: 28px; + font-weight: 600; +} + +.header-info { + display: flex; + gap: 10px; +} + +.badge { + background: rgba(255, 255, 255, 0.2); + padding: 6px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + backdrop-filter: blur(10px); +} + +.tabs { + display: flex; + background: #f7f9fc; + border-bottom: 2px solid #e2e8f0; +} + +.tab-button { + flex: 1; + padding: 16px 24px; + background: none; + border: none; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + color: #64748b; +} + +.tab-button:hover { + background: #e2e8f0; +} + +.tab-button.active { + background: white; + color: #667eea; + border-bottom: 3px solid #667eea; +} + +.tab-content { + display: none; + padding: 30px; +} + +.tab-content.active { + display: block; +} + +.toolbar { + display: flex; + gap: 10px; + margin-bottom: 20px; + align-items: center; +} + +.btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; +} + +.btn-primary { + background: #667eea; + color: white; +} + +.btn-primary:hover { + background: #5568d3; + transform: translateY(-1px); +} + +.btn-secondary { + background: #e2e8f0; + color: #475569; +} + +.btn-secondary:hover { + background: #cbd5e1; +} + +.btn-danger { + background: #ef4444; + color: white; +} + +.btn-danger:hover { + background: #dc2626; +} + +.btn-success { + background: #10b981; + color: white; +} + +.btn-large { + padding: 14px 28px; + font-size: 16px; +} + +.count-badge { + background: #f0f9ff; + color: #0369a1; + padding: 8px 16px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + margin-left: auto; +} + +.table-container { + overflow-x: auto; + border: 1px solid #e2e8f0; + border-radius: 8px; +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +thead { + background: #f7f9fc; +} + +th { + padding: 14px 12px; + text-align: left; + font-weight: 600; + color: #475569; + border-bottom: 2px solid #e2e8f0; + white-space: nowrap; +} + +td { + padding: 12px; + border-bottom: 1px solid #e2e8f0; +} + +tbody tr:hover { + background: #f7f9fc; +} + +.loading { + text-align: center; + color: #94a3b8; + padding: 40px !important; +} + +.result-badge { + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +.result-pass { + background: #d1fae5; + color: #065f46; +} + +.result-fail { + background: #fee2e2; + color: #991b1b; +} + +.result-skip { + background: #fef3c7; + color: #92400e; +} + +.result-error { + background: #fecaca; + color: #7f1d1d; +} + +.tag { + display: inline-block; + background: #e0e7ff; + color: #3730a3; + padding: 3px 8px; + border-radius: 10px; + font-size: 11px; + margin: 2px; +} + +.file-type-badge { + background: #dbeafe; + color: #1e40af; + padding: 4px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + margin-top: 20px; + padding: 20px; +} + +#page-info { + font-weight: 500; + color: #64748b; +} + +.upload-section, .query-section { + max-width: 800px; + margin: 0 auto; +} + +.form-group { + margin-bottom: 20px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +label { + display: block; + font-weight: 500; + color: #475569; + margin-bottom: 6px; + font-size: 14px; +} + +input[type="text"], +input[type="file"], +input[type="datetime-local"], +select, +textarea { + width: 100%; + padding: 10px 14px; + border: 1px solid #e2e8f0; + border-radius: 6px; + font-size: 14px; + font-family: inherit; + transition: border-color 0.3s; +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +small { + color: #94a3b8; + font-size: 12px; + display: block; + margin-top: 4px; +} + +#upload-status { + margin-top: 20px; + padding: 14px; + border-radius: 6px; + display: none; +} + +#upload-status.success { + background: #d1fae5; + color: #065f46; + display: block; +} + +#upload-status.error { + background: #fee2e2; + color: #991b1b; + display: block; +} + +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +.modal.active { + display: flex; + align-items: center; + justify-content: center; +} + +.modal-content { + background: white; + padding: 30px; + border-radius: 12px; + max-width: 700px; + max-height: 80vh; + overflow-y: auto; + position: relative; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); +} + +.close { + position: absolute; + right: 20px; + top: 20px; + font-size: 28px; + font-weight: bold; + color: #94a3b8; + cursor: pointer; + transition: color 0.3s; +} + +.close:hover { + color: #475569; +} + +.detail-row { + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid #e2e8f0; +} + +.detail-row:last-child { + border-bottom: none; +} + +.detail-label { + font-weight: 600; + color: #475569; + margin-bottom: 4px; +} + +.detail-value { + color: #64748b; +} + +pre { + background: #f7f9fc; + padding: 12px; + border-radius: 6px; + overflow-x: auto; + font-size: 12px; +} + +.action-buttons { + display: flex; + gap: 8px; +} + +.icon-btn { + background: none; + border: none; + cursor: pointer; + font-size: 18px; + padding: 6px; + border-radius: 4px; + transition: background 0.3s; +} + +.icon-btn:hover { + background: #e2e8f0; +} + +@media (max-width: 768px) { + .form-row { + grid-template-columns: 1fr; + } + + header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .table-container { + font-size: 12px; + } + + th, td { + padding: 8px 6px; + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..4fcd19c --- /dev/null +++ b/static/index.html @@ -0,0 +1,208 @@ + + + + + + Test Artifact Data Lake + + + +
+
+

🗄️ Test Artifact Data Lake

+
+ + +
+
+ + + + +
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + +
IDFilenameTypeSizeTest NameSuiteResultTagsCreatedActions
Loading artifacts...
+
+ + +
+ + +
+
+

Upload Artifact

+
+
+ + + Supported: CSV, JSON, binary files, PCAP +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+ + +
+
+

Query Artifacts

+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ + + +
+ + + + diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 0000000..c7ff00a --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,462 @@ +// 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 = ` + + Error loading artifacts: ${error.message} + + `; + } +} + +// Display artifacts in table +function displayArtifacts(artifacts) { + const tbody = document.getElementById('artifacts-tbody'); + + if (artifacts.length === 0) { + tbody.innerHTML = 'No artifacts found. Upload some files to get started!'; + document.getElementById('artifact-count').textContent = '0 artifacts'; + return; + } + + tbody.innerHTML = artifacts.map(artifact => ` + + ${artifact.id} + + + ${escapeHtml(artifact.filename)} + + + ${artifact.file_type} + ${formatBytes(artifact.file_size)} + ${artifact.test_name || '-'} + ${artifact.test_suite || '-'} + ${formatResult(artifact.test_result)} + ${formatTags(artifact.tags)} + ${formatDate(artifact.created_at)} + +
+ + +
+ + + `).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 `${result}`; +} + +// Format tags +function formatTags(tags) { + if (!tags || tags.length === 0) return '-'; + return tags.map(tag => `${escapeHtml(tag)}`).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 = ` +
+
ID
+
${artifact.id}
+
+
+
Filename
+
${escapeHtml(artifact.filename)}
+
+
+
File Type
+
${artifact.file_type}
+
+
+
Size
+
${formatBytes(artifact.file_size)}
+
+
+
Storage Path
+
${artifact.storage_path}
+
+
+
Test Name
+
${artifact.test_name || '-'}
+
+
+
Test Suite
+
${artifact.test_suite || '-'}
+
+
+
Test Result
+
${formatResult(artifact.test_result)}
+
+ ${artifact.test_config ? ` +
+
Test Config
+
${JSON.stringify(artifact.test_config, null, 2)}
+
+ ` : ''} + ${artifact.custom_metadata ? ` +
+
Custom Metadata
+
${JSON.stringify(artifact.custom_metadata, null, 2)}
+
+ ` : ''} + ${artifact.description ? ` +
+
Description
+
${escapeHtml(artifact.description)}
+
+ ` : ''} + ${artifact.tags && artifact.tags.length > 0 ? ` +
+
Tags
+
${formatTags(artifact.tags)}
+
+ ` : ''} +
+
Version
+
${artifact.version || '-'}
+
+
+
Created
+
${formatDate(artifact.created_at)}
+
+
+
Updated
+
${formatDate(artifact.updated_at)}
+
+
+ + +
+ `; + + 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); +}