Update gitignore, combined docker for frotnend and api

This commit is contained in:
pratik
2025-10-16 13:38:11 -05:00
parent 2584e92af2
commit 122e3f2edc
16 changed files with 18 additions and 1533 deletions

3
.gitignore vendored
View File

@@ -90,3 +90,6 @@ temp/
# Node.js # Node.js
package-lock.json package-lock.json
**/package-lock.json **/package-lock.json
# Built static files (generated during Docker build from Angular)
static/

View File

@@ -48,11 +48,6 @@ A lightweight, cloud-native API for storing and querying test artifacts includin
.\quickstart.ps1 .\quickstart.ps1
``` ```
**Windows (Command Prompt):**
```batch
quickstart.bat
```
### Air-Gapped/Restricted Environment Deployment ### Air-Gapped/Restricted Environment Deployment
**For environments with restricted npm access:** **For environments with restricted npm access:**
@@ -65,7 +60,7 @@ This script:
2. Packages pre-built files into Docker 2. Packages pre-built files into Docker
3. Starts all services 3. Starts all services
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed instructions. See [DEPLOYMENT.md](docs/DEPLOYMENT.md) for detailed instructions.
### Manual Setup with Docker Compose ### Manual Setup with Docker Compose

View File

@@ -1,4 +1,4 @@
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
@@ -39,10 +39,8 @@ app.add_middleware(
app.include_router(artifacts_router) app.include_router(artifacts_router)
app.include_router(seed_router) app.include_router(seed_router)
# Mount static files # Static directory setup
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static") 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")
@@ -87,15 +85,13 @@ async def ui_root():
# Catch-all route for Angular SPA routing - must be last # Catch-all route for Angular SPA routing - must be last
@app.get("/{full_path:path}") @app.get("/{full_path:path}")
async def serve_spa(full_path: str): async def serve_spa(full_path: str):
"""Serve Angular SPA for all non-API routes""" """Serve Angular SPA static files and handle client-side routing"""
# If it's an API route, it should have been handled by routers above # Try to serve static file first (JS, CSS, images, etc.)
# If it's a static file request, try to serve it file_path = os.path.join(static_dir, full_path)
if full_path.startswith("static/"): if os.path.exists(file_path) and os.path.isfile(file_path):
file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), full_path)
if os.path.exists(file_path):
return FileResponse(file_path) return FileResponse(file_path)
# For all other routes (Angular routes), serve index.html # For all other routes (Angular client-side routes), serve index.html
index_path = os.path.join(static_dir, "index.html") index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path): if os.path.exists(index_path):
return FileResponse(index_path) return FileResponse(index_path)

View File

@@ -36,7 +36,8 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
api: app:
container_name: warehouse13-app
build: . build: .
ports: ports:
- "8000:8000" - "8000:8000"

View File

View File

@@ -93,5 +93,8 @@
} }
} }
} }
},
"cli": {
"analytics": false
} }
} }

View File

@@ -1,106 +0,0 @@
@echo off
setlocal enabledelayedexpansion
echo =========================================
echo Warehouse13 - Quick Start
echo =========================================
echo.
REM Check if Docker is installed
where docker >nul 2>nul
if %errorlevel% neq 0 (
echo Error: Docker is not installed. Please install Docker Desktop first.
echo Visit: https://www.docker.com/products/docker-desktop
pause
exit /b 1
)
REM Check if Docker Compose is available
where docker-compose >nul 2>nul
if %errorlevel% neq 0 (
REM Try docker compose (new version)
docker compose version >nul 2>nul
if %errorlevel% neq 0 (
echo Error: Docker Compose is not available.
echo Please ensure Docker Desktop is running.
pause
exit /b 1
)
set COMPOSE_CMD=docker compose
) else (
set COMPOSE_CMD=docker-compose
)
REM Create .env file if it doesn't exist
if not exist .env (
echo Creating .env file from .env.example...
copy .env.example .env >nul
echo [OK] .env file created
) else (
echo [OK] .env file already exists
)
echo.
echo Building and starting services with Docker Compose...
%COMPOSE_CMD% up -d --build
if %errorlevel% neq 0 (
echo.
echo Error: Failed to start services.
echo Make sure Docker Desktop is running.
pause
exit /b 1
)
echo.
echo Waiting for services to be ready...
timeout /t 15 /nobreak >nul
echo.
echo =========================================
echo Services are running!
echo =========================================
echo.
echo Web UI: http://localhost:8000
echo API Docs: http://localhost:8000/docs
echo MinIO Console: http://localhost:9001
echo Username: minioadmin
echo Password: minioadmin
echo.
echo To view logs: %COMPOSE_CMD% logs -f
echo To stop: %COMPOSE_CMD% down
echo.
echo =========================================
echo Testing the API...
echo =========================================
echo.
REM Wait a bit more for API to be fully ready
timeout /t 5 /nobreak >nul
REM Test health endpoint
curl -s http://localhost:8000/health | findstr "healthy" >nul 2>nul
if %errorlevel% equ 0 (
echo [OK] API is healthy!
echo.
echo =========================================
echo Open your browser to get started:
echo http://localhost:8000
echo =========================================
) else (
echo [WARNING] API is not responding yet.
echo Please wait a moment and check http://localhost:8000
)
echo.
echo =========================================
echo Setup complete!
echo =========================================
echo.
echo Press any key to open the UI in your browser...
pause >nul
REM Open browser
start http://localhost:8000
exit /b 0

View File

@@ -121,7 +121,7 @@ Write-Host "Useful Commands:" -ForegroundColor Cyan
Write-Host " Generate seed data: " -NoNewline Write-Host " Generate seed data: " -NoNewline
Write-Host "Use the 'Generate Seed Data' button in the UI" -ForegroundColor Yellow Write-Host "Use the 'Generate Seed Data' button in the UI" -ForegroundColor Yellow
Write-Host " View logs: " -NoNewline Write-Host " View logs: " -NoNewline
Write-Host "$composeCmd logs -f api" -ForegroundColor Yellow Write-Host "$composeCmd logs -f app" -ForegroundColor Yellow
Write-Host " Restart services: " -NoNewline Write-Host " Restart services: " -NoNewline
Write-Host "$composeCmd restart" -ForegroundColor Yellow Write-Host "$composeCmd restart" -ForegroundColor Yellow
Write-Host " Stop all: " -NoNewline Write-Host " Stop all: " -NoNewline

View File

@@ -1,564 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f172a;
min-height: 100vh;
padding: 20px;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: #1e293b;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #1e3a8a 0%, #4338ca 100%);
color: white;
padding: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 28px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.logo {
font-family: 'Courier New', monospace;
font-weight: 700;
font-size: 24px;
color: #60a5fa;
letter-spacing: -1px;
padding: 2px 4px;
border: 2px solid #60a5fa;
border-radius: 4px;
background: rgba(96, 165, 250, 0.1);
}
.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: #0f172a;
border-bottom: 2px solid #334155;
}
.tab-button {
flex: 1;
padding: 16px 24px;
background: none;
border: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
color: #94a3b8;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.tab-button:hover {
background: #1e293b;
color: #e2e8f0;
}
.tab-button.active {
background: #1e293b;
color: #60a5fa;
border-bottom: 3px solid #60a5fa;
}
.tab-content {
display: none;
padding: 30px;
}
.tab-content.active {
display: block;
}
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
}
.filter-inline {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #0f172a;
border-radius: 6px;
border: 1px solid #334155;
min-width: 250px;
}
.filter-inline input {
flex: 1;
padding: 4px 8px;
background: transparent;
border: none;
color: #e2e8f0;
font-size: 14px;
}
.filter-inline input:focus {
outline: none;
}
.filter-inline input::placeholder {
color: #64748b;
}
.btn-clear {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.btn-clear:hover {
background: #334155;
color: #e2e8f0;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
transform: translateY(-1px);
}
.btn-secondary {
background: #334155;
color: #e2e8f0;
}
.btn-secondary:hover {
background: #475569;
}
.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: #1e3a8a;
color: #93c5fd;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
margin-left: auto;
}
.table-container {
overflow-x: auto;
border: 1px solid #334155;
border-radius: 8px;
background: #0f172a;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
thead {
background: #1e293b;
}
th {
padding: 14px 12px;
text-align: left;
font-weight: 600;
color: #94a3b8;
border-bottom: 2px solid #334155;
white-space: nowrap;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.5px;
}
th.sortable {
cursor: pointer;
user-select: none;
transition: color 0.3s;
}
th.sortable:hover {
color: #60a5fa;
}
.sort-indicator {
display: inline-block;
margin-left: 5px;
font-size: 10px;
color: #64748b;
}
th.sort-asc .sort-indicator::after {
content: '▲';
color: #60a5fa;
}
th.sort-desc .sort-indicator::after {
content: '▼';
color: #60a5fa;
}
td {
padding: 16px 12px;
border-bottom: 1px solid #1e293b;
color: #cbd5e1;
}
tbody tr:hover {
background: #1e293b;
}
.loading {
text-align: center;
color: #64748b;
padding: 40px !important;
}
.result-badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.result-pass {
background: #064e3b;
color: #6ee7b7;
}
.result-fail {
background: #7f1d1d;
color: #fca5a5;
}
.result-skip {
background: #78350f;
color: #fcd34d;
}
.result-error {
background: #7f1d1d;
color: #fca5a5;
}
.tag {
display: inline-block;
background: #1e3a8a;
color: #93c5fd;
padding: 3px 8px;
border-radius: 10px;
font-size: 11px;
margin: 2px;
}
.file-type-badge {
background: #1e3a8a;
color: #93c5fd;
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: #94a3b8;
}
.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: #cbd5e1;
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 #334155;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.3s;
background: #0f172a;
color: #e2e8f0;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
small {
color: #64748b;
font-size: 12px;
display: block;
margin-top: 4px;
}
#upload-status {
margin-top: 20px;
padding: 14px;
border-radius: 6px;
display: none;
}
#upload-status.success {
background: #064e3b;
color: #6ee7b7;
display: block;
}
#upload-status.error {
background: #7f1d1d;
color: #fca5a5;
display: block;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: #1e293b;
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.5);
border: 1px solid #334155;
}
.close {
position: absolute;
right: 20px;
top: 20px;
font-size: 28px;
font-weight: bold;
color: #64748b;
cursor: pointer;
transition: color 0.3s;
}
.close:hover {
color: #e2e8f0;
}
.detail-row {
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #334155;
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
font-weight: 600;
color: #94a3b8;
margin-bottom: 4px;
}
.detail-value {
color: #cbd5e1;
}
pre {
background: #0f172a;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
font-size: 12px;
border: 1px solid #334155;
}
code {
background: #0f172a;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: #93c5fd;
}
.action-buttons {
display: flex;
gap: 8px;
}
.icon-btn {
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: all 0.3s;
color: #94a3b8;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon-btn:hover {
background: #334155;
color: #e2e8f0;
transform: scale(1.1);
}
/* Ensure SVG icons inherit color */
.icon-btn svg {
stroke: currentColor;
}
@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;
}
.toolbar {
flex-wrap: wrap;
}
}

View File

@@ -1,251 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warehouse13 - Test Artifact Data Lake</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body>
<div class="container">
<header>
<h1><span class="logo">[W13]</span></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')">
<i data-lucide="database" style="width: 16px; height: 16px;"></i> Artifacts
</button>
<button class="tab-button" onclick="showTab('upload')">
<i data-lucide="upload" style="width: 16px; height: 16px;"></i> Upload
</button>
<button class="tab-button" onclick="showTab('query')">
<i data-lucide="search" style="width: 16px; height: 16px;"></i> Query
</button>
</nav>
<!-- Artifacts Tab -->
<div id="artifacts-tab" class="tab-content active">
<div class="toolbar">
<button onclick="loadArtifacts()" class="btn btn-primary">
<i data-lucide="refresh-cw" style="width: 16px; height: 16px;"></i> Refresh
</button>
<button id="auto-refresh-toggle" onclick="toggleAutoRefresh()" class="btn btn-success">
Auto-refresh: ON
</button>
<button onclick="generateSeedData()" class="btn btn-secondary">
<i data-lucide="sparkles" style="width: 16px; height: 16px;"></i> Generate Seed Data
</button>
<span id="artifact-count" class="count-badge"></span>
<div class="filter-inline">
<i data-lucide="search" style="width: 16px; height: 16px; color: #64748b;"></i>
<input type="text" id="filter-search" placeholder="Search..." oninput="filterTable()">
<button onclick="clearFilters()" class="btn-clear" title="Clear search">
<i data-lucide="x" style="width: 14px; height: 14px;"></i>
</button>
</div>
</div>
<div class="table-container">
<table id="artifacts-table">
<thead>
<tr>
<th class="sortable" onclick="sortTable('test_suite')">
Sim Source <span class="sort-indicator"></span>
</th>
<th class="sortable" onclick="sortTable('filename')">
Artifacts <span class="sort-indicator"></span>
</th>
<th class="sortable" onclick="sortTable('created_at')">
Date <span class="sort-indicator"></span>
</th>
<th class="sortable" onclick="sortTable('test_name')">
Uploaded By <span class="sort-indicator"></span>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="artifacts-tbody">
<tr>
<td colspan="5" 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="sim-source">Sim Source *</label>
<input type="text" id="sim-source" name="test_suite" placeholder="e.g., Jenkins, GitLab CI" required>
</div>
<div class="form-group">
<label for="uploaded-by">Uploaded By *</label>
<input type="text" id="uploaded-by" name="test_name" placeholder="e.g., john.doe" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="sim-source-id">SIM Source ID (for grouping)</label>
<input type="text" id="sim-source-id" name="sim_source_id" placeholder="e.g., sim_run_20251015_001">
<small>Use same ID for multiple artifacts from same source</small>
</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" required>
</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="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">
<i data-lucide="upload" style="width: 18px; height: 18px;"></i> 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">
<i data-lucide="search" style="width: 18px; height: 18px;"></i> Search
</button>
<button type="button" onclick="clearQuery()" class="btn btn-secondary">
<i data-lucide="x" style="width: 18px; height: 18px;"></i> Clear
</button>
</form>
</div>
</div>
<!-- Artifact Detail Modal -->
<div id="detail-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeDetailModal()">&times;</span>
<h2>Artifact Details</h2>
<div id="detail-content"></div>
</div>
</div>
</div>
<script src="/static/js/app.js"></script>
<script>
// Initialize Lucide icons
lucide.createIcons();
</script>
</body>
</html>

View File

@@ -1,592 +0,0 @@
// API Base URL
const API_BASE = '/api/v1';
// Pagination
let currentPage = 1;
let pageSize = 25;
let totalArtifacts = 0;
// Auto-refresh
let autoRefreshEnabled = true;
let autoRefreshInterval = null;
const REFRESH_INTERVAL_MS = 5000; // 5 seconds
// Sorting and filtering
let allArtifacts = []; // Store all artifacts for client-side sorting/filtering
let currentSortColumn = null;
let currentSortDirection = 'asc';
// Load API info on page load
window.addEventListener('DOMContentLoaded', () => {
loadApiInfo();
loadArtifacts();
startAutoRefresh();
});
// 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();
allArtifacts = artifacts; // Store for sorting/filtering
displayArtifacts(artifacts);
updatePagination(artifacts.length);
} catch (error) {
console.error('Error loading artifacts:', error);
document.getElementById('artifacts-tbody').innerHTML = `
<tr><td colspan="5" 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="5" class="loading">No artifacts found. Upload some files to get started!</td></tr>';
document.getElementById('artifact-count').textContent = '0 artifacts';
return;
}
// Apply current sort if active
let displayedArtifacts = artifacts;
if (currentSortColumn) {
displayedArtifacts = applySorting([...artifacts]);
}
tbody.innerHTML = displayedArtifacts.map(artifact => `
<tr>
<td>${artifact.sim_source_id || artifact.test_suite || '-'}</td>
<td>
<a href="#" onclick="showDetail(${artifact.id}); return false;" style="color: #60a5fa; text-decoration: none;">
${escapeHtml(artifact.filename)}
</a>
${artifact.tags && artifact.tags.length > 0 ? `<br><div style="margin-top: 5px;">${formatTags(artifact.tags)}</div>` : ''}
</td>
<td>${formatDate(artifact.created_at)}</td>
<td>${artifact.test_name || '-'}</td>
<td>
<div class="action-buttons">
<button class="icon-btn" onclick="downloadArtifact(${artifact.id}, '${escapeHtml(artifact.filename)}')" title="Download">
<i data-lucide="download" style="width: 16px; height: 16px;"></i>
</button>
<button class="icon-btn" onclick="deleteArtifact(${artifact.id})" title="Delete">
<i data-lucide="trash-2" style="width: 16px; height: 16px;"></i>
</button>
</div>
</td>
</tr>
`).join('');
document.getElementById('artifact-count').textContent = `${displayedArtifacts.length} artifacts`;
// Re-initialize Lucide icons for dynamically added content
lucide.createIcons();
}
// 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">Uploaded By</div>
<div class="detail-value">${artifact.test_name || '-'}</div>
</div>
<div class="detail-row">
<div class="detail-label">Sim Source</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">
<i data-lucide="download" style="width: 16px; height: 16px;"></i> Download
</button>
<button onclick="deleteArtifact(${artifact.id}); closeDetailModal();" class="btn btn-danger">
<i data-lucide="trash-2" style="width: 16px; height: 16px;"></i> Delete
</button>
</div>
`;
document.getElementById('detail-modal').classList.add('active');
// Re-initialize Lucide icons for modal content
lucide.createIcons();
} 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', 'sim_source_id'];
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);
}
// Auto-refresh functions
function startAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
if (autoRefreshEnabled) {
autoRefreshInterval = setInterval(() => {
// Only refresh if on the artifacts tab
const artifactsTab = document.getElementById('artifacts-tab');
if (artifactsTab && artifactsTab.classList.contains('active')) {
loadArtifacts(pageSize, (currentPage - 1) * pageSize);
}
}, REFRESH_INTERVAL_MS);
}
}
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
const toggleBtn = document.getElementById('auto-refresh-toggle');
if (autoRefreshEnabled) {
toggleBtn.textContent = 'Auto-refresh: ON';
toggleBtn.classList.remove('btn-secondary');
toggleBtn.classList.add('btn-success');
startAutoRefresh();
} else {
toggleBtn.textContent = 'Auto-refresh: OFF';
toggleBtn.classList.remove('btn-success');
toggleBtn.classList.add('btn-secondary');
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
}
}
// Apply sorting to artifacts array
function applySorting(artifacts) {
if (!currentSortColumn) return artifacts;
return artifacts.sort((a, b) => {
let aVal = a[currentSortColumn] || '';
let bVal = b[currentSortColumn] || '';
// Handle date sorting
if (currentSortColumn === 'created_at') {
aVal = new Date(aVal).getTime();
bVal = new Date(bVal).getTime();
} else {
// String comparison (case insensitive)
aVal = String(aVal).toLowerCase();
bVal = String(bVal).toLowerCase();
}
if (aVal < bVal) return currentSortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return currentSortDirection === 'asc' ? 1 : -1;
return 0;
});
}
// Sorting functionality
function sortTable(column) {
// Toggle sort direction if clicking same column
if (currentSortColumn === column) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
currentSortColumn = column;
currentSortDirection = 'asc';
}
// Update sort indicators
document.querySelectorAll('th.sortable').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
});
const sortedHeader = event.target.closest('th');
sortedHeader.classList.add(`sort-${currentSortDirection}`);
// Apply filter and sort
filterTable();
}
// Filtering functionality - searches across all columns
function filterTable() {
const searchTerm = document.getElementById('filter-search').value.toLowerCase();
const filteredArtifacts = allArtifacts.filter(artifact => {
if (!searchTerm) return true;
// Search across all relevant fields
const searchableText = [
artifact.test_suite || '',
artifact.filename || '',
artifact.test_name || '',
formatDate(artifact.created_at)
].join(' ').toLowerCase();
return searchableText.includes(searchTerm);
});
displayArtifacts(filteredArtifacts);
}
// Clear all filters
function clearFilters() {
document.getElementById('filter-search').value = '';
filterTable();
}