Add web UI with artifact table and upload functionality
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>
This commit is contained in:
@@ -14,8 +14,10 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
# Copy application code
|
# Copy application code
|
||||||
COPY app/ ./app/
|
COPY app/ ./app/
|
||||||
|
COPY utils/ ./utils/
|
||||||
COPY alembic/ ./alembic/
|
COPY alembic/ ./alembic/
|
||||||
COPY alembic.ini .
|
COPY alembic.ini .
|
||||||
|
COPY static/ ./static/
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||||
|
|||||||
38
app/api/seed.py
Normal file
38
app/api/seed.py
Normal file
@@ -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)}")
|
||||||
33
app/main.py
33
app/main.py
@@ -1,9 +1,13 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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.artifacts import router as artifacts_router
|
||||||
|
from app.api.seed import router as seed_router
|
||||||
from app.database import init_db
|
from app.database import init_db
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -33,6 +37,12 @@ app.add_middleware(
|
|||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(artifacts_router)
|
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")
|
@app.on_event("startup")
|
||||||
@@ -45,9 +55,9 @@ async def startup_event():
|
|||||||
logger.info("Application started successfully")
|
logger.info("Application started successfully")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/api")
|
||||||
async def root():
|
async def api_root():
|
||||||
"""Root endpoint"""
|
"""API root endpoint"""
|
||||||
return {
|
return {
|
||||||
"message": "Test Artifact Data Lake API",
|
"message": "Test Artifact Data Lake API",
|
||||||
"version": "1.0.0",
|
"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")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Health check endpoint"""
|
"""Health check endpoint"""
|
||||||
|
|||||||
436
static/css/styles.css
Normal file
436
static/css/styles.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
208
static/index.html
Normal file
208
static/index.html
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Test Artifact Data Lake</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>🗄️ Test Artifact Data Lake</h1>
|
||||||
|
<div class="header-info">
|
||||||
|
<span id="deployment-mode" class="badge"></span>
|
||||||
|
<span id="storage-backend" class="badge"></span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="tabs">
|
||||||
|
<button class="tab-button active" onclick="showTab('artifacts')">📋 Artifacts</button>
|
||||||
|
<button class="tab-button" onclick="showTab('upload')">⬆️ Upload</button>
|
||||||
|
<button class="tab-button" onclick="showTab('query')">🔍 Query</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Artifacts Tab -->
|
||||||
|
<div id="artifacts-tab" class="tab-content active">
|
||||||
|
<div class="toolbar">
|
||||||
|
<button onclick="loadArtifacts()" class="btn btn-primary">🔄 Refresh</button>
|
||||||
|
<button onclick="generateSeedData()" class="btn btn-secondary">🌱 Generate Seed Data</button>
|
||||||
|
<span id="artifact-count" class="count-badge"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table id="artifacts-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Filename</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Test Name</th>
|
||||||
|
<th>Suite</th>
|
||||||
|
<th>Result</th>
|
||||||
|
<th>Tags</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="artifacts-tbody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="loading">Loading artifacts...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<button onclick="previousPage()" id="prev-btn" class="btn">← Previous</button>
|
||||||
|
<span id="page-info">Page 1</span>
|
||||||
|
<button onclick="nextPage()" id="next-btn" class="btn">Next →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Tab -->
|
||||||
|
<div id="upload-tab" class="tab-content">
|
||||||
|
<div class="upload-section">
|
||||||
|
<h2>Upload Artifact</h2>
|
||||||
|
<form id="upload-form" onsubmit="uploadArtifact(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="file">File *</label>
|
||||||
|
<input type="file" id="file" name="file" required>
|
||||||
|
<small>Supported: CSV, JSON, binary files, PCAP</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="test-name">Test Name</label>
|
||||||
|
<input type="text" id="test-name" name="test_name" placeholder="e.g., login_test">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="test-suite">Test Suite</label>
|
||||||
|
<input type="text" id="test-suite" name="test_suite" placeholder="e.g., integration">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="test-result">Test Result</label>
|
||||||
|
<select id="test-result" name="test_result">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
<option value="pass">Pass</option>
|
||||||
|
<option value="fail">Fail</option>
|
||||||
|
<option value="skip">Skip</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="version">Version</label>
|
||||||
|
<input type="text" id="version" name="version" placeholder="e.g., v1.0.0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tags">Tags (comma-separated)</label>
|
||||||
|
<input type="text" id="tags" name="tags" placeholder="e.g., regression, smoke, critical">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" rows="3" placeholder="Describe this artifact..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="test-config">Test Config (JSON)</label>
|
||||||
|
<textarea id="test-config" name="test_config" rows="4" placeholder='{"browser": "chrome", "timeout": 30}'></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="custom-metadata">Custom Metadata (JSON)</label>
|
||||||
|
<textarea id="custom-metadata" name="custom_metadata" rows="4" placeholder='{"build": "1234", "commit": "abc123"}'></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-large">📤 Upload Artifact</button>
|
||||||
|
</form>
|
||||||
|
<div id="upload-status"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Query Tab -->
|
||||||
|
<div id="query-tab" class="tab-content">
|
||||||
|
<div class="query-section">
|
||||||
|
<h2>Query Artifacts</h2>
|
||||||
|
<form id="query-form" onsubmit="queryArtifacts(event)">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="q-filename">Filename</label>
|
||||||
|
<input type="text" id="q-filename" placeholder="Search filename...">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="q-type">File Type</label>
|
||||||
|
<select id="q-type">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="csv">CSV</option>
|
||||||
|
<option value="json">JSON</option>
|
||||||
|
<option value="binary">Binary</option>
|
||||||
|
<option value="pcap">PCAP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="q-test-name">Test Name</label>
|
||||||
|
<input type="text" id="q-test-name" placeholder="Search test name...">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="q-suite">Test Suite</label>
|
||||||
|
<input type="text" id="q-suite" placeholder="e.g., integration">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="q-result">Test Result</label>
|
||||||
|
<select id="q-result">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="pass">Pass</option>
|
||||||
|
<option value="fail">Fail</option>
|
||||||
|
<option value="skip">Skip</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="q-tags">Tags (comma-separated)</label>
|
||||||
|
<input type="text" id="q-tags" placeholder="e.g., regression, smoke">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="q-start-date">Start Date</label>
|
||||||
|
<input type="datetime-local" id="q-start-date">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="q-end-date">End Date</label>
|
||||||
|
<input type="datetime-local" id="q-end-date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-large">🔍 Search</button>
|
||||||
|
<button type="button" onclick="clearQuery()" class="btn btn-secondary">Clear</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Artifact Detail Modal -->
|
||||||
|
<div id="detail-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeDetailModal()">×</span>
|
||||||
|
<h2>Artifact Details</h2>
|
||||||
|
<div id="detail-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
462
static/js/app.js
Normal file
462
static/js/app.js
Normal file
@@ -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 = `
|
||||||
|
<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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user