From f6d1412bc8b434056179740462d72c4923729e69 Mon Sep 17 00:00:00 2001 From: pratik Date: Tue, 14 Oct 2025 23:32:38 -0500 Subject: [PATCH] Switch to angular --- .claude/settings.local.json | 34 ++ .dockerignore | 36 ++ Dockerfile | 1 - Dockerfile.frontend | 38 ++ Dockerfile.frontend.simple | 14 + README.md | 29 +- app/api/tags.py | 117 +++++ app/database.py | 6 +- app/main.py | 31 +- app/models/tag.py | 21 + app/schemas/tag.py | 28 ++ docker-compose.production.yml | 77 +++ docker-compose.yml | 3 + API.md => docs/API.md | 0 ARCHITECTURE.md => docs/ARCHITECTURE.md | 0 DEPLOYMENT.md => docs/DEPLOYMENT.md | 0 FEATURES.md => docs/FEATURES.md | 0 FRONTEND_SETUP.md => docs/FRONTEND_SETUP.md | 0 docs/FRONTEND_USAGE.md | 114 +++++ docs/QUICKSTART.md | 195 ++++++++ SUMMARY.md => docs/SUMMARY.md | 0 docs/todos.md | 0 frontend/.editorconfig | 17 + frontend/.gitignore | 42 ++ frontend/README.md | 151 ++++++ frontend/angular.json | 111 +++++ frontend/package.json | 39 ++ frontend/proxy.conf.json | 11 + frontend/public/favicon.ico | Bin 0 -> 15086 bytes frontend/src/app/app.component.html | 125 +++++ frontend/src/app/app.component.scss | 244 +++++++++ frontend/src/app/app.component.spec.ts | 29 ++ frontend/src/app/app.component.ts | 93 ++++ frontend/src/app/app.config.ts | 13 + frontend/src/app/app.routes.ts | 3 + .../artifacts-table.component.html | 283 +++++++++++ .../artifacts-table.component.scss | 281 +++++++++++ .../artifacts-table.component.ts | 279 +++++++++++ .../query-form/query-form.component.html | 197 ++++++++ .../query-form/query-form.component.scss | 207 ++++++++ .../query-form/query-form.component.ts | 137 ++++++ .../tab-navigation.component.html | 30 ++ .../tab-navigation.component.scss | 44 ++ .../tab-navigation.component.ts | 33 ++ .../tag-manager/tag-manager.component.html | 151 ++++++ .../tag-manager/tag-manager.component.scss | 198 ++++++++ .../tag-manager/tag-manager.component.ts | 173 +++++++ .../upload-form/upload-form.component.html | 199 ++++++++ .../upload-form/upload-form.component.scss | 124 +++++ .../upload-form/upload-form.component.ts | 176 +++++++ frontend/src/app/models/artifact.interface.ts | 51 ++ frontend/src/app/services/api.service.ts | 18 + frontend/src/app/services/artifact.service.ts | 72 +++ .../src/app/services/notification.service.ts | 64 +++ frontend/src/index.html | 15 + frontend/src/main.ts | 6 + .../styles.css => frontend/src/styles.scss | 283 ++++++++++- frontend/tsconfig.app.json | 15 + frontend/tsconfig.json | 27 + frontend/tsconfig.spec.json | 15 + nginx.conf | 76 +++ quickstart.bat | 106 ---- quickstart.ps1 | 129 ----- scripts/dev-start.ps1 | 156 ++++++ scripts/dev-start.sh | 89 ++++ quickstart.sh => scripts/quickstart-build.sh | 79 ++- scripts/quickstart.ps1 | 213 ++++++++ scripts/quickstart.sh | 189 +++++++ static/index.html | 208 -------- static/js/app.js | 462 ------------------ utils/seed_data.py | 126 ++++- 71 files changed, 5542 insertions(+), 991 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 Dockerfile.frontend create mode 100644 Dockerfile.frontend.simple create mode 100644 app/api/tags.py create mode 100644 app/models/tag.py create mode 100644 app/schemas/tag.py create mode 100644 docker-compose.production.yml rename API.md => docs/API.md (100%) rename ARCHITECTURE.md => docs/ARCHITECTURE.md (100%) rename DEPLOYMENT.md => docs/DEPLOYMENT.md (100%) rename FEATURES.md => docs/FEATURES.md (100%) rename FRONTEND_SETUP.md => docs/FRONTEND_SETUP.md (100%) create mode 100644 docs/FRONTEND_USAGE.md create mode 100644 docs/QUICKSTART.md rename SUMMARY.md => docs/SUMMARY.md (100%) create mode 100644 docs/todos.md create mode 100644 frontend/.editorconfig create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/angular.json create mode 100644 frontend/package.json create mode 100644 frontend/proxy.conf.json create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/src/app/app.component.html create mode 100644 frontend/src/app/app.component.scss create mode 100644 frontend/src/app/app.component.spec.ts create mode 100644 frontend/src/app/app.component.ts create mode 100644 frontend/src/app/app.config.ts create mode 100644 frontend/src/app/app.routes.ts create mode 100644 frontend/src/app/components/artifacts-table/artifacts-table.component.html create mode 100644 frontend/src/app/components/artifacts-table/artifacts-table.component.scss create mode 100644 frontend/src/app/components/artifacts-table/artifacts-table.component.ts create mode 100644 frontend/src/app/components/query-form/query-form.component.html create mode 100644 frontend/src/app/components/query-form/query-form.component.scss create mode 100644 frontend/src/app/components/query-form/query-form.component.ts create mode 100644 frontend/src/app/components/tab-navigation/tab-navigation.component.html create mode 100644 frontend/src/app/components/tab-navigation/tab-navigation.component.scss create mode 100644 frontend/src/app/components/tab-navigation/tab-navigation.component.ts create mode 100644 frontend/src/app/components/tag-manager/tag-manager.component.html create mode 100644 frontend/src/app/components/tag-manager/tag-manager.component.scss create mode 100644 frontend/src/app/components/tag-manager/tag-manager.component.ts create mode 100644 frontend/src/app/components/upload-form/upload-form.component.html create mode 100644 frontend/src/app/components/upload-form/upload-form.component.scss create mode 100644 frontend/src/app/components/upload-form/upload-form.component.ts create mode 100644 frontend/src/app/models/artifact.interface.ts create mode 100644 frontend/src/app/services/api.service.ts create mode 100644 frontend/src/app/services/artifact.service.ts create mode 100644 frontend/src/app/services/notification.service.ts create mode 100644 frontend/src/index.html create mode 100644 frontend/src/main.ts rename static/css/styles.css => frontend/src/styles.scss (58%) create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.spec.json create mode 100644 nginx.conf delete mode 100644 quickstart.bat delete mode 100644 quickstart.ps1 create mode 100644 scripts/dev-start.ps1 create mode 100644 scripts/dev-start.sh rename quickstart.sh => scripts/quickstart-build.sh (53%) mode change 100755 => 100644 create mode 100644 scripts/quickstart.ps1 create mode 100644 scripts/quickstart.sh delete mode 100644 static/index.html delete mode 100644 static/js/app.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2649fcb --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,34 @@ +{ + "permissions": { + "allow": [ + "Bash(npx:*)", + "Bash(npm run build:*)", + "Bash(ng add:*)", + "Bash(npm run start:*)", + "Bash(chmod:*)", + "Bash(pkill:*)", + "Bash(./quickstart.sh:*)", + "Bash(docker-compose:*)", + "Bash(timeout:*)", + "Bash(curl:*)", + "Bash(quickstart.bat --help)", + "Bash(powershell:*)", + "Bash(netstat:*)", + "Bash(findstr:*)", + "Bash(taskkill:*)", + "Bash(Get-Process -Name \"node\" -ErrorAction SilentlyContinue)", + "Bash(Stop-Process -Force)", + "Bash(python -m uvicorn:*)", + "Bash(docker compose:*)", + "Bash(docker:*)", + "Bash(npm start)", + "Bash(python:*)", + "Bash(ng serve:*)", + "Bash(if exist .angular rmdir /s /q .angular)", + "Bash(dir:*)", + "Bash(tree:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.dockerignore b/.dockerignore index 1fbf578..6b02f27 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +# Python cache __pycache__ *.pyc *.pyo @@ -5,15 +6,50 @@ __pycache__ .Python env/ venv/ + +# Node modules (critical for fixing esbuild platform issue) +node_modules +frontend/node_modules +*/node_modules + +# Build outputs +frontend/dist +frontend/.angular + +# Environment variables *.env .env + +# Git .git .gitignore + +# Documentation *.md + +# IDE files .vscode .idea + +# Logs *.log + +# OS files .DS_Store + +# Configuration helm/ .gitlab-ci.yml docker-compose.yml + +# Development files +frontend/.vscode +frontend/src/**/*.spec.ts + +# Runtime data +*.pid +*.seed +*.pid.lock + +# Coverage +coverage diff --git a/Dockerfile b/Dockerfile index ea95f18..1741c6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,6 @@ 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/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..e03368d --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,38 @@ +# Multi-stage build for Angular frontend +FROM node:18-alpine as frontend-builder + +# Install dependencies for native modules +RUN apk add --no-cache python3 make g++ + +WORKDIR /frontend + +# Copy package files first for better layer caching +COPY frontend/package*.json ./ + +# Clean install dependencies with explicit platform targeting +# This ensures esbuild and other native modules are built for Alpine Linux +RUN npm ci --force + +# Copy frontend source (excluding node_modules via .dockerignore) +COPY frontend/src ./src +COPY frontend/public ./public +COPY frontend/angular.json ./ +COPY frontend/tsconfig*.json ./ + +# Build the Angular app for production +RUN npm run build --verbose + +# Production image with nginx +FROM nginx:alpine + +# Copy built Angular app +COPY --from=frontend-builder /frontend/dist/frontend /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/Dockerfile.frontend.simple b/Dockerfile.frontend.simple new file mode 100644 index 0000000..5ba7e3b --- /dev/null +++ b/Dockerfile.frontend.simple @@ -0,0 +1,14 @@ +# Simple approach - build on host and copy dist folder +FROM nginx:alpine + +# Copy pre-built Angular app +COPY frontend/dist/frontend /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README.md b/README.md index 31127db..c2faf59 100644 --- a/README.md +++ b/README.md @@ -38,17 +38,12 @@ A lightweight, cloud-native API for storing and querying test artifacts includin **Linux/macOS:** ```bash -./quickstart.sh +./scripts/quickstart.sh ``` **Windows (PowerShell):** ```powershell -.\quickstart.ps1 -``` - -**Windows (Command Prompt):** -```batch -quickstart.bat +.\scripts\quickstart.ps1 ``` ### Manual Setup with Docker Compose @@ -306,6 +301,26 @@ alembic upgrade head - Verify `MINIO_ENDPOINT` is correct - Check MinIO credentials +## Documentation + +Detailed documentation is available in the `docs/` folder: + +- **[Quick Start Guide](docs/QUICKSTART.md)** - Get started in minutes +- **[API Documentation](docs/API.md)** - Complete API reference +- **[Architecture](docs/ARCHITECTURE.md)** - System design and architecture +- **[Features](docs/FEATURES.md)** - Detailed feature descriptions +- **[Deployment Guide](docs/DEPLOYMENT.md)** - Production deployment instructions +- **[Frontend Setup](docs/FRONTEND_SETUP.md)** - Angular frontend setup +- **[Frontend Usage](docs/FRONTEND_USAGE.md)** - Using the web UI + +## Scripts + +Helper scripts are available in the `scripts/` folder: + +- **`quickstart.sh` / `quickstart.ps1`** - Quick start with Docker Compose +- **`quickstart-build.sh`** - Quick start with image rebuild +- **`dev-start.sh` / `dev-start.ps1`** - Start development environment + ## License [Your License Here] diff --git a/app/api/tags.py b/app/api/tags.py new file mode 100644 index 0000000..bb45623 --- /dev/null +++ b/app/api/tags.py @@ -0,0 +1,117 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List + +from app.database import get_db +from app.models.tag import Tag +from app.schemas.tag import TagCreate, TagUpdate, TagResponse + +router = APIRouter(prefix="/api/v1/tags", tags=["tags"]) + + +@router.post("/", response_model=TagResponse, status_code=201) +async def create_tag(tag: TagCreate, db: Session = Depends(get_db)): + """ + Create a new tag + + - **name**: Tag name (unique, required) + - **description**: Tag description (optional) + - **color**: Hex color code (optional, e.g., #FF5733) + """ + # Check if tag already exists + existing_tag = db.query(Tag).filter(Tag.name == tag.name).first() + if existing_tag: + raise HTTPException(status_code=400, detail=f"Tag with name '{tag.name}' already exists") + + db_tag = Tag(**tag.model_dump()) + db.add(db_tag) + db.commit() + db.refresh(db_tag) + + return db_tag + + +@router.get("/", response_model=List[TagResponse]) +async def list_tags( + limit: int = Query(default=100, le=1000), + offset: int = Query(default=0, ge=0), + db: Session = Depends(get_db) +): + """List all tags with pagination""" + tags = db.query(Tag).order_by(Tag.name).offset(offset).limit(limit).all() + return tags + + +@router.get("/{tag_id}", response_model=TagResponse) +async def get_tag(tag_id: int, db: Session = Depends(get_db)): + """Get tag by ID""" + tag = db.query(Tag).filter(Tag.id == tag_id).first() + if not tag: + raise HTTPException(status_code=404, detail="Tag not found") + return tag + + +@router.get("/name/{tag_name}", response_model=TagResponse) +async def get_tag_by_name(tag_name: str, db: Session = Depends(get_db)): + """Get tag by name""" + tag = db.query(Tag).filter(Tag.name == tag_name).first() + if not tag: + raise HTTPException(status_code=404, detail="Tag not found") + return tag + + +@router.put("/{tag_id}", response_model=TagResponse) +async def update_tag(tag_id: int, tag_update: TagUpdate, db: Session = Depends(get_db)): + """ + Update a tag + + - **name**: Tag name (optional) + - **description**: Tag description (optional) + - **color**: Hex color code (optional) + """ + tag = db.query(Tag).filter(Tag.id == tag_id).first() + if not tag: + raise HTTPException(status_code=404, detail="Tag not found") + + # Check if new name conflicts with existing tag + if tag_update.name and tag_update.name != tag.name: + existing_tag = db.query(Tag).filter(Tag.name == tag_update.name).first() + if existing_tag: + raise HTTPException(status_code=400, detail=f"Tag with name '{tag_update.name}' already exists") + + # Update fields + update_data = tag_update.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(tag, field, value) + + db.commit() + db.refresh(tag) + + return tag + + +@router.delete("/{tag_id}") +async def delete_tag(tag_id: int, db: Session = Depends(get_db)): + """Delete a tag""" + tag = db.query(Tag).filter(Tag.id == tag_id).first() + if not tag: + raise HTTPException(status_code=404, detail="Tag not found") + + db.delete(tag) + db.commit() + + return {"message": f"Tag '{tag.name}' deleted successfully"} + + +@router.post("/search", response_model=List[TagResponse]) +async def search_tags( + query: str = Query(..., min_length=1, description="Search query"), + limit: int = Query(default=100, le=1000), + db: Session = Depends(get_db) +): + """Search tags by name or description""" + tags = db.query(Tag).filter( + (Tag.name.ilike(f"%{query}%")) | (Tag.description.ilike(f"%{query}%")) + ).order_by(Tag.name).limit(limit).all() + + return tags diff --git a/app/database.py b/app/database.py index b53ae14..2e2b077 100644 --- a/app/database.py +++ b/app/database.py @@ -1,7 +1,8 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.config import settings -from app.models.artifact import Base +from app.models.artifact import Base as ArtifactBase +from app.models.tag import Base as TagBase engine = create_engine(settings.database_url) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @@ -9,7 +10,8 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def init_db(): """Initialize database tables""" - Base.metadata.create_all(bind=engine) + ArtifactBase.metadata.create_all(bind=engine) + TagBase.metadata.create_all(bind=engine) def get_db(): diff --git a/app/main.py b/app/main.py index c5d3770..95facbc 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,8 @@ 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.api.tags import router as tags_router from app.database import init_db from app.config import settings import logging @@ -38,11 +37,9 @@ app.add_middleware( # Include routers app.include_router(artifacts_router) app.include_router(seed_router) +app.include_router(tags_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") +# Note: Frontend is now served separately as an Angular application @app.on_event("startup") @@ -69,19 +66,15 @@ async def api_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 - } + """API root - Frontend is served separately""" + return { + "message": "Test Artifact Data Lake API", + "version": "1.0.0", + "docs": "/docs", + "frontend": "Frontend is served separately on port 4200 (development) or via reverse proxy (production)", + "deployment_mode": settings.deployment_mode, + "storage_backend": settings.storage_backend + } @app.get("/health") diff --git a/app/models/tag.py b/app/models/tag.py new file mode 100644 index 0000000..3765daf --- /dev/null +++ b/app/models/tag.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, String, Integer, DateTime, Text +from sqlalchemy.ext.declarative import declarative_base +from datetime import datetime + +Base = declarative_base() + + +class Tag(Base): + __tablename__ = "tags" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), unique=True, nullable=False, index=True) + description = Column(Text) + color = Column(String(7)) # Hex color code, e.g., #FF5733 + + # Timestamps + created_at = Column(DateTime, default=datetime.utcnow, index=True) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f"" diff --git a/app/schemas/tag.py b/app/schemas/tag.py new file mode 100644 index 0000000..8d894c8 --- /dev/null +++ b/app/schemas/tag.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional + + +class TagBase(BaseModel): + name: str = Field(..., min_length=1, max_length=100, description="Tag name") + description: Optional[str] = Field(None, description="Tag description") + color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$", description="Hex color code (e.g., #FF5733)") + + +class TagCreate(TagBase): + pass + + +class TagUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1, max_length=100, description="Tag name") + description: Optional[str] = Field(None, description="Tag description") + color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$", description="Hex color code") + + +class TagResponse(TagBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 0000000..b8ba04b --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,77 @@ +version: '3.8' + +services: + postgres: + image: postgres:15 + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: datalake + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U user"] + interval: 10s + timeout: 5s + retries: 5 + + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 10s + timeout: 5s + retries: 5 + + api: + build: . + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql://user:password@postgres:5432/datalake + STORAGE_BACKEND: minio + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin + MINIO_BUCKET_NAME: test-artifacts + MINIO_SECURE: "false" + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_healthy + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"] + interval: 30s + timeout: 10s + retries: 3 + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + ports: + - "80:80" + depends_on: + api: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + postgres_data: + minio_data: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4807e7d..cf82292 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,6 +57,9 @@ services: timeout: 10s retries: 3 +# Frontend service removed from default compose - use dev-start.sh for development +# For production with built frontend, use: docker-compose -f docker-compose.production.yml up + volumes: postgres_data: minio_data: diff --git a/API.md b/docs/API.md similarity index 100% rename from API.md rename to docs/API.md diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/DEPLOYMENT.md b/docs/DEPLOYMENT.md similarity index 100% rename from DEPLOYMENT.md rename to docs/DEPLOYMENT.md diff --git a/FEATURES.md b/docs/FEATURES.md similarity index 100% rename from FEATURES.md rename to docs/FEATURES.md diff --git a/FRONTEND_SETUP.md b/docs/FRONTEND_SETUP.md similarity index 100% rename from FRONTEND_SETUP.md rename to docs/FRONTEND_SETUP.md diff --git a/docs/FRONTEND_USAGE.md b/docs/FRONTEND_USAGE.md new file mode 100644 index 0000000..064edf2 --- /dev/null +++ b/docs/FRONTEND_USAGE.md @@ -0,0 +1,114 @@ +# Frontend Usage Guide + +The Test Artifact Data Lake now features a modern Angular frontend with Material Design components. This guide explains how to run the application in different modes. + +## Quick Start Options + +### 1. Development Mode (Recommended for Development) +**Hot reload enabled, fastest for development** + +**Linux/macOS:** +```bash +./dev-start.sh +``` + +**Windows:** +```batch +dev-start.bat +``` + +- Backend services: `http://localhost:8000` +- Frontend: `http://localhost:4200` (with hot reload) +- API Docs: `http://localhost:8000/docs` +- MinIO Console: `http://localhost:9001` + +### 2. Production Mode (Complete Docker Stack) +**Pre-built frontend served via Nginx** + +**Linux/macOS:** +```bash +./quickstart-build.sh +``` + +**Windows:** (Manual steps) +```batch +cd frontend +npm install +npm run build +cd .. +docker-compose -f docker-compose.production.yml up -d +``` + +- Complete application: `http://localhost:80` +- API (proxied): `http://localhost:80/api/` +- API Docs: `http://localhost:80/docs` +- MinIO Console: `http://localhost:9001` + +### 3. Backend Only Mode +**For API-only usage or custom frontend setup** + +**Any platform:** +```bash +./quickstart.sh # Linux/macOS +quickstart.bat # Windows +quickstart.ps1 # PowerShell +``` + +- Backend API: `http://localhost:8000` +- API Docs: `http://localhost:8000/docs` +- MinIO Console: `http://localhost:9001` + +## Technical Details + +### Architecture +- **Frontend**: Angular 19 with Angular Material Design +- **Backend**: FastAPI with PostgreSQL and MinIO +- **Development**: Frontend dev server + Backend containers +- **Production**: Nginx serving Angular + Backend containers + +### Ports +- `80` - Production frontend (Nginx) +- `4200` - Development frontend (Angular dev server) +- `8000` - Backend API (FastAPI) +- `5432` - PostgreSQL database +- `9000` - MinIO storage +- `9001` - MinIO console + +### Development Workflow +1. Use `dev-start.sh` or `dev-start.bat` for daily development +2. Frontend changes automatically reload at `http://localhost:4200` +3. Backend API available at `http://localhost:8000` +4. Use browser dev tools for debugging + +### Production Deployment +1. Build frontend: `npm run build` in `frontend/` directory +2. Use `docker-compose.production.yml` for complete stack +3. Nginx proxies API requests to backend +4. Static assets served efficiently by Nginx + +## Troubleshooting + +### Frontend Build Issues +If you encounter esbuild platform errors: +1. Delete `frontend/node_modules` +2. Run `npm install` in `frontend/` directory +3. Try the development mode first: `./dev-start.sh` + +### Port Conflicts +- Development: Change Angular port in `angular.json` +- Production: Modify `docker-compose.production.yml` ports + +### Docker Issues +- Ensure Docker Desktop is running +- Try `docker-compose down` and restart +- Check logs: `docker-compose logs -f api` + +## Features +- Modern Material Design interface +- Responsive design for mobile/tablet +- File upload with drag-and-drop +- Advanced search and filtering +- Tag management system +- Real-time notifications +- Data visualization +- Export capabilities \ No newline at end of file diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md new file mode 100644 index 0000000..8fd012c --- /dev/null +++ b/docs/QUICKSTART.md @@ -0,0 +1,195 @@ +# Quick Start Guide + +## Overview + +The Test Artifact Data Lake platform provides several ways to run the application depending on your use case: + +- **Development**: Backend services in Docker + Frontend dev server with hot reload +- **Production**: Complete stack in Docker containers +- **Testing**: Various rebuild and cleanup options + +## Prerequisites + +- **Docker Desktop** (Windows/macOS) or **Docker** + **Docker Compose** (Linux) +- **Node.js 18+** (for development mode) +- **npm** (for development mode) + +## Quick Start Options + +### 1. Development Mode (Recommended for Development) + +**Linux/macOS:** +```bash +./quickstart.sh # Backend services only +./dev-start.sh # Backend + Frontend dev server +``` + +**Windows:** +```powershell +.\quickstart.ps1 # Backend services only +.\quickstart.ps1 -FullStack # Complete stack +.\quickstart.ps1 -Rebuild # Rebuild containers +.\dev-start.ps1 # Backend + Frontend dev server +``` + +**URLs:** +- Frontend: http://localhost:4200 (with hot reload) +- API: http://localhost:8000 +- API Docs: http://localhost:8000/docs +- MinIO Console: http://localhost:9001 + +### 2. Production Mode (Complete Stack) + +**Linux/macOS:** +```bash +./quickstart.sh --full-stack +``` + +**Windows:** +```cmd +.\quickstart.ps1 -FullStack +``` + +**URLs:** +- Frontend: http://localhost:80 (production build) +- API: http://localhost:8000 +- MinIO Console: http://localhost:9001 + +### 3. Force Rebuild (When Code Changes) + +**Linux/macOS:** +```bash +./quickstart.sh --rebuild # Rebuild backend only +./quickstart.sh --rebuild --full-stack # Rebuild complete stack +``` + +**Windows:** +```cmd +.\quickstart.ps1 -Rebuild # Rebuild backend only +.\quickstart.ps1 -Rebuild -FullStack # Rebuild complete stack +``` + +## Detailed Usage + +### Development Workflow + +1. **Start backend services:** + ```bash + ./quickstart.sh + ``` + +2. **Start frontend in development mode:** + ```bash + ./dev-start.sh + ``` + Or manually: + ```bash + cd frontend + npm install + npm run start + ``` + +3. **Make changes to your code** - Frontend will auto-reload + +4. **When backend code changes:** + ```bash + ./quickstart.sh --rebuild + ``` + +### Production Testing + +1. **Build and run complete stack:** + ```bash + ./quickstart.sh --full-stack + ``` + +2. **Test at http://localhost:80** + +3. **When code changes:** + ```bash + ./quickstart.sh --rebuild --full-stack + ``` + +## Command Reference + +### quickstart.sh / quickstart.ps1 + +| Option | Description | +|--------|-------------| +| (none) | Start backend services only (default) | +| `--full-stack` | Start complete stack including frontend | +| `--rebuild` | Force rebuild of containers | +| `--help` | Show help message | + +### dev-start.sh / dev-start.ps1 + +Starts backend services + frontend development server with hot reload. + +## Stopping Services + +**Backend only:** +```bash +docker-compose down +``` + +**Complete stack:** +```bash +docker-compose -f docker-compose.production.yml down +``` + +## Logs + +**Backend services:** +```bash +docker-compose logs -f +``` + +**Complete stack:** +```bash +docker-compose -f docker-compose.production.yml logs -f +``` + +**Specific service:** +```bash +docker-compose logs -f api +docker-compose logs -f postgres +docker-compose logs -f minio +``` + +## Environment Variables + +Copy `.env.example` to `.env` and modify as needed: + +```bash +cp .env.example .env +``` + +The quickstart scripts will automatically create this file if it doesn't exist. + +## Troubleshooting + +### Container Issues +- **Force rebuild:** Use `--rebuild` flag +- **Clean everything:** `docker-compose down --volumes --rmi all` +- **Check Docker:** Ensure Docker Desktop is running + +### Frontend Issues +- **Dependencies:** Run `npm install` in `frontend/` directory +- **Port conflicts:** Check if port 4200 is available +- **Node version:** Ensure Node.js 18+ is installed + +### Backend Issues +- **API not responding:** Wait longer for services to start (can take 30+ seconds) +- **Database issues:** Check `docker-compose logs postgres` +- **Storage issues:** Check `docker-compose logs minio` + +## Development vs Production + +| Feature | Development | Production | +|---------|-------------|------------| +| Frontend | Hot reload dev server | Built Angular app | +| Port | 4200 | 80 | +| Build time | Fast startup | Slower (builds Angular) | +| Use case | Development, testing | Demo, staging | + +Choose the mode that best fits your workflow! \ No newline at end of file diff --git a/SUMMARY.md b/docs/SUMMARY.md similarity index 100% rename from SUMMARY.md rename to docs/SUMMARY.md diff --git a/docs/todos.md b/docs/todos.md new file mode 100644 index 0000000..e69de29 diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..cc7b141 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..303a19f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,151 @@ +# Test Artifact Data Lake - Angular Frontend + +This is the Angular 19 frontend for the Test Artifact Data Lake application. It replaces the static HTML/JS implementation with a modern, component-based architecture. + +## Features + +✅ **Multi-component Architecture**: Built with reusable Angular components +✅ **Tab Navigation**: Clean tab-based interface for Artifacts, Upload, and Query +✅ **Event ID Support**: Group multiple artifacts under the same event ID +✅ **Expandable Binaries Display**: Show first 4 binaries, expandable for more +✅ **Advanced Tag Management**: Create tags on-the-spot with database persistence +✅ **Scoped Tags**: Organize tags by scope (project, environment, priority, etc.) +✅ **Comprehensive Filtering**: Filter artifacts by all table criteria +✅ **Real-time Search**: As-you-type filtering in query form +✅ **Responsive Design**: Mobile-friendly interface + +## Components + +### Core Components +- **TabNavigationComponent**: Manages tab switching between Artifacts, Upload, and Query +- **ArtifactsTableComponent**: Displays artifacts with expandable binaries/tags and Event ID support +- **UploadFormComponent**: File upload with Event ID and binaries support +- **QueryFormComponent**: Advanced search with real-time filtering +- **TagManagerComponent**: On-the-spot tag creation with scoped tags + +### Services +- **ArtifactService**: Handles all artifact-related API calls +- **ApiService**: Manages general API information + +## Development + +### Prerequisites +- Node.js (v18 or later) +- Angular CLI 19 + +### Setup +```bash +cd frontend +npm install +``` + +### Development Server +```bash +npm start +# or +ng serve +``` +The app will be available at `http://localhost:4200` + +### Build for Production +```bash +npm run build +# or +ng build +``` +Built files will be in `dist/frontend/` + +## API Integration + +The frontend expects the backend API to be available at: +- Development: Same origin as the frontend +- Production: Configurable via environment files + +### Required API Endpoints +- `GET /api` - API information +- `GET /api/v1/artifacts/` - List artifacts +- `GET /api/v1/artifacts/{id}` - Get artifact details +- `POST /api/v1/artifacts/upload` - Upload artifact +- `DELETE /api/v1/artifacts/{id}` - Delete artifact +- `GET /api/v1/artifacts/{id}/download` - Download artifact +- `POST /api/v1/artifacts/query` - Query artifacts +- `POST /api/v1/seed/generate/{count}` - Generate seed data +- `GET /api/v1/tags` - List all tags +- `POST /api/v1/tags` - Create tag +- `POST /api/v1/artifacts/{id}/tags` - Add tag to artifact +- `DELETE /api/v1/artifacts/{id}/tags/{tag_id}` - Remove tag from artifact + +## Key Features Implementation + +### Event ID Support +Each artifact can be assigned an Event ID to group related artifacts together. This is displayed prominently in the table and can be used for filtering. + +### Expandable Binaries +When an artifact has more than 4 associated binaries, only the first 4 are shown with a "+X more" button to expand and see all binaries. + +### Advanced Tag Management +- Create tags on-the-spot directly in the table +- Organize tags by scope (project, environment, priority, category, status) +- Tags persist in the database across app restarts +- Visual indicators show which tags are already attached +- Quick-add existing tags from a categorized list + +### Comprehensive Filtering +The query form provides real-time filtering by: +- Filename (partial match) +- File type +- Test name +- Test suite +- Test result +- Tags (comma-separated) +- Date range + +Filters are applied immediately as you type, and active filters are displayed as visual chips. + +## Architecture Improvements + +### From Static to Angular +The original static JavaScript implementation has been converted to: + +1. **Component-based Architecture**: Each major feature is now a reusable component +2. **Type Safety**: Full TypeScript support with proper interfaces +3. **Reactive Programming**: Uses RxJS observables for API calls +4. **State Management**: Centralized state management through services +5. **Modular Design**: Easy to extend and maintain + +### Benefits +- **Maintainability**: Clear separation of concerns +- **Reusability**: Components can be reused and extended +- **Testing**: Angular's testing framework support +- **Performance**: Optimized change detection and lazy loading +- **Developer Experience**: Hot reload, TypeScript, and Angular DevTools + +## Deployment + +### Development +The Angular frontend can be served during development using `ng serve` and will proxy API calls to the backend. + +### Production +Build the application and serve the static files from any web server. Ensure the backend API is accessible from the same domain or configure CORS appropriately. + +### Integration with Existing Backend +The Angular frontend is designed to be a drop-in replacement for the static frontend. Simply: + +1. Build the Angular app: `npm run build` +2. Copy contents of `dist/frontend/` to your static files directory +3. Update your backend to serve the new `index.html` +4. Ensure API endpoints match the expected interface + +## Browser Support +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) + +## Future Enhancements +- Drag and drop file upload +- Bulk operations +- Advanced data visualization +- Real-time updates via WebSocket +- Export functionality +- User authentication integration \ No newline at end of file diff --git a/frontend/angular.json b/frontend/angular.json new file mode 100644 index 0000000..ea9cfa8 --- /dev/null +++ b/frontend/angular.json @@ -0,0 +1,111 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm", + "analytics": false + }, + "newProjectRoot": "projects", + "projects": { + "frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/frontend", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + }, + "configurations": { + "production": { + "buildTarget": "frontend:build:production" + }, + "development": { + "buildTarget": "frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "@angular/material/prebuilt-themes/azure-blue.css", + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..544a46a --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/cdk": "^19.2.19", + "@angular/common": "^19.2.0", + "@angular/compiler": "^19.2.0", + "@angular/core": "^19.2.0", + "@angular/forms": "^19.2.0", + "@angular/material": "^19.2.19", + "@angular/platform-browser": "^19.2.0", + "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/router": "^19.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.2.17", + "@angular/cli": "^19.2.17", + "@angular/compiler-cli": "^19.2.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.7.2" + } +} \ No newline at end of file diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json new file mode 100644 index 0000000..951b7da --- /dev/null +++ b/frontend/proxy.conf.json @@ -0,0 +1,11 @@ +{ + "/api": { + "target": "http://localhost:8000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug", + "pathRewrite": { + "^/api": "/api" + } + } +} diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html new file mode 100644 index 0000000..6c8bb29 --- /dev/null +++ b/frontend/src/app/app.component.html @@ -0,0 +1,125 @@ +
+ + + storage + {{ title }} + +
+ + + settings + {{ apiInfo.deployment_mode }} + + + folder + {{ apiInfo.storage_backend }} + + +
+
+ + + + + + +
+
+

+ storage + Artifacts ({{ artifacts.length }}) +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{ artifact.id }}Filename + description + {{ artifact.filename }} + Type + {{ artifact.file_type }} + Size{{ formatBytes(artifact.file_size) }}Test Name{{ artifact.test_name || '-' }}Result + + {{ artifact.test_result }} + + - +
+
+
+
+ + + + +
+
+

+ cloud_upload + Upload Artifacts +

+
+ +
+
+
+ + + + +
+
+

+ search + Query Artifacts +

+
+ + +
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss new file mode 100644 index 0000000..e0a91b5 --- /dev/null +++ b/frontend/src/app/app.component.scss @@ -0,0 +1,244 @@ +.app-container { + display: flex; + flex-direction: column; + height: 100vh; + background: #f5f5f5; +} + +.app-toolbar { + position: sticky; + top: 0; + z-index: 10; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); +} + +.app-icon { + margin-right: 12px; +} + +.app-title { + font-size: 20px; + font-weight: 500; + letter-spacing: 0.5px; +} + +.spacer { + flex: 1 1 auto; +} + +.header-info { + mat-chip-set { + mat-chip { + background-color: rgba(255, 255, 255, 0.2) !important; + color: white !important; + + mat-icon { + color: white !important; + } + } + } +} + +// Tab Group Styling +.main-tabs { + flex: 1; + display: flex; + flex-direction: column; + background: white; + + ::ng-deep { + .mat-mdc-tab-body-wrapper { + flex: 1; + padding: 0; + } + + .mat-mdc-tab-header { + background: white; + box-shadow: 0 2px 4px rgba(0,0,0,0.08); + } + + .mat-mdc-tab-labels { + padding: 0 24px; + } + + .mat-mdc-tab { + min-width: 120px; + height: 56px; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.5px; + text-transform: uppercase; + } + } +} + +// Tab Content Wrapper +.tab-content-wrapper { + padding: 24px; + max-width: 1400px; + margin: 0 auto; + width: 100%; + min-height: calc(100vh - 180px); +} + +// Content Header +.content-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 2px solid #e0e0e0; + + h2 { + display: flex; + align-items: center; + gap: 12px; + margin: 0; + font-size: 24px; + font-weight: 500; + color: #333; + + mat-icon { + color: #3f51b5; + font-size: 28px; + width: 28px; + height: 28px; + } + } + + button { + mat-icon { + margin-right: 8px; + } + } +} + +// Artifacts Table Styling +.artifacts-table { + width: 100%; + background: white; + border-radius: 8px; + overflow: hidden; + + th.mat-mdc-header-cell { + background: #f8f9fa; + color: #333; + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 16px 12px; + border-bottom: 2px solid #e0e0e0; + } + + td.mat-mdc-cell { + padding: 16px 12px; + font-size: 14px; + color: #555; + border-bottom: 1px solid #f0f0f0; + } + + tr.mat-mdc-row { + transition: background-color 0.2s ease; + + &:hover { + background-color: #f8f9fa; + } + } + + .filename-cell { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + + .file-icon { + color: #3f51b5; + font-size: 20px; + width: 20px; + height: 20px; + } + } + + .type-chip { + background-color: #e3f2fd !important; + color: #1976d2 !important; + font-weight: 500; + font-size: 12px; + padding: 4px 12px; + height: 28px; + } + + .text-muted { + color: #999; + } +} + +// Result Chips +.result-pass { + background-color: #e8f5e9 !important; + color: #2e7d32 !important; + font-weight: 600; +} + +.result-fail { + background-color: #ffebee !important; + color: #c62828 !important; + font-weight: 600; +} + +.result-skip { + background-color: #fff3e0 !important; + color: #ef6c00 !important; + font-weight: 600; +} + +.result-error { + background-color: #fce4ec !important; + color: #c2185b !important; + font-weight: 600; +} + +// Responsive Design +@media (max-width: 768px) { + .tab-content-wrapper { + padding: 16px; + } + + .app-title { + font-size: 16px; + } + + .header-info { + display: none; + } + + .content-header { + flex-direction: column; + align-items: flex-start; + gap: 16px; + + h2 { + font-size: 20px; + } + } + + .artifacts-table { + font-size: 12px; + + th.mat-mdc-header-cell, + td.mat-mdc-cell { + padding: 12px 8px; + } + } +} + +@media (max-width: 480px) { + .artifacts-table { + th.mat-mdc-header-cell:nth-child(n+5), + td.mat-mdc-cell:nth-child(n+5) { + display: none; + } + } +} diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..a6b0ab9 --- /dev/null +++ b/frontend/src/app/app.component.spec.ts @@ -0,0 +1,29 @@ +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'frontend' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('frontend'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend'); + }); +}); diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts new file mode 100644 index 0000000..7c709f2 --- /dev/null +++ b/frontend/src/app/app.component.ts @@ -0,0 +1,93 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatTableModule } from '@angular/material/table'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { UploadFormComponent } from './components/upload-form/upload-form.component'; +import { QueryFormComponent } from './components/query-form/query-form.component'; +import { ApiService } from './services/api.service'; +import { ArtifactService } from './services/artifact.service'; +import { ApiInfo, Artifact } from './models/artifact.interface'; + +@Component({ + selector: 'app-root', + imports: [ + CommonModule, + MatToolbarModule, + MatTableModule, + MatTabsModule, + MatChipsModule, + MatIconModule, + MatButtonModule, + UploadFormComponent, + QueryFormComponent + ], + templateUrl: './app.component.html', + styleUrl: './app.component.scss' +}) +export class AppComponent implements OnInit { + title = 'Test Artifact Data Lake'; + apiInfo: ApiInfo | null = null; + artifacts: Artifact[] = []; + displayedColumns: string[] = ['id', 'filename', 'file_type', 'file_size', 'test_name', 'test_result']; + selectedTabIndex = 0; + + constructor( + private apiService: ApiService, + private artifactService: ArtifactService + ) {} + + ngOnInit(): void { + this.loadApiInfo(); + this.loadArtifacts(); + } + + loadApiInfo(): void { + this.apiService.getApiInfo().subscribe({ + next: (info) => { + this.apiInfo = info; + }, + error: (error) => { + console.error('Error loading API info:', error); + } + }); + } + + loadArtifacts(): void { + this.artifactService.getArtifacts().subscribe({ + next: (artifacts) => { + console.log('Loaded artifacts:', artifacts.length); + this.artifacts = artifacts; + }, + error: (error) => { + console.error('Error loading artifacts:', error); + } + }); + } + + onUploadSuccess(): void { + this.loadArtifacts(); + this.selectedTabIndex = 0; // Switch back to artifacts tab + } + + onQueryResults(artifacts: Artifact[]): void { + this.artifacts = artifacts; + this.selectedTabIndex = 0; // Switch to artifacts tab to show results + } + + onFiltersChange(filters: any): void { + // Filters will be handled by the query component + console.log('Filters changed:', filters); + } + + formatBytes(bytes: number): string { + 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]; + } +} diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts new file mode 100644 index 0000000..d037d76 --- /dev/null +++ b/frontend/src/app/app.config.ts @@ -0,0 +1,13 @@ +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; + +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideHttpClient() + ] +}; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts new file mode 100644 index 0000000..dc39edb --- /dev/null +++ b/frontend/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = []; diff --git a/frontend/src/app/components/artifacts-table/artifacts-table.component.html b/frontend/src/app/components/artifacts-table/artifacts-table.component.html new file mode 100644 index 0000000..d619846 --- /dev/null +++ b/frontend/src/app/components/artifacts-table/artifacts-table.component.html @@ -0,0 +1,283 @@ +
+ +
+ LOADING STATE: {{ loading ? 'TRUE (Loading...)' : 'FALSE (Not Loading)' }} +
+ + + + +
+
+ + +
+ + + storage + {{ filteredArtifacts.length }} artifacts + + +
+
+
+ + +
+ +

Loading artifacts...

+
+ + +
+

Simple List Test

+

Filtered Artifacts Count: {{ filteredArtifacts.length }}

+
+ ID: {{ artifact.id }} | + Filename: {{ artifact.filename }} | + Type: {{ artifact.file_type }} +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID + {{ artifact.id }} + Event ID + + {{ artifact.event_id }} + + {{ artifact.id }} + Filename + + Type + {{ artifact.file_type }} + Size{{ formatBytes(artifact.file_size) }}Binaries +
+ + + code + {{ binary }} + + + + +{{ getHiddenBinariesCount(artifact.binaries) }} more + + - less + + + + + code + {{ binary }} + + +
+ + - + +
Test Name + {{ artifact.test_name || '-' }} + Suite + {{ artifact.test_suite || '-' }} + Result + + {{ getResultIcon(artifact.test_result) }} + {{ artifact.test_result }} + + - + Tags +
+ + + {{ tag }} + + + + +{{ getHiddenTagsCount(artifact.tags) }} more + + - less + + + + + {{ tag }} + + + + +
+ + + + +
Created + {{ formatDate(artifact.created_at) }} + Actions +
+ + +
+
+
+ inbox +

No artifacts found. Upload some files to get started!

+
+
+
+
+ + + + + + + +
+ + +
+ + + Artifact Details + + + + +

Artifact ID: {{ selectedArtifact.id }}

+

Filename: {{ selectedArtifact.filename }}

+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/artifacts-table/artifacts-table.component.scss b/frontend/src/app/components/artifacts-table/artifacts-table.component.scss new file mode 100644 index 0000000..c708d8b --- /dev/null +++ b/frontend/src/app/components/artifacts-table/artifacts-table.component.scss @@ -0,0 +1,281 @@ +.artifacts-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.toolbar-card { + margin-bottom: 0; +} + +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 16px; +} + +.toolbar-buttons { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.count-chip { + margin-left: auto; +} + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + gap: 16px; + + p { + color: #666; + margin: 0; + } +} + +.table-card { + overflow: hidden; +} + +.table-container { + overflow-x: auto; + max-height: 70vh; +} + +.artifacts-table { + width: 100%; + + .mat-mdc-cell, + .mat-mdc-header-cell { + padding: 12px 8px; + border-bottom: 1px solid #e0e0e0; + } + + .mat-mdc-header-cell { + font-weight: 600; + background-color: #fafafa; + } + + .mat-mdc-row:hover { + background-color: #f5f5f5; + } +} + +// Column specific styles +.binaries-cell, +.tags-cell { + max-width: 250px; + + mat-chip-set { + max-width: 100%; + } +} + +.filename-link { + text-align: left; + justify-content: flex-start; + text-transform: none; + + mat-icon { + margin-right: 8px; + } +} + +.text-muted { + color: #999; + font-style: italic; +} + +// Chip styles +.type-chip { + background-color: #e3f2fd !important; + color: #1976d2 !important; + font-size: 11px; + font-weight: 500; +} + +.binary-chip { + background-color: #f3e5f5 !important; + color: #7b1fa2 !important; + font-size: 10px; + + mat-icon { + font-size: 14px; + } +} + +.tag-chip { + background-color: #e8f5e8 !important; + color: #2e7d32 !important; + font-size: 10px; +} + +.expand-chip { + background-color: #fff3e0 !important; + color: #f57c00 !important; + font-size: 9px; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #ffe0b2 !important; + } +} + +.expanded-binaries, +.expanded-tags { + margin-top: 8px; +} + +// Result chips +.result-pass { + background-color: #e8f5e8 !important; + color: #2e7d32 !important; +} + +.result-fail { + background-color: #ffebee !important; + color: #d32f2f !important; +} + +.result-skip { + background-color: #fff8e1 !important; + color: #f57c00 !important; +} + +.result-error { + background-color: #fce4ec !important; + color: #c2185b !important; +} + +// Action buttons +.action-buttons { + display: flex; + gap: 4px; +} + +// No data state +.no-data { + text-align: center; + padding: 40px !important; +} + +.no-data-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + color: #666; + + mat-icon { + font-size: 48px; + width: 48px; + height: 48px; + color: #ccc; + } + + p { + margin: 0; + font-size: 16px; + } +} + +// Pagination +.pagination-card { + margin-top: 0; +} + +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 16px; +} + +// Detail modal +.detail-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.detail-modal { + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + position: relative; + + .close-button { + position: absolute; + top: 8px; + right: 8px; + } +} + +// Responsive design +@media (max-width: 768px) { + .toolbar { + flex-direction: column; + align-items: stretch; + } + + .toolbar-buttons { + justify-content: center; + } + + .count-chip { + margin-left: 0; + align-self: center; + } + + .artifacts-table { + font-size: 12px; + + .mat-mdc-cell, + .mat-mdc-header-cell { + padding: 8px 4px; + } + } + + .binaries-cell, + .tags-cell { + max-width: 150px; + } + + .action-buttons { + flex-direction: column; + } +} + +// Override Material styles +:host ::ng-deep { + .mat-mdc-table { + background: transparent; + } + + .mat-mdc-chip { + --mdc-chip-container-height: 24px; + --mdc-chip-with-avatar-container-height: 28px; + font-size: 11px; + } + + .mat-mdc-chip-set { + display: flex; + flex-wrap: wrap; + gap: 4px; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/artifacts-table/artifacts-table.component.ts b/frontend/src/app/components/artifacts-table/artifacts-table.component.ts new file mode 100644 index 0000000..c0b1f5f --- /dev/null +++ b/frontend/src/app/components/artifacts-table/artifacts-table.component.ts @@ -0,0 +1,279 @@ +import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MatTableModule } from '@angular/material/table'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatCardModule } from '@angular/material/card'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { Artifact } from '../../models/artifact.interface'; +import { ArtifactService } from '../../services/artifact.service'; +import { NotificationService } from '../../services/notification.service'; +import { TagManagerComponent } from '../tag-manager/tag-manager.component'; + +@Component({ + selector: 'app-artifacts-table', + imports: [ + CommonModule, + FormsModule, + MatTableModule, + MatButtonModule, + MatIconModule, + MatChipsModule, + MatCardModule, + MatProgressSpinnerModule, + MatTooltipModule, + MatDialogModule, + MatSnackBarModule, + TagManagerComponent + ], + templateUrl: './artifacts-table.component.html', + styleUrl: './artifacts-table.component.scss' +}) +export class ArtifactsTableComponent implements OnInit, OnChanges { + @Input() artifacts: Artifact[] = []; + @Input() filters: any = {}; + + displayedColumns: string[] = [ + 'id', + 'eventId', + 'filename', + 'type', + 'size', + 'binaries', + 'testName', + 'suite', + 'result', + 'tags', + 'created', + 'actions' + ]; + + expandedBinaries: { [key: number]: boolean } = {}; + expandedTags: { [key: number]: boolean } = {}; + + currentPage = 1; + pageSize = 25; + loading = false; // Start with false to show content immediately + + selectedArtifact: Artifact | null = null; + showDetailModal = false; + + filteredArtifacts: Artifact[] = []; + + constructor( + private artifactService: ArtifactService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + console.log('ArtifactsTableComponent ngOnInit - artifacts count:', this.artifacts.length); + console.log('Initial loading state:', this.loading); + // Always load artifacts on init + this.loadArtifacts(); + // Force show after a delay to debug + setTimeout(() => { + console.log('Timeout - forcing loading to false'); + this.loading = false; + }, 2000); + } + + ngOnChanges(changes: SimpleChanges): void { + console.log('ArtifactsTableComponent ngOnChanges - artifacts:', changes['artifacts']?.currentValue?.length || 0); + // Re-apply filters when artifacts or filters input changes + if (changes['artifacts'] || changes['filters']) { + this.applyFilters(); + } + } + + loadArtifacts(): void { + console.log('Loading artifacts...'); + this.loading = true; + this.artifactService.getArtifacts(this.pageSize, (this.currentPage - 1) * this.pageSize) + .subscribe({ + next: (artifacts) => { + console.log('Loaded artifacts:', artifacts.length); + this.artifacts = artifacts; + this.applyFilters(); + this.loading = false; + console.log('Loading complete. loading =', this.loading); + }, + error: (error) => { + console.error('Error loading artifacts:', error); + this.loading = false; + console.log('Error occurred. loading =', this.loading); + } + }); + } + + applyFilters(): void { + console.log('Applying filters to', this.artifacts.length, 'artifacts'); + this.filteredArtifacts = this.artifacts.filter(artifact => { + if (this.filters.filename && !artifact.filename.toLowerCase().includes(this.filters.filename.toLowerCase())) { + return false; + } + if (this.filters.fileType && artifact.file_type !== this.filters.fileType) { + return false; + } + if (this.filters.testName && !artifact.test_name?.toLowerCase().includes(this.filters.testName.toLowerCase())) { + return false; + } + if (this.filters.testSuite && !artifact.test_suite?.toLowerCase().includes(this.filters.testSuite.toLowerCase())) { + return false; + } + if (this.filters.testResult && artifact.test_result !== this.filters.testResult) { + return false; + } + if (this.filters.tags && this.filters.tags.length > 0) { + const hasMatchingTag = this.filters.tags.some((tag: string) => + artifact.tags.some(artifactTag => artifactTag.toLowerCase().includes(tag.toLowerCase())) + ); + if (!hasMatchingTag) return false; + } + return true; + }); + console.log('Filtered artifacts count:', this.filteredArtifacts.length); + } + + toggleBinariesExpansion(artifactId: number): void { + this.expandedBinaries[artifactId] = !this.expandedBinaries[artifactId]; + } + + toggleTagsExpansion(artifactId: number): void { + this.expandedTags[artifactId] = !this.expandedTags[artifactId]; + } + + showDetail(artifact: Artifact): void { + this.selectedArtifact = artifact; + this.showDetailModal = true; + } + + closeDetailModal(): void { + this.showDetailModal = false; + this.selectedArtifact = null; + } + + downloadArtifact(artifact: Artifact): void { + this.artifactService.downloadArtifact(artifact.id).subscribe({ + next: (blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = artifact.filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }, + error: (error) => { + console.error('Error downloading artifact:', error); + this.notificationService.showError('Error downloading artifact: ' + error.message); + } + }); + } + + async deleteArtifact(artifact: Artifact): Promise { + const confirmed = await this.notificationService.showConfirmation( + `Are you sure you want to delete "${artifact.filename}"? This cannot be undone.`, + 'Delete' + ); + + if (!confirmed) { + return; + } + + this.artifactService.deleteArtifact(artifact.id).subscribe({ + next: () => { + this.notificationService.showSuccess('Artifact deleted successfully'); + this.loadArtifacts(); + }, + error: (error) => { + console.error('Error deleting artifact:', error); + this.notificationService.showError('Error deleting artifact: ' + error.message); + } + }); + } + + generateSeedData(): void { + 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) { + this.notificationService.showWarning('Please enter a number between 1 and 100'); + return; + } + + this.artifactService.generateSeedData(num).subscribe({ + next: (result) => { + this.notificationService.showSuccess(result.message || 'Seed data generated successfully'); + this.loadArtifacts(); + }, + error: (error) => { + console.error('Error generating seed data:', error); + this.notificationService.showError('Error generating seed data: ' + error.message); + } + }); + } + + formatBytes(bytes: number): string { + 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]; + } + + formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleString(); + } + + getVisibleBinaries(binaries: string[] | undefined): string[] { + if (!binaries) return []; + return binaries.slice(0, 4); + } + + getHiddenBinariesCount(binaries: string[] | undefined): number { + if (!binaries) return 0; + return Math.max(0, binaries.length - 4); + } + + getVisibleTags(tags: string[]): string[] { + return tags.slice(0, 3); + } + + getHiddenTagsCount(tags: string[]): number { + return Math.max(0, tags.length - 3); + } + + previousPage(): void { + if (this.currentPage > 1) { + this.currentPage--; + this.loadArtifacts(); + } + } + + nextPage(): void { + this.currentPage++; + this.loadArtifacts(); + } + + onTagsUpdated(): void { + this.loadArtifacts(); + } + + getResultIcon(result: string): string { + switch (result) { + case 'pass': return 'check_circle'; + case 'fail': return 'cancel'; + case 'skip': return 'skip_next'; + case 'error': return 'error'; + default: return 'help'; + } + } +} diff --git a/frontend/src/app/components/query-form/query-form.component.html b/frontend/src/app/components/query-form/query-form.component.html new file mode 100644 index 0000000..9ccd289 --- /dev/null +++ b/frontend/src/app/components/query-form/query-form.component.html @@ -0,0 +1,197 @@ + + + + search + Query Artifacts + + + Search and filter your artifact collection + + + + +
+ +
+

Basic Search

+
+ + Filename + + description + + + + File Type + + All Types + + {{ type.toUpperCase() }} + + + category + +
+
+ + +
+

Test Information

+
+ + Test Name + + quiz + + + + Test Suite + + folder + +
+ +
+ + Test Result + + All Results + + {{ getResultIcon(result) }} + {{ result | titlecase }} + + + assignment_turned_in + + + + Tags + + label + Comma-separated tags + +
+
+ + +
+

Date Range

+
+ + Start Date + + + + + + + End Date + + + + +
+
+ + +
+ + Searching artifacts... +
+ + +
+ + +
+
+
+
+ + + + + + filter_list + Active Filters + + + + + + + description + Filename: {{ queryForm.filename }} + + + category + Type: {{ queryForm.file_type }} + + + quiz + Test: {{ queryForm.test_name }} + + + folder + Suite: {{ queryForm.test_suite }} + + + assignment_turned_in + Result: {{ queryForm.test_result }} + + + label + Tags: {{ tagsInput }} + + + + \ No newline at end of file diff --git a/frontend/src/app/components/query-form/query-form.component.scss b/frontend/src/app/components/query-form/query-form.component.scss new file mode 100644 index 0000000..d0c3486 --- /dev/null +++ b/frontend/src/app/components/query-form/query-form.component.scss @@ -0,0 +1,207 @@ +.query-card { + max-width: 900px; + margin: 0 auto 24px; +} + +.filters-card { + max-width: 900px; + margin: 0 auto; +} + +.query-form { + display: flex; + flex-direction: column; + gap: 24px; +} + +.form-section { + padding: 16px 0; + + h3 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 500; + color: #424242; + display: flex; + align-items: center; + gap: 8px; + + &::before { + content: ''; + width: 4px; + height: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 2px; + } + } + + &:not(:last-child) { + border-bottom: 1px solid #e0e0e0; + } +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.search-progress { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 16px; + background-color: #f5f5f5; + border-radius: 8px; + color: #666; + + mat-spinner { + margin: 0; + } + + span { + font-size: 14px; + font-weight: 500; + } +} + +.form-actions { + display: flex; + gap: 16px; + justify-content: center; + padding-top: 16px; + border-top: 1px solid #e0e0e0; + + .search-button { + padding: 12px 32px; + font-size: 16px; + + mat-icon { + margin-right: 8px; + } + } +} + +.filter-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + + mat-chip { + --mdc-chip-container-height: 36px; + + mat-icon[matChipAvatar] { + background-color: transparent !important; + color: currentColor !important; + } + } +} + +// Material Design overrides +:host ::ng-deep { + .mat-mdc-form-field { + width: 100%; + + .mat-mdc-form-field-hint { + font-size: 12px; + } + } + + .mat-mdc-select-panel { + max-height: 250px; + } + + .mat-mdc-option { + display: flex; + align-items: center; + gap: 8px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } + } + + .mat-mdc-chip { + font-size: 12px; + } + + .mat-mdc-raised-button { + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + + &:hover { + box-shadow: 0 4px 8px rgba(0,0,0,0.15); + } + } + + .mat-datepicker-toggle { + .mat-icon-button { + width: 32px; + height: 32px; + + .mat-mdc-button-touch-target { + width: 32px; + height: 32px; + } + } + } +} + +// Card title styling +:host ::ng-deep .mat-mdc-card-header { + .mat-mdc-card-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 20px; + font-weight: 500; + } + + .mat-mdc-card-subtitle { + margin-top: 4px; + color: #666; + } +} + +// Responsive design +@media (max-width: 768px) { + .query-card, + .filters-card { + margin: 0 16px 16px; + } + + .form-row { + grid-template-columns: 1fr; + } + + .form-actions { + flex-direction: column; + align-items: stretch; + + .search-button { + width: 100%; + } + } + + .filter-chips { + gap: 6px; + + mat-chip { + --mdc-chip-container-height: 32px; + font-size: 11px; + } + } +} + +@media (max-width: 480px) { + .query-card, + .filters-card { + margin: 0 8px 12px; + } + + .form-section h3 { + font-size: 14px; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/query-form/query-form.component.ts b/frontend/src/app/components/query-form/query-form.component.ts new file mode 100644 index 0000000..01f4a27 --- /dev/null +++ b/frontend/src/app/components/query-form/query-form.component.ts @@ -0,0 +1,137 @@ +import { Component, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatNativeDateModule } from '@angular/material/core'; +import { ArtifactQuery, Artifact } from '../../models/artifact.interface'; +import { ArtifactService } from '../../services/artifact.service'; +import { NotificationService } from '../../services/notification.service'; + +@Component({ + selector: 'app-query-form', + imports: [ + CommonModule, + FormsModule, + MatCardModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatButtonModule, + MatIconModule, + MatChipsModule, + MatProgressSpinnerModule, + MatDatepickerModule, + MatNativeDateModule + ], + templateUrl: './query-form.component.html', + styleUrl: './query-form.component.scss' +}) +export class QueryFormComponent { + @Output() queryResults = new EventEmitter(); + @Output() filtersChange = new EventEmitter(); + + queryForm: ArtifactQuery = { + filename: '', + file_type: '', + test_name: '', + test_suite: '', + test_result: '', + tags: [], + start_date: '', + end_date: '', + limit: 100, + offset: 0 + }; + + searching = false; + fileTypes = ['csv', 'json', 'binary', 'pcap']; + testResults = ['pass', 'fail', 'skip', 'error']; + + tagsInput = ''; + + constructor( + private artifactService: ArtifactService, + private notificationService: NotificationService + ) {} + + queryArtifacts(): void { + this.searching = true; + + const query: ArtifactQuery = { ...this.queryForm }; + + if (this.tagsInput) { + query.tags = this.tagsInput.split(',').map(t => t.trim()).filter(t => t); + } + + if (query.start_date) { + query.start_date = new Date(query.start_date).toISOString(); + } + + if (query.end_date) { + query.end_date = new Date(query.end_date).toISOString(); + } + + this.artifactService.queryArtifacts(query).subscribe({ + next: (artifacts) => { + this.queryResults.emit(artifacts); + this.searching = false; + }, + error: (error) => { + console.error('Query failed:', error); + this.notificationService.showError('Query failed: ' + error.message); + this.searching = false; + } + }); + } + + clearQuery(): void { + this.queryForm = { + filename: '', + file_type: '', + test_name: '', + test_suite: '', + test_result: '', + tags: [], + start_date: '', + end_date: '', + limit: 100, + offset: 0 + }; + this.tagsInput = ''; + this.emitFilters(); + } + + emitFilters(): void { + const filters = { + filename: this.queryForm.filename, + fileType: this.queryForm.file_type, + testName: this.queryForm.test_name, + testSuite: this.queryForm.test_suite, + testResult: this.queryForm.test_result, + tags: this.tagsInput ? this.tagsInput.split(',').map(t => t.trim()).filter(t => t) : [] + }; + this.filtersChange.emit(filters); + } + + onFilterChange(): void { + this.emitFilters(); + } + + getResultIcon(result: string): string { + switch (result) { + case 'pass': return 'check_circle'; + case 'fail': return 'cancel'; + case 'skip': return 'skip_next'; + case 'error': return 'error'; + default: return 'help'; + } + } +} diff --git a/frontend/src/app/components/tab-navigation/tab-navigation.component.html b/frontend/src/app/components/tab-navigation/tab-navigation.component.html new file mode 100644 index 0000000..27352cd --- /dev/null +++ b/frontend/src/app/components/tab-navigation/tab-navigation.component.html @@ -0,0 +1,30 @@ + + + + + + view_list + Artifacts + + + + + + + cloud_upload + Upload + + + + + + + search + Query + + + diff --git a/frontend/src/app/components/tab-navigation/tab-navigation.component.scss b/frontend/src/app/components/tab-navigation/tab-navigation.component.scss new file mode 100644 index 0000000..ad49c34 --- /dev/null +++ b/frontend/src/app/components/tab-navigation/tab-navigation.component.scss @@ -0,0 +1,44 @@ +.tab-icon { + margin-right: 8px; + font-size: 18px; + vertical-align: middle; +} + +:host ::ng-deep { + .mat-mdc-tab-group { + .mat-mdc-tab-header { + border-bottom: 1px solid #e0e0e0; + } + + .mat-mdc-tab { + min-width: 120px; + + .mat-mdc-tab-label { + display: flex; + align-items: center; + font-weight: 500; + } + } + + .mat-mdc-tab-body-wrapper { + display: none; + } + } +} + +@media (max-width: 768px) { + :host ::ng-deep { + .mat-mdc-tab { + min-width: 80px; + + .mat-mdc-tab-label { + font-size: 12px; + } + } + } + + .tab-icon { + font-size: 16px; + margin-right: 4px; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/tab-navigation/tab-navigation.component.ts b/frontend/src/app/components/tab-navigation/tab-navigation.component.ts new file mode 100644 index 0000000..7a5641e --- /dev/null +++ b/frontend/src/app/components/tab-navigation/tab-navigation.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatIconModule } from '@angular/material/icon'; + +export type TabType = 'artifacts' | 'upload' | 'query'; + +@Component({ + selector: 'app-tab-navigation', + imports: [ + CommonModule, + MatTabsModule, + MatIconModule + ], + templateUrl: './tab-navigation.component.html', + styleUrl: './tab-navigation.component.scss' +}) +export class TabNavigationComponent { + @Output() tabChange = new EventEmitter(); + + selectedIndex = 0; + + tabs = [ + { id: 'artifacts' as TabType, label: 'Artifacts', icon: 'view_list' }, + { id: 'upload' as TabType, label: 'Upload', icon: 'cloud_upload' }, + { id: 'query' as TabType, label: 'Query', icon: 'search' } + ]; + + onTabChange(event: any): void { + const selectedTab = this.tabs[event.index]; + this.tabChange.emit(selectedTab.id); + } +} diff --git a/frontend/src/app/components/tag-manager/tag-manager.component.html b/frontend/src/app/components/tag-manager/tag-manager.component.html new file mode 100644 index 0000000..a43e2d0 --- /dev/null +++ b/frontend/src/app/components/tag-manager/tag-manager.component.html @@ -0,0 +1,151 @@ +
+ +
+ + + label + {{ tag }} + cancel + + +
+ + +
+ +
+ + + + + + new_label + Add New Tag + + + + +
+ + + Tag Name + + label + + + +
+ + +
+ + Predefined Scope + + No scope + + {{ scope | titlecase }} + + + + + + Custom Scope + + category + +
+
+ + +
+ + +
+
+
+
+ + + + + + library_add + Quick Add Existing Tags + + + Click to add existing tags + + + + +
+

General Tags

+ + + {{ tag.name }} + check + + +
+ + +
+

{{ scope | titlecase }} Tags

+ + + {{ tag.name }} + check + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/tag-manager/tag-manager.component.scss b/frontend/src/app/components/tag-manager/tag-manager.component.scss new file mode 100644 index 0000000..e5c5ff9 --- /dev/null +++ b/frontend/src/app/components/tag-manager/tag-manager.component.scss @@ -0,0 +1,198 @@ +.tag-manager { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; +} + +.current-tags-section { + margin-bottom: 8px; + + mat-chip-set { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .current-tag { + background-color: #e3f2fd !important; + color: #1976d2 !important; + + mat-icon[matChipAvatar] { + background-color: #1976d2 !important; + color: white !important; + } + } +} + +.add-tag-section { + display: flex; + justify-content: center; + margin: 16px 0; + + .add-tag-fab { + width: 56px; + height: 56px; + } +} + +.add-tag-card { + max-width: 500px; + margin: 0 auto; + + mat-card-header { + margin-bottom: 16px; + + mat-card-title { + display: flex; + align-items: center; + gap: 8px; + } + } + + .tag-form { + display: flex; + flex-direction: column; + gap: 16px; + + .full-width { + width: 100%; + } + + .scope-section { + display: flex; + flex-direction: column; + gap: 12px; + + .scope-inputs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + } + } + + .form-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 16px; + } + } +} + +.available-tags-panel { + margin-top: 16px; + + .tag-group { + margin-bottom: 20px; + + h4 { + font-size: 14px; + font-weight: 500; + color: #424242; + margin: 0 0 12px 0; + display: flex; + align-items: center; + gap: 8px; + + &::before { + content: ''; + width: 4px; + height: 16px; + background-color: #2196f3; + border-radius: 2px; + } + } + + .available-chip-set { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .available-tag { + cursor: pointer; + transition: all 0.2s ease; + + &:not(.attached):hover { + background-color: #e8f5e8 !important; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + + &.attached { + background-color: #c8e6c9 !important; + color: #2e7d32 !important; + cursor: default; + opacity: 0.7; + } + + &.scoped { + border-left: 3px solid #ff9800; + } + } + } +} + +// Material Design overrides +:host ::ng-deep { + .mat-mdc-chip { + --mdc-chip-container-height: 32px; + font-size: 12px; + + &.current-tag { + --mdc-chip-with-avatar-container-height: 36px; + } + } + + .mat-mdc-chip-set { + margin: 0; + } + + .mat-mdc-fab { + box-shadow: 0 4px 8px rgba(0,0,0,0.15); + + &:hover { + box-shadow: 0 6px 12px rgba(0,0,0,0.2); + } + } + + .mat-expansion-panel-header { + font-weight: 500; + } + + .mat-expansion-panel-body { + padding: 16px 24px 24px; + } +} + +// Responsive design +@media (max-width: 768px) { + .tag-manager { + padding: 12px; + } + + .add-tag-card { + margin: 0 8px; + + .tag-form .scope-inputs { + grid-template-columns: 1fr; + } + } + + .tag-group { + .available-chip-set { + gap: 6px; + } + } +} + +@media (max-width: 480px) { + .add-tag-card .tag-form .form-actions { + flex-direction: column; + + button { + width: 100%; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/tag-manager/tag-manager.component.ts b/frontend/src/app/components/tag-manager/tag-manager.component.ts new file mode 100644 index 0000000..d2473ee --- /dev/null +++ b/frontend/src/app/components/tag-manager/tag-manager.component.ts @@ -0,0 +1,173 @@ +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatCardModule } from '@angular/material/card'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { Tag } from '../../models/artifact.interface'; +import { ArtifactService } from '../../services/artifact.service'; +import { NotificationService } from '../../services/notification.service'; + +@Component({ + selector: 'app-tag-manager', + imports: [ + CommonModule, + FormsModule, + MatChipsModule, + MatButtonModule, + MatIconModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatCardModule, + MatTooltipModule, + MatExpansionModule + ], + templateUrl: './tag-manager.component.html', + styleUrl: './tag-manager.component.scss' +}) +export class TagManagerComponent implements OnInit { + @Input() artifactId!: number; + @Input() currentTags: string[] = []; + @Output() tagsUpdated = new EventEmitter(); + + availableTags: Tag[] = []; + newTagName = ''; + newTagScope = ''; + showAddTag = false; + showScopeInput = false; + + predefinedScopes = ['project', 'environment', 'priority', 'category', 'status']; + + constructor( + private artifactService: ArtifactService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.loadAvailableTags(); + } + + loadAvailableTags(): void { + this.artifactService.getAllTags().subscribe({ + next: (tags) => { + this.availableTags = tags; + }, + error: (error) => { + // Tags endpoint not implemented yet - silently ignore + if (error.status === 404) { + console.log('Tags API not implemented yet'); + this.availableTags = []; + } else { + console.error('Error loading tags:', error); + } + } + }); + } + + toggleAddTag(): void { + this.showAddTag = !this.showAddTag; + if (!this.showAddTag) { + this.resetForm(); + } + } + + toggleScopeInput(): void { + this.showScopeInput = !this.showScopeInput; + } + + resetForm(): void { + this.newTagName = ''; + this.newTagScope = ''; + this.showScopeInput = false; + } + + addTag(): void { + if (!this.newTagName.trim()) return; + + const tag: Tag = { + name: this.newTagName.trim(), + scope: this.newTagScope.trim() || undefined + }; + + this.artifactService.createTag(tag).subscribe({ + next: (createdTag) => { + this.artifactService.addTag(this.artifactId, createdTag).subscribe({ + next: () => { + this.loadAvailableTags(); + this.tagsUpdated.emit(); + this.resetForm(); + this.showAddTag = false; + }, + error: (error) => { + console.error('Error adding tag to artifact:', error); + this.notificationService.showError('Error adding tag to artifact: ' + error.message); + } + }); + }, + error: (error) => { + console.error('Error creating tag:', error); + this.notificationService.showError('Error creating tag: ' + error.message); + } + }); + } + + removeTag(tag: string): void { + const tagToRemove = this.availableTags.find(t => t.name === tag); + if (!tagToRemove?.id) return; + + this.artifactService.removeTag(this.artifactId, tagToRemove.id).subscribe({ + next: () => { + this.tagsUpdated.emit(); + }, + error: (error) => { + console.error('Error removing tag:', error); + this.notificationService.showError('Error removing tag: ' + error.message); + } + }); + } + + addExistingTag(tag: Tag): void { + this.artifactService.addTag(this.artifactId, tag).subscribe({ + next: () => { + this.tagsUpdated.emit(); + }, + error: (error) => { + console.error('Error adding existing tag:', error); + this.notificationService.showError('Error adding tag: ' + error.message); + } + }); + } + + isTagAttached(tagName: string): boolean { + return this.currentTags.includes(tagName); + } + + getTagsByScope(scope?: string): Tag[] { + return this.availableTags.filter(tag => tag.scope === scope); + } + + getUniqueScopes(): string[] { + const scopes = this.availableTags + .map(tag => tag.scope) + .filter((scope, index, arr) => scope && arr.indexOf(scope) === index) as string[]; + return scopes.sort(); + } + + getTagColor(tag: Tag): string { + if (tag.color) return tag.color; + + const colors = ['#e0e7ff', '#fef3c7', '#d1fae5', '#fee2e2', '#f3e8ff', '#dbeafe']; + const hash = tag.name.split('').reduce((a, b) => { + a = ((a << 5) - a) + b.charCodeAt(0); + return a & a; + }, 0); + return colors[Math.abs(hash) % colors.length]; + } +} diff --git a/frontend/src/app/components/upload-form/upload-form.component.html b/frontend/src/app/components/upload-form/upload-form.component.html new file mode 100644 index 0000000..9c2e69a --- /dev/null +++ b/frontend/src/app/components/upload-form/upload-form.component.html @@ -0,0 +1,199 @@ + + + + cloud_upload + Upload Artifact + + + + +
+ +
+
+ + + + Select File + + + Supported: CSV, JSON, binary files, PCAP + +
+ +
+ + + description + {{ selectedFile.name }} + + + data_usage + {{ selectedFile.size | number }} bytes + + +
+
+ + +
+ + Event ID + + event + Groups multiple artifacts under the same event + + + + Test Name + + quiz + +
+ + +
+ + Test Suite + + category + + + + Test Result + + -- Select -- + + {{ getResultIcon(result) }} + {{ result | titlecase }} + + + assignment_turned_in + +
+ + +
+ + Version + + tag + + + + Associated Binaries + + code + Comma-separated list of binaries/files + +
+ + + + Tags + + label + Comma-separated tags + + + + + Description + + description + + + +
+

Advanced Configuration

+ + + Test Config (JSON) + + settings + + + + Custom Metadata (JSON) + + data_object + +
+ + + + + +
+ +
+
+ + +
+ + + + {{ uploadStatusType === 'success' ? 'check_circle' : 'error' }} + + {{ uploadStatus }} + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/upload-form/upload-form.component.scss b/frontend/src/app/components/upload-form/upload-form.component.scss new file mode 100644 index 0000000..5ca9e8c --- /dev/null +++ b/frontend/src/app/components/upload-form/upload-form.component.scss @@ -0,0 +1,124 @@ +.upload-card { + max-width: 800px; + margin: 0 auto; +} + +.upload-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.file-upload-section { + margin-bottom: 24px; + + .file-input-container { + position: relative; + } +} + +.selected-file-info { + margin-top: 12px; + + mat-chip-set { + gap: 8px; + } +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.full-width { + width: 100%; +} + +.json-section { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid #e0e0e0; + + h3 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 500; + color: #424242; + } +} + +.upload-progress { + margin: 16px 0; +} + +.form-actions { + display: flex; + justify-content: center; + margin-top: 24px; +} + +.upload-button { + padding: 12px 32px; + font-size: 16px; + + mat-icon { + margin-right: 8px; + } +} + +.status-section { + margin-top: 20px; + display: flex; + justify-content: center; + + mat-chip-set { + justify-content: center; + } + + .success { + background-color: #c8e6c9 !important; + color: #2e7d32 !important; + } + + .error { + background-color: #ffcdd2 !important; + color: #d32f2f !important; + } +} + +// Material Design overrides +:host ::ng-deep { + .mat-mdc-form-field { + width: 100%; + } + + .mat-mdc-form-field-hint { + font-size: 12px; + } + + .mat-mdc-chip { + --mdc-chip-container-height: 28px; + } + + .mat-mdc-text-field-wrapper { + background-color: transparent; + } +} + +// Responsive design +@media (max-width: 768px) { + .form-row { + grid-template-columns: 1fr; + } + + .upload-card { + margin: 16px; + } +} + +@media (max-width: 480px) { + .upload-button { + width: 100%; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/upload-form/upload-form.component.ts b/frontend/src/app/components/upload-form/upload-form.component.ts new file mode 100644 index 0000000..eeb5a7a --- /dev/null +++ b/frontend/src/app/components/upload-form/upload-form.component.ts @@ -0,0 +1,176 @@ +import { Component, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { ArtifactService } from '../../services/artifact.service'; + +@Component({ + selector: 'app-upload-form', + imports: [ + CommonModule, + FormsModule, + MatCardModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatButtonModule, + MatIconModule, + MatChipsModule, + MatProgressBarModule, + MatSnackBarModule + ], + templateUrl: './upload-form.component.html', + styleUrl: './upload-form.component.scss' +}) +export class UploadFormComponent { + @Output() uploadSuccess = new EventEmitter(); + + selectedFile: File | null = null; + uploading = false; + uploadStatus = ''; + uploadStatusType: 'success' | 'error' | '' = ''; + + formData = { + testName: '', + testSuite: '', + testResult: '', + version: '', + description: '', + tags: '', + testConfig: '', + customMetadata: '', + eventId: '', + binaries: '' + }; + + testResults = ['pass', 'fail', 'skip', 'error']; + + constructor(private artifactService: ArtifactService) {} + + onFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.selectedFile = file; + } + } + + resetForm(): void { + this.selectedFile = null; + this.formData = { + testName: '', + testSuite: '', + testResult: '', + version: '', + description: '', + tags: '', + testConfig: '', + customMetadata: '', + eventId: '', + binaries: '' + }; + + const fileInput = document.getElementById('file') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + } + + showUploadStatus(message: string, success: boolean): void { + this.uploadStatus = message; + this.uploadStatusType = success ? 'success' : 'error'; + + setTimeout(() => { + this.uploadStatus = ''; + this.uploadStatusType = ''; + }, 5000); + } + + uploadArtifact(): void { + if (!this.selectedFile) { + this.showUploadStatus('Please select a file to upload', false); + return; + } + + this.uploading = true; + + const formData = new FormData(); + formData.append('file', this.selectedFile); + + const fields = ['testName', 'testSuite', 'testResult', 'version', 'description', 'eventId']; + fields.forEach(field => { + const key = field === 'testName' ? 'test_name' : + field === 'testSuite' ? 'test_suite' : + field === 'testResult' ? 'test_result' : + field === 'eventId' ? 'event_id' : field; + + const value = this.formData[field as keyof typeof this.formData]; + if (value) { + formData.append(key, value); + } + }); + + if (this.formData.tags) { + const tagsArray = this.formData.tags.split(',').map(t => t.trim()).filter(t => t); + formData.append('tags', JSON.stringify(tagsArray)); + } + + if (this.formData.binaries) { + const binariesArray = this.formData.binaries.split(',').map(b => b.trim()).filter(b => b); + formData.append('binaries', JSON.stringify(binariesArray)); + } + + if (this.formData.testConfig) { + try { + JSON.parse(this.formData.testConfig); + formData.append('test_config', this.formData.testConfig); + } catch (e) { + this.showUploadStatus('Invalid Test Config JSON', false); + this.uploading = false; + return; + } + } + + if (this.formData.customMetadata) { + try { + JSON.parse(this.formData.customMetadata); + formData.append('custom_metadata', this.formData.customMetadata); + } catch (e) { + this.showUploadStatus('Invalid Custom Metadata JSON', false); + this.uploading = false; + return; + } + } + + this.artifactService.uploadArtifact(formData).subscribe({ + next: (response) => { + this.showUploadStatus(`Successfully uploaded: ${response.filename}`, true); + this.resetForm(); + this.uploadSuccess.emit(); + this.uploading = false; + }, + error: (error) => { + console.error('Upload error:', error); + this.showUploadStatus('Upload failed: ' + (error.error?.detail || error.message), false); + this.uploading = false; + } + }); + } + + getResultIcon(result: string): string { + switch (result) { + case 'pass': return 'check_circle'; + case 'fail': return 'cancel'; + case 'skip': return 'skip_next'; + case 'error': return 'error'; + default: return 'help'; + } + } +} diff --git a/frontend/src/app/models/artifact.interface.ts b/frontend/src/app/models/artifact.interface.ts new file mode 100644 index 0000000..612bd2b --- /dev/null +++ b/frontend/src/app/models/artifact.interface.ts @@ -0,0 +1,51 @@ +export interface Artifact { + id: number; + filename: string; + file_type: string; + file_size: number; + storage_path: string; + test_name?: string; + test_suite?: string; + test_result?: 'pass' | 'fail' | 'skip' | 'error'; + test_config?: any; + custom_metadata?: any; + description?: string; + tags: string[]; + version?: string; + created_at: string; + updated_at: string; + event_id?: string; + binaries?: string[]; +} + +export interface ArtifactQuery { + filename?: string; + file_type?: string; + test_name?: string; + test_suite?: string; + test_result?: string; + tags?: string[]; + start_date?: string; + end_date?: string; + limit?: number; + offset?: number; +} + +export interface ApiInfo { + deployment_mode: string; + storage_backend: string; +} + +export interface UploadResponse { + id: number; + filename: string; + message: string; +} + +export interface Tag { + id?: number; + name: string; + scope?: string; + color?: string; + created_at?: string; +} \ No newline at end of file diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts new file mode 100644 index 0000000..1aec64c --- /dev/null +++ b/frontend/src/app/services/api.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ApiInfo } from '../models/artifact.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class ApiService { + // Use relative URL - proxy will forward to backend + private readonly API_BASE = ''; + + constructor(private http: HttpClient) { } + + getApiInfo(): Observable { + return this.http.get(`${this.API_BASE}/api`); + } +} diff --git a/frontend/src/app/services/artifact.service.ts b/frontend/src/app/services/artifact.service.ts new file mode 100644 index 0000000..64b7234 --- /dev/null +++ b/frontend/src/app/services/artifact.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { Artifact, ArtifactQuery, UploadResponse, Tag } from '../models/artifact.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class ArtifactService { + // Use relative URL - proxy will forward to backend + private readonly API_BASE = '/api/v1'; + private artifactsSubject = new BehaviorSubject([]); + public artifacts$ = this.artifactsSubject.asObservable(); + + constructor(private http: HttpClient) { } + + getArtifacts(limit: number = 25, offset: number = 0): Observable { + const params = new HttpParams() + .set('limit', limit.toString()) + .set('offset', offset.toString()); + + return this.http.get(`${this.API_BASE}/artifacts/`, { params }); + } + + getArtifact(id: number): Observable { + return this.http.get(`${this.API_BASE}/artifacts/${id}`); + } + + uploadArtifact(formData: FormData): Observable { + return this.http.post(`${this.API_BASE}/artifacts/upload`, formData); + } + + deleteArtifact(id: number): Observable { + return this.http.delete(`${this.API_BASE}/artifacts/${id}`); + } + + downloadArtifact(id: number): Observable { + return this.http.get(`${this.API_BASE}/artifacts/${id}/download`, { responseType: 'blob' }); + } + + queryArtifacts(query: ArtifactQuery): Observable { + return this.http.post(`${this.API_BASE}/artifacts/query`, query); + } + + generateSeedData(count: number): Observable { + return this.http.post(`${this.API_BASE}/seed/generate/${count}`, {}); + } + + updateArtifactsCache(artifacts: Artifact[]): void { + this.artifactsSubject.next(artifacts); + } + + getCurrentArtifacts(): Artifact[] { + return this.artifactsSubject.value; + } + + addTag(artifactId: number, tag: Tag): Observable { + return this.http.post(`${this.API_BASE}/artifacts/${artifactId}/tags`, tag); + } + + removeTag(artifactId: number, tagId: number): Observable { + return this.http.delete(`${this.API_BASE}/artifacts/${artifactId}/tags/${tagId}`); + } + + getAllTags(): Observable { + return this.http.get(`${this.API_BASE}/tags`); + } + + createTag(tag: Tag): Observable { + return this.http.post(`${this.API_BASE}/tags`, tag); + } +} diff --git a/frontend/src/app/services/notification.service.ts b/frontend/src/app/services/notification.service.ts new file mode 100644 index 0000000..2c5d249 --- /dev/null +++ b/frontend/src/app/services/notification.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; + +@Injectable({ + providedIn: 'root' +}) +export class NotificationService { + + constructor(private snackBar: MatSnackBar) {} + + showSuccess(message: string, duration: number = 3000): void { + this.snackBar.open(message, 'Close', { + duration, + panelClass: ['success-snackbar'], + horizontalPosition: 'center', + verticalPosition: 'bottom' + }); + } + + showError(message: string, duration: number = 5000): void { + this.snackBar.open(message, 'Close', { + duration, + panelClass: ['error-snackbar'], + horizontalPosition: 'center', + verticalPosition: 'bottom' + }); + } + + showInfo(message: string, duration: number = 3000): void { + this.snackBar.open(message, 'Close', { + duration, + panelClass: ['info-snackbar'], + horizontalPosition: 'center', + verticalPosition: 'bottom' + }); + } + + showWarning(message: string, duration: number = 4000): void { + this.snackBar.open(message, 'Close', { + duration, + panelClass: ['warning-snackbar'], + horizontalPosition: 'center', + verticalPosition: 'bottom' + }); + } + + showConfirmation(message: string, action: string = 'Confirm'): Promise { + const snackBarRef = this.snackBar.open(message, action, { + duration: 10000, + panelClass: ['confirmation-snackbar'], + horizontalPosition: 'center', + verticalPosition: 'bottom' + }); + + return new Promise((resolve) => { + snackBarRef.onAction().subscribe(() => resolve(true)); + snackBarRef.afterDismissed().subscribe((info) => { + if (!info.dismissedByAction) { + resolve(false); + } + }); + }); + } +} \ No newline at end of file diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 0000000..d4f82fb --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,15 @@ + + + + + Frontend + + + + + + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..35b00f3 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/static/css/styles.css b/frontend/src/styles.scss similarity index 58% rename from static/css/styles.css rename to frontend/src/styles.scss index 2a73e1e..d83a444 100644 --- a/static/css/styles.css +++ b/frontend/src/styles.scss @@ -1,3 +1,5 @@ +/* Global styles for the Test Artifact Data Lake Angular app with Material Design */ + * { margin: 0; padding: 0; @@ -5,8 +7,8 @@ } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + font-family: Roboto, "Helvetica Neue", sans-serif; + background: linear-gradient(135deg, #3f51b5 0%, #9c27b0 100%); min-height: 100vh; padding: 20px; } @@ -141,6 +143,11 @@ header h1 { font-size: 16px; } +.btn-sm { + padding: 6px 12px; + font-size: 12px; +} + .count-badge { background: #f0f9ff; color: #0369a1; @@ -227,6 +234,36 @@ tbody tr:hover { border-radius: 10px; font-size: 11px; margin: 2px; + cursor: pointer; + transition: all 0.2s; +} + +.tag:hover { + background: #c7d2fe; +} + +.tag.removable { + background: #f87171; + color: white; +} + +.tag.removable:hover { + background: #ef4444; +} + +.tag.available { + background: #f3f4f6; + color: #374151; + border: 1px solid #d1d5db; +} + +.tag.available:hover:not(.attached) { + background: #e5e7eb; +} + +.tag.attached { + opacity: 0.5; + cursor: not-allowed; } .file-type-badge { @@ -268,6 +305,12 @@ tbody tr:hover { gap: 20px; } +.form-actions { + display: flex; + gap: 10px; + align-items: center; +} + label { display: block; font-weight: 500; @@ -305,25 +348,34 @@ small { margin-top: 4px; } -#upload-status { +.upload-status { margin-top: 20px; padding: 14px; border-radius: 6px; display: none; } -#upload-status.success { +.upload-status.success { background: #d1fae5; color: #065f46; display: block; } -#upload-status.error { +.upload-status.error { background: #fee2e2; color: #991b1b; display: block; } +.selected-file { + margin-top: 8px; + padding: 8px; + background: #f0f9ff; + border-radius: 4px; + font-size: 12px; + color: #0369a1; +} + .modal { display: none; position: fixed; @@ -415,6 +467,194 @@ pre { background: #e2e8f0; } +.filename-link { + color: #667eea; + text-decoration: none; +} + +.filename-link:hover { + text-decoration: underline; +} + +/* Binaries and Tags cells */ +.binaries-cell, .tags-cell { + max-width: 200px; +} + +.binaries-list, .tags-list { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: center; +} + +.binary-item { + background: #f3f4f6; + color: #374151; + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + font-family: monospace; +} + +.expand-btn { + background: #e5e7eb; + color: #374151; + border: none; + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + cursor: pointer; + transition: background 0.2s; +} + +.expand-btn:hover { + background: #d1d5db; +} + +.expanded-binaries, .expanded-tags { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +/* Tag Manager */ +.tag-manager { + position: relative; + display: inline-block; +} + +.current-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 8px; +} + +.add-tag-btn { + background: #667eea; + color: white; + border: none; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.add-tag-btn:hover { + background: #5568d3; +} + +.add-tag-btn.active { + background: #ef4444; +} + +.add-tag-form { + position: absolute; + top: 100%; + left: 0; + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + z-index: 100; + min-width: 300px; +} + +.tag-input, .scope-select, .scope-custom-input { + width: 100%; + padding: 8px; + border: 1px solid #e2e8f0; + border-radius: 4px; + font-size: 12px; + margin-bottom: 8px; +} + +.scope-toggle-btn { + background: #f3f4f6; + color: #374151; + border: none; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + cursor: pointer; + margin-bottom: 8px; +} + +.scope-input { + display: flex; + gap: 8px; +} + +.available-tags { + margin-top: 16px; + max-height: 200px; + overflow-y: auto; +} + +.available-tags h4 { + font-size: 12px; + color: #475569; + margin-bottom: 8px; +} + +.tag-group { + margin-bottom: 12px; +} + +.tag-group h5 { + font-size: 11px; + color: #6b7280; + margin-bottom: 4px; +} + +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.attached-indicator { + margin-left: 4px; + color: #10b981; +} + +/* Filter chips */ +.filter-info { + margin-top: 20px; + padding: 16px; + background: #f7f9fc; + border-radius: 8px; +} + +.filter-info h4 { + font-size: 14px; + color: #475569; + margin-bottom: 8px; +} + +.active-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.filter-chip { + background: #667eea; + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; +} + @media (max-width: 768px) { .form-row { grid-template-columns: 1fr; @@ -433,4 +673,37 @@ pre { th, td { padding: 8px 6px; } + + .binaries-cell, .tags-cell { + max-width: 150px; + } +} + +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } + +/* Material Snackbar Styles */ +.success-snackbar { + background-color: #4caf50 !important; + color: white !important; +} + +.error-snackbar { + background-color: #f44336 !important; + color: white !important; +} + +.info-snackbar { + background-color: #2196f3 !important; + color: white !important; +} + +.warning-snackbar { + background-color: #ff9800 !important; + color: white !important; +} + +.confirmation-snackbar { + background-color: #673ab7 !important; + color: white !important; } diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..3775b37 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..5525117 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json new file mode 100644 index 0000000..5fb748d --- /dev/null +++ b/frontend/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..47d6474 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,76 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + # Enable gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + server { + listen 80; + server_name localhost; + + # Serve Angular app + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to backend + location /api/ { + proxy_pass http://api:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Proxy docs requests to backend + location /docs { + proxy_pass http://api:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy redoc requests to backend + location /redoc { + proxy_pass http://api:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy health check to backend + location /health { + proxy_pass http://api:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Error pages + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +} \ No newline at end of file diff --git a/quickstart.bat b/quickstart.bat deleted file mode 100644 index be8fc12..0000000 --- a/quickstart.bat +++ /dev/null @@ -1,106 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -echo ========================================= -echo Test Artifact Data Lake - 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 Starting services with Docker Compose... -%COMPOSE_CMD% up -d - -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 diff --git a/quickstart.ps1 b/quickstart.ps1 deleted file mode 100644 index 09373cb..0000000 --- a/quickstart.ps1 +++ /dev/null @@ -1,129 +0,0 @@ -# Test Artifact Data Lake - Quick Start (PowerShell) - -Write-Host "=========================================" -ForegroundColor Cyan -Write-Host "Test Artifact Data Lake - Quick Start" -ForegroundColor Cyan -Write-Host "=========================================" -ForegroundColor Cyan -Write-Host "" - -# Check if Docker is installed -try { - $dockerVersion = docker --version - Write-Host "[OK] Docker found: $dockerVersion" -ForegroundColor Green -} catch { - Write-Host "[ERROR] Docker is not installed." -ForegroundColor Red - Write-Host "Please install Docker Desktop first:" -ForegroundColor Yellow - Write-Host "https://www.docker.com/products/docker-desktop" -ForegroundColor Yellow - Read-Host "Press Enter to exit" - exit 1 -} - -# Determine Docker Compose command -$composeCmd = "docker-compose" -try { - docker-compose version | Out-Null -} catch { - # Try new docker compose syntax - try { - docker compose version | Out-Null - $composeCmd = "docker compose" - } catch { - Write-Host "[ERROR] Docker Compose is not available." -ForegroundColor Red - Write-Host "Please ensure Docker Desktop is running." -ForegroundColor Yellow - Read-Host "Press Enter to exit" - exit 1 - } -} - -Write-Host "[OK] Using: $composeCmd" -ForegroundColor Green - -# Create .env file if it doesn't exist -if (-Not (Test-Path ".env")) { - Write-Host "Creating .env file from .env.example..." -ForegroundColor Yellow - Copy-Item .env.example .env - Write-Host "[OK] .env file created" -ForegroundColor Green -} else { - Write-Host "[OK] .env file already exists" -ForegroundColor Green -} - -Write-Host "" -Write-Host "Starting services with Docker Compose..." -ForegroundColor Yellow - -# Start services -if ($composeCmd -eq "docker-compose") { - docker-compose up -d -} else { - docker compose up -d -} - -if ($LASTEXITCODE -ne 0) { - Write-Host "" - Write-Host "[ERROR] Failed to start services." -ForegroundColor Red - Write-Host "Make sure Docker Desktop is running." -ForegroundColor Yellow - Read-Host "Press Enter to exit" - exit 1 -} - -Write-Host "" -Write-Host "Waiting for services to be ready..." -ForegroundColor Yellow -Start-Sleep -Seconds 15 - -Write-Host "" -Write-Host "=========================================" -ForegroundColor Cyan -Write-Host "Services are running!" -ForegroundColor Green -Write-Host "=========================================" -ForegroundColor Cyan -Write-Host "" -Write-Host "Web UI: " -NoNewline -Write-Host "http://localhost:8000" -ForegroundColor Yellow -Write-Host "API Docs: " -NoNewline -Write-Host "http://localhost:8000/docs" -ForegroundColor Yellow -Write-Host "MinIO Console: " -NoNewline -Write-Host "http://localhost:9001" -ForegroundColor Yellow -Write-Host " Username: minioadmin" -Write-Host " Password: minioadmin" -Write-Host "" -Write-Host "To view logs: $composeCmd logs -f" -ForegroundColor Cyan -Write-Host "To stop: $composeCmd down" -ForegroundColor Cyan -Write-Host "" -Write-Host "=========================================" -ForegroundColor Cyan -Write-Host "Testing the API..." -ForegroundColor Yellow -Write-Host "=========================================" -ForegroundColor Cyan -Write-Host "" - -# Wait a bit more for API -Start-Sleep -Seconds 5 - -# Test health endpoint -try { - $response = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 5 - if ($response.Content -like "*healthy*") { - Write-Host "[OK] API is healthy!" -ForegroundColor Green - Write-Host "" - Write-Host "=========================================" -ForegroundColor Cyan - Write-Host "Opening browser..." -ForegroundColor Yellow - Write-Host "http://localhost:8000" -ForegroundColor Yellow - Write-Host "=========================================" -ForegroundColor Cyan - - # Open browser - Start-Process "http://localhost:8000" - } -} catch { - Write-Host "[WARNING] API is not responding yet." -ForegroundColor Yellow - Write-Host "Please wait a moment and check http://localhost:8000" -ForegroundColor Yellow -} - -Write-Host "" -Write-Host "=========================================" -ForegroundColor Cyan -Write-Host "Setup complete! " -NoNewline -Write-Host "🚀" -ForegroundColor Green -Write-Host "=========================================" -ForegroundColor Cyan -Write-Host "" -Write-Host "Useful Commands:" -ForegroundColor Cyan -Write-Host " Generate seed data: " -NoNewline -Write-Host "Use the 'Generate Seed Data' button in the UI" -ForegroundColor Yellow -Write-Host " View logs: " -NoNewline -Write-Host "$composeCmd logs -f api" -ForegroundColor Yellow -Write-Host " Restart services: " -NoNewline -Write-Host "$composeCmd restart" -ForegroundColor Yellow -Write-Host " Stop all: " -NoNewline -Write-Host "$composeCmd down" -ForegroundColor Yellow -Write-Host "" diff --git a/scripts/dev-start.ps1 b/scripts/dev-start.ps1 new file mode 100644 index 0000000..a5dd219 --- /dev/null +++ b/scripts/dev-start.ps1 @@ -0,0 +1,156 @@ +[CmdletBinding()] +param( + [switch]$Help +) + +$ErrorActionPreference = "Stop" + +if ($Help) { + Write-Host "=========================================" -ForegroundColor Cyan + Write-Host "Test Artifact Data Lake - Development Setup" -ForegroundColor Cyan + Write-Host "=========================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Usage: .\dev-start.ps1 [OPTIONS]" -ForegroundColor White + Write-Host "" + Write-Host "Options:" -ForegroundColor Yellow + Write-Host " -Help Show this help message" -ForegroundColor White + Write-Host "" + Write-Host "This script starts backend services and frontend development server." -ForegroundColor Green + Write-Host "" + exit 0 +} + +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "Test Artifact Data Lake - Development Setup" -ForegroundColor Cyan +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "" + +# Check if Node.js is installed +if (-not (Get-Command "node" -ErrorAction SilentlyContinue)) { + Write-Host "Error: Node.js is not installed. Please install Node.js 18+ first." -ForegroundColor Red + Write-Host "Visit: https://nodejs.org/" -ForegroundColor Yellow + Read-Host "Press Enter to exit" + exit 1 +} + +# Check Node.js version +try { + $nodeVersion = & node --version + $majorVersion = [int]($nodeVersion -replace 'v(\d+)\..*', '$1') + if ($majorVersion -lt 18) { + Write-Host "Error: Node.js version 18 or higher is required. Current version: $nodeVersion" -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 1 + } + Write-Host "[OK] Node.js version: $nodeVersion" -ForegroundColor Green +} +catch { + Write-Host "Error: Failed to check Node.js version" -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 1 +} + +# Check if npm is installed +if (-not (Get-Command "npm" -ErrorAction SilentlyContinue)) { + Write-Host "Error: npm is not installed. Please install npm first." -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 1 +} + +$npmVersion = & npm --version +Write-Host "[OK] npm version: $npmVersion" -ForegroundColor Green + +# Check if Docker is installed +if (-not (Get-Command "docker" -ErrorAction SilentlyContinue)) { + Write-Host "Error: Docker is not installed. Please install Docker Desktop first." -ForegroundColor Red + Write-Host "Visit: https://www.docker.com/products/docker-desktop" -ForegroundColor Yellow + Read-Host "Press Enter to exit" + exit 1 +} + +# Check if Docker Compose is available +$ComposeCmd = $null +if (Get-Command "docker-compose" -ErrorAction SilentlyContinue) { + $ComposeCmd = "docker-compose" +} else { + try { + & docker compose version | Out-Null + $ComposeCmd = "docker compose" + } + catch { + Write-Host "Error: Docker Compose is not available." -ForegroundColor Red + Write-Host "Please ensure Docker Desktop is running." -ForegroundColor Yellow + Read-Host "Press Enter to exit" + exit 1 + } +} + +Write-Host "[OK] Docker Compose command: $ComposeCmd" -ForegroundColor Green +Write-Host "" + +# Create .env file if it doesn't exist +if (-not (Test-Path ".env")) { + Write-Host "Creating .env file from .env.example..." -ForegroundColor Yellow + Copy-Item ".env.example" ".env" + Write-Host "[OK] .env file created" -ForegroundColor Green +} else { + Write-Host "[OK] .env file already exists" -ForegroundColor Green +} + +Write-Host "" +Write-Host "Starting backend services (PostgreSQL, MinIO, API)..." -ForegroundColor Yellow + +# Start backend services +try { + if ($ComposeCmd -eq "docker compose") { + & docker compose up -d postgres minio api + } else { + & docker-compose up -d postgres minio api + } +} +catch { + Write-Host "Error: Failed to start backend services." -ForegroundColor Red + Write-Host "Make sure Docker Desktop is running." -ForegroundColor Yellow + Read-Host "Press Enter to exit" + exit 1 +} + +Write-Host "" +Write-Host "Waiting for backend services to be ready..." -ForegroundColor Yellow +Start-Sleep -Seconds 10 + +Write-Host "" +Write-Host "Installing frontend dependencies..." -ForegroundColor Yellow +Set-Location "frontend" + +try { + & npm install + Write-Host "[OK] Frontend dependencies installed" -ForegroundColor Green +} +catch { + Write-Host "Error: Failed to install frontend dependencies" -ForegroundColor Red + Set-Location ".." + Read-Host "Press Enter to exit" + exit 1 +} + +Write-Host "" +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "Development Environment Ready!" -ForegroundColor Green +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Backend API: http://localhost:8000" -ForegroundColor White +Write-Host "API Docs: http://localhost:8000/docs" -ForegroundColor White +Write-Host "MinIO Console: http://localhost:9001" -ForegroundColor White +Write-Host " Username: minioadmin" -ForegroundColor Gray +Write-Host " Password: minioadmin" -ForegroundColor Gray +Write-Host "" +Write-Host "Frontend will be available at: http://localhost:4200" -ForegroundColor White +Write-Host "" +Write-Host "To view backend logs: $ComposeCmd logs -f api" -ForegroundColor Yellow +Write-Host "To stop backend: $ComposeCmd down" -ForegroundColor Yellow +Write-Host "" +Write-Host "Starting frontend development server..." -ForegroundColor Green + +# Start the frontend development server +& npm run start \ No newline at end of file diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh new file mode 100644 index 0000000..dd86542 --- /dev/null +++ b/scripts/dev-start.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +set -e + +echo "=========================================" +echo "Test Artifact Data Lake - Development Setup" +echo "=========================================" +echo "" + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "Error: Node.js is not installed. Please install Node.js 18+ first." + exit 1 +fi + +# Check Node.js version +NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1) +if [ "$NODE_VERSION" -lt "18" ]; then + echo "Error: Node.js version 18 or higher is required. Current version: $(node --version)" + exit 1 +fi + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + echo "Error: npm is not installed. Please install npm first." + exit 1 +fi + +# Check if Docker is installed for backend services +if ! command -v docker &> /dev/null; then + echo "Error: Docker is not installed. Please install Docker first." + exit 1 +fi + +# Check if Docker Compose is installed +if ! command -v docker-compose &> /dev/null; then + echo "Error: Docker Compose is not installed. Please install Docker Compose first." + exit 1 +fi + +echo "✓ Node.js version: $(node --version)" +echo "✓ npm version: $(npm --version)" +echo "✓ Docker version: $(docker --version)" +echo "" + +# Create .env file if it doesn't exist +if [ ! -f .env ]; then + echo "Creating .env file from .env.example..." + cp .env.example .env + echo "✓ .env file created" +else + echo "✓ .env file already exists" +fi + +echo "" +echo "Starting backend services (PostgreSQL, MinIO, API)..." +docker-compose up -d postgres minio api + +echo "" +echo "Waiting for backend services to be ready..." +sleep 10 + +echo "" +echo "Installing frontend dependencies..." +cd frontend +npm install + +echo "" +echo "=========================================" +echo "Development Environment Ready!" +echo "=========================================" +echo "" +echo "Backend API: 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 start the frontend development server:" +echo " cd frontend" +echo " npm run start" +echo "" +echo "Frontend will be available at: http://localhost:4200" +echo "" +echo "To view backend logs: docker-compose logs -f api" +echo "To stop backend: docker-compose down" +echo "" +echo "Starting frontend development server..." +npm run start \ No newline at end of file diff --git a/quickstart.sh b/scripts/quickstart-build.sh old mode 100755 new mode 100644 similarity index 53% rename from quickstart.sh rename to scripts/quickstart-build.sh index e963763..3d0fb9a --- a/quickstart.sh +++ b/scripts/quickstart-build.sh @@ -3,10 +3,16 @@ set -e echo "=========================================" -echo "Test Artifact Data Lake - Quick Start" +echo "Test Artifact Data Lake - Production Build" echo "=========================================" echo "" +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "Error: Node.js is not installed. Please install Node.js 18+ first." + exit 1 +fi + # Check if Docker is installed if ! command -v docker &> /dev/null; then echo "Error: Docker is not installed. Please install Docker first." @@ -29,52 +35,71 @@ else fi echo "" -echo "Starting services with Docker Compose..." -docker-compose up -d +echo "=========================================" +echo "Building Angular Frontend..." +echo "=========================================" +echo "" + +cd frontend + +# Install dependencies if node_modules doesn't exist +if [ ! -d "node_modules" ]; then + echo "Installing frontend dependencies..." + npm install +fi + +# Build the Angular app for production +echo "Building Angular app for production..." +npm run build + +if [ $? -ne 0 ]; then + echo "Error: Frontend build failed. Please check the errors above." + exit 1 +fi + +echo "✓ Frontend built successfully" + +cd .. + +echo "" +echo "=========================================" +echo "Starting Docker Services..." +echo "=========================================" +echo "" + +docker-compose -f docker-compose.production.yml up -d echo "" echo "Waiting for services to be ready..." -sleep 10 +sleep 15 echo "" echo "=========================================" echo "Services are running!" echo "=========================================" echo "" +echo "Frontend (UI): http://localhost:80" echo "API: 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: docker-compose logs -f" -echo "To stop: docker-compose down" +echo "To view logs: docker-compose -f docker-compose.production.yml logs -f" +echo "To stop: docker-compose -f docker-compose.production.yml down" echo "" -echo "=========================================" -echo "Testing the API..." -echo "=========================================" +echo "NOTE: The main application UI is now available at http://localhost:80" +echo " This is an Angular application with Material Design components." echo "" -# Wait a bit more for API to be fully ready -sleep 5 - # Test health endpoint +sleep 5 if curl -s http://localhost:8000/health | grep -q "healthy"; then echo "✓ API is healthy!" echo "" - echo "Example: Upload a test file" - echo "----------------------------" - echo 'echo "test,data" > test.csv' - echo 'curl -X POST "http://localhost:8000/api/v1/artifacts/upload" \' - echo ' -F "file=@test.csv" \' - echo ' -F "test_name=sample_test" \' - echo ' -F "test_suite=demo" \' - echo ' -F "test_result=pass"' - echo "" + echo "=========================================" + echo "Setup complete! 🚀" + echo "=========================================" else - echo "⚠ API is not responding yet. Please wait a moment and check http://localhost:8000/health" -fi - -echo "=========================================" -echo "Setup complete! 🚀" -echo "=========================================" + echo "⚠ API is not responding yet. Please wait a moment and check http://localhost:80" +fi \ No newline at end of file diff --git a/scripts/quickstart.ps1 b/scripts/quickstart.ps1 new file mode 100644 index 0000000..6b8568e --- /dev/null +++ b/scripts/quickstart.ps1 @@ -0,0 +1,213 @@ +[CmdletBinding()] +param( + [switch]$Rebuild, + [switch]$FullStack, + [switch]$Dev, + [switch]$Help +) + +$ErrorActionPreference = "Stop" + +if ($Help) { + Write-Host "=========================================" -ForegroundColor Cyan + Write-Host "Test Artifact Data Lake - Quick Start" -ForegroundColor Cyan + Write-Host "=========================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Usage: .\quickstart.ps1 [OPTIONS]" -ForegroundColor White + Write-Host "" + Write-Host "Options:" -ForegroundColor Yellow + Write-Host " -Rebuild Force rebuild of all containers" -ForegroundColor White + Write-Host " -FullStack Start complete stack including frontend (production build)" -ForegroundColor White + Write-Host " -Dev Start backend only, use separate dev server for frontend" -ForegroundColor White + Write-Host " -Help Show this help message" -ForegroundColor White + Write-Host "" + Write-Host "Default: Starts backend services only (recommended for development)" -ForegroundColor Green + Write-Host "" + exit 0 +} + +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "Test Artifact Data Lake - Quick Start" -ForegroundColor Cyan +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "" + +# Check if Docker is installed +if (-not (Get-Command "docker" -ErrorAction SilentlyContinue)) { + Write-Host "Error: Docker is not installed. Please install Docker Desktop first." -ForegroundColor Red + Write-Host "Visit: https://www.docker.com/products/docker-desktop" -ForegroundColor Yellow + Read-Host "Press Enter to exit" + exit 1 +} + +# Check if Docker Compose is available +$ComposeCmd = $null +if (Get-Command "docker-compose" -ErrorAction SilentlyContinue) { + $ComposeCmd = "docker-compose" +} else { + try { + & docker compose version | Out-Null + $ComposeCmd = "docker compose" + } + catch { + Write-Host "Error: Docker Compose is not available." -ForegroundColor Red + Write-Host "Please ensure Docker Desktop is running." -ForegroundColor Yellow + Read-Host "Press Enter to exit" + exit 1 + } +} + +# Create .env file if it doesn't exist +if (-not (Test-Path ".env")) { + Write-Host "Creating .env file from .env.example..." -ForegroundColor Yellow + Copy-Item ".env.example" ".env" + Write-Host "[OK] .env file created" -ForegroundColor Green +} else { + Write-Host "[OK] .env file already exists" -ForegroundColor Green +} + +Write-Host "" + +# Handle rebuild logic +if ($Rebuild) { + Write-Host "Rebuilding containers..." -ForegroundColor Yellow + Write-Host "Stopping existing containers..." -ForegroundColor White + + if ($FullStack) { + if ($ComposeCmd -eq "docker compose") { + & docker compose -f "docker-compose.production.yml" down + Write-Host "Removing existing images for full rebuild..." -ForegroundColor White + & docker compose -f "docker-compose.production.yml" down --rmi local 2>$null + Write-Host "Building and starting full stack..." -ForegroundColor White + & docker compose -f "docker-compose.production.yml" up -d --build + } else { + & docker-compose -f "docker-compose.production.yml" down + Write-Host "Removing existing images for full rebuild..." -ForegroundColor White + & docker-compose -f "docker-compose.production.yml" down --rmi local 2>$null + Write-Host "Building and starting full stack..." -ForegroundColor White + & docker-compose -f "docker-compose.production.yml" up -d --build + } + } else { + if ($ComposeCmd -eq "docker compose") { + & docker compose down + Write-Host "Removing existing backend images for rebuild..." -ForegroundColor White + & docker compose down --rmi local 2>$null + Write-Host "Building and starting backend services..." -ForegroundColor White + & docker compose up -d --build + } else { + & docker-compose down + Write-Host "Removing existing backend images for rebuild..." -ForegroundColor White + & docker-compose down --rmi local 2>$null + Write-Host "Building and starting backend services..." -ForegroundColor White + & docker-compose up -d --build + } + } +} else { + # Regular startup + if ($FullStack) { + Write-Host "Starting full production stack (backend + frontend)..." -ForegroundColor Green + if ($ComposeCmd -eq "docker compose") { + & docker compose -f "docker-compose.production.yml" up -d + } else { + & docker-compose -f "docker-compose.production.yml" up -d + } + } else { + Write-Host "Starting backend services..." -ForegroundColor Green + if ($ComposeCmd -eq "docker compose") { + & docker compose up -d + } else { + & docker-compose up -d + } + } +} + +Write-Host "" +Write-Host "Waiting for services to be ready..." -ForegroundColor Yellow +Start-Sleep -Seconds 15 + +Write-Host "" +Write-Host "=========================================" -ForegroundColor Cyan +if ($FullStack) { + Write-Host "Complete Stack is running!" -ForegroundColor Green +} else { + Write-Host "Backend Services are running!" -ForegroundColor Green +} +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "API: http://localhost:8000" -ForegroundColor White +Write-Host "API Docs: http://localhost:8000/docs" -ForegroundColor White +Write-Host "MinIO Console: http://localhost:9001" -ForegroundColor White +Write-Host " Username: minioadmin" -ForegroundColor Gray +Write-Host " Password: minioadmin" -ForegroundColor Gray +Write-Host "" + +if ($FullStack) { + Write-Host "Frontend: http://localhost:80" -ForegroundColor White + Write-Host " (Production build with Nginx)" -ForegroundColor Gray + Write-Host "" + Write-Host "To view logs: $ComposeCmd -f docker-compose.production.yml logs -f" -ForegroundColor Yellow + Write-Host "To stop: $ComposeCmd -f docker-compose.production.yml down" -ForegroundColor Yellow +} else { + Write-Host "To view logs: $ComposeCmd logs -f" -ForegroundColor Yellow + Write-Host "To stop: $ComposeCmd down" -ForegroundColor Yellow + Write-Host "" + Write-Host "=========================================" -ForegroundColor Cyan + Write-Host "Frontend Options:" -ForegroundColor Cyan + Write-Host "=========================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Development (with hot reload):" -ForegroundColor White + Write-Host " .\dev-start.ps1" -ForegroundColor Green + Write-Host " Frontend will be available at: http://localhost:4200" -ForegroundColor Gray + Write-Host "" + Write-Host "Or start full production stack:" -ForegroundColor White + Write-Host " .\quickstart.ps1 -FullStack" -ForegroundColor Green + Write-Host " Complete stack at: http://localhost:80" -ForegroundColor Gray +} + +Write-Host "" +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "Rebuild Options:" -ForegroundColor Cyan +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Force rebuild backend: .\quickstart.ps1 -Rebuild" -ForegroundColor Yellow +Write-Host "Force rebuild full stack: .\quickstart.ps1 -Rebuild -FullStack" -ForegroundColor Yellow +Write-Host "" +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "Testing the API..." -ForegroundColor Cyan +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "" + +# Wait a bit more for API to be fully ready +Start-Sleep -Seconds 5 + +# Test health endpoint +try { + $response = Invoke-RestMethod -Uri "http://localhost:8000/health" -Method Get -TimeoutSec 10 + if ($response.status -eq "healthy") { + Write-Host "[OK] API is healthy!" -ForegroundColor Green + Write-Host "" + if (-not $FullStack) { + Write-Host "Ready for development!" -ForegroundColor Green + Write-Host "Run '.\dev-start.ps1' to start the frontend with hot reload" -ForegroundColor Yellow + Write-Host "" + } + Write-Host "Example: Upload a test file" -ForegroundColor White + Write-Host "----------------------------" -ForegroundColor Gray + Write-Host 'echo "test,data" > test.csv' -ForegroundColor Green + Write-Host 'curl -X POST "http://localhost:8000/api/v1/artifacts/upload"' -ForegroundColor Green + Write-Host ' -F "file=@test.csv"' -ForegroundColor Green + Write-Host ' -F "test_name=sample_test"' -ForegroundColor Green + Write-Host ' -F "test_suite=demo"' -ForegroundColor Green + Write-Host ' -F "test_result=pass"' -ForegroundColor Green + Write-Host "" + } else { + Write-Host "[WARNING] API returned unexpected status. Please check http://localhost:8000/health" -ForegroundColor Yellow + } +} +catch { + Write-Host "[WARNING] API is not responding yet. Please wait a moment and check http://localhost:8000/health" -ForegroundColor Yellow +} + +Write-Host "" +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host "Setup complete!" -ForegroundColor Green +Write-Host "=========================================" -ForegroundColor Cyan \ No newline at end of file diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh new file mode 100644 index 0000000..b83fd7e --- /dev/null +++ b/scripts/quickstart.sh @@ -0,0 +1,189 @@ +#!/bin/bash + +set -e + +echo "=========================================" +echo "Test Artifact Data Lake - Quick Start" +echo "=========================================" +echo "" + +# Check if Docker is installed +if ! command -v docker &> /dev/null; then + echo "Error: Docker is not installed. Please install Docker first." + exit 1 +fi + +# Check if Docker Compose is installed +COMPOSE_CMD="" +if command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" +elif docker compose version &> /dev/null; then + COMPOSE_CMD="docker compose" +else + echo "Error: Docker Compose is not installed. Please install Docker Compose first." + exit 1 +fi + +# Parse command line arguments +REBUILD=false +FULL_STACK=false +DEV_MODE=false + +while [[ $# -gt 0 ]]; do + case $1 in + --rebuild) + REBUILD=true + shift + ;; + --full-stack) + FULL_STACK=true + shift + ;; + --dev) + DEV_MODE=true + shift + ;; + --help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --rebuild Force rebuild of all containers" + echo " --full-stack Start complete stack including frontend (production build)" + echo " --dev Start backend only, use separate dev server for frontend" + echo " --help Show this help message" + echo "" + echo "Default: Starts backend services only (recommended for development)" + echo "" + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Create .env file if it doesn't exist +if [ ! -f .env ]; then + echo "Creating .env file from .env.example..." + cp .env.example .env + echo "✓ .env file created" +else + echo "✓ .env file already exists" +fi + +echo "" + +# Stop existing containers if rebuild is requested +if [ "$REBUILD" = true ]; then + echo "🔄 Rebuilding containers..." + echo "Stopping existing containers..." + $COMPOSE_CMD down + + if [ "$FULL_STACK" = true ]; then + echo "Removing existing images for full rebuild..." + $COMPOSE_CMD -f docker-compose.production.yml down --rmi local 2>/dev/null || true + echo "Building and starting full stack..." + $COMPOSE_CMD -f docker-compose.production.yml up -d --build + else + echo "Removing existing backend images for rebuild..." + $COMPOSE_CMD down --rmi local 2>/dev/null || true + echo "Building and starting backend services..." + $COMPOSE_CMD up -d --build + fi +else + # Regular startup + if [ "$FULL_STACK" = true ]; then + echo "Starting full production stack (backend + frontend)..." + $COMPOSE_CMD -f docker-compose.production.yml up -d + else + echo "Starting backend services..." + $COMPOSE_CMD up -d + fi +fi + +echo "" +echo "Waiting for services to be ready..." +sleep 15 + +echo "" +echo "=========================================" +if [ "$FULL_STACK" = true ]; then + echo "Complete Stack is running! 🚀" +else + echo "Backend Services are running!" +fi +echo "=========================================" +echo "" +echo "API: http://localhost:8000" +echo "API Docs: http://localhost:8000/docs" +echo "MinIO Console: http://localhost:9001" +echo " Username: minioadmin" +echo " Password: minioadmin" +echo "" + +if [ "$FULL_STACK" = true ]; then + echo "Frontend: http://localhost:80" + echo " (Production build with Nginx)" + echo "" + echo "To view logs: $COMPOSE_CMD -f docker-compose.production.yml logs -f" + echo "To stop: $COMPOSE_CMD -f docker-compose.production.yml down" +else + echo "To view logs: $COMPOSE_CMD logs -f" + echo "To stop: $COMPOSE_CMD down" + echo "" + echo "=========================================" + echo "Frontend Options:" + echo "=========================================" + echo "" + echo "Development (with hot reload):" + echo " ./dev-start.sh" + echo " Frontend will be available at: http://localhost:4200" + echo "" + echo "Or start full production stack:" + echo " ./quickstart.sh --full-stack" + echo " Complete stack at: http://localhost:80" +fi + +echo "" +echo "=========================================" +echo "Rebuild Options:" +echo "=========================================" +echo "" +echo "Force rebuild backend: ./quickstart.sh --rebuild" +echo "Force rebuild full stack: ./quickstart.sh --rebuild --full-stack" +echo "" +echo "=========================================" +echo "Testing the API..." +echo "=========================================" +echo "" + +# Wait a bit more for API to be fully ready +sleep 5 + +# Test health endpoint +if curl -s http://localhost:8000/health | grep -q "healthy"; then + echo "✓ API is healthy!" + echo "" + if [ "$FULL_STACK" = false ]; then + echo "🎯 Ready for development!" + echo "Run './dev-start.sh' to start the frontend with hot reload" + echo "" + fi + echo "Example: Upload a test file" + echo "----------------------------" + echo 'echo "test,data" > test.csv' + echo 'curl -X POST "http://localhost:8000/api/v1/artifacts/upload" \' + echo ' -F "file=@test.csv" \' + echo ' -F "test_name=sample_test" \' + echo ' -F "test_suite=demo" \' + echo ' -F "test_result=pass"' + echo "" +else + echo "⚠ API is not responding yet. Please wait a moment and check http://localhost:8000/health" +fi + +echo "=========================================" +echo "Setup complete! 🚀" +echo "=========================================" diff --git a/static/index.html b/static/index.html deleted file mode 100644 index 4fcd19c..0000000 --- a/static/index.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - 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 deleted file mode 100644 index c7ff00a..0000000 --- a/static/js/app.js +++ /dev/null @@ -1,462 +0,0 @@ -// 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); -} diff --git a/utils/seed_data.py b/utils/seed_data.py index e54f8cd..07cf9a8 100755 --- a/utils/seed_data.py +++ b/utils/seed_data.py @@ -23,6 +23,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..') from app.database import SessionLocal from app.models.artifact import Artifact +from app.models.tag import Tag from app.storage import get_storage_backend from app.config import settings @@ -48,6 +49,22 @@ TAGS = [ "integration", "unit", "e2e", "api" ] +# Predefined tags with descriptions and colors +PREDEFINED_TAGS = [ + {"name": "regression", "description": "Regression tests to verify existing functionality", "color": "#FF6B6B"}, + {"name": "smoke", "description": "Quick smoke tests for basic functionality", "color": "#4ECDC4"}, + {"name": "critical", "description": "Critical tests that must pass", "color": "#E74C3C"}, + {"name": "high-priority", "description": "High priority tests", "color": "#F39C12"}, + {"name": "automated", "description": "Automated test execution", "color": "#3498DB"}, + {"name": "manual", "description": "Manual test execution required", "color": "#9B59B6"}, + {"name": "performance", "description": "Performance and load tests", "color": "#1ABC9C"}, + {"name": "security", "description": "Security and vulnerability tests", "color": "#E67E22"}, + {"name": "integration", "description": "Integration tests between components", "color": "#2ECC71"}, + {"name": "unit", "description": "Unit tests for individual components", "color": "#16A085"}, + {"name": "e2e", "description": "End-to-end user journey tests", "color": "#8E44AD"}, + {"name": "api", "description": "API endpoint tests", "color": "#2C3E50"}, +] + def generate_csv_content() -> bytes: """Generate random CSV test data""" @@ -183,6 +200,49 @@ def get_file_type(filename: str) -> str: return type_mapping.get(extension, 'binary') +async def seed_predefined_tags() -> List[int]: + """ + Seed predefined tags into the database. + + Returns: + List of created tag IDs + """ + db = SessionLocal() + tag_ids = [] + + try: + print("Seeding predefined tags...") + + for tag_data in PREDEFINED_TAGS: + # Check if tag already exists + existing_tag = db.query(Tag).filter(Tag.name == tag_data["name"]).first() + if existing_tag: + print(f" Tag '{tag_data['name']}' already exists, skipping...") + tag_ids.append(existing_tag.id) + continue + + tag = Tag( + name=tag_data["name"], + description=tag_data["description"], + color=tag_data["color"] + ) + db.add(tag) + db.commit() + db.refresh(tag) + tag_ids.append(tag.id) + print(f" Created tag: {tag_data['name']}") + + print(f"✓ Successfully seeded {len(tag_ids)} tags") + return tag_ids + + except Exception as e: + db.rollback() + print(f"✗ Error seeding tags: {e}") + raise + finally: + db.close() + + async def generate_seed_data(num_artifacts: int = 50) -> List[int]: """ Generate and upload seed data to the database and storage. @@ -197,7 +257,11 @@ async def generate_seed_data(num_artifacts: int = 50) -> List[int]: artifact_ids = [] try: - print(f"Generating {num_artifacts} seed artifacts...") + # First, seed tags + print("Step 1: Seeding tags...") + await seed_predefined_tags() + + print(f"\nStep 2: Generating {num_artifacts} seed artifacts...") print(f"Deployment mode: {settings.deployment_mode}") print(f"Storage backend: {settings.storage_backend}") @@ -269,42 +333,54 @@ async def generate_seed_data(num_artifacts: int = 50) -> List[int]: async def clear_all_data(): """ - Clear all artifacts from database and storage. + Clear all artifacts and tags from database and storage. WARNING: This will delete ALL data! """ db = SessionLocal() storage = get_storage_backend() try: - print("Clearing all artifacts...") + print("Clearing all data...") - # Get all artifacts + # Clear artifacts artifacts = db.query(Artifact).all() - count = len(artifacts) + artifact_count = len(artifacts) - if count == 0: + if artifact_count > 0: + print(f"Found {artifact_count} artifacts to delete...") + + # Delete from storage and database + for i, artifact in enumerate(artifacts): + try: + # Delete from storage + object_name = artifact.storage_path.split('/')[-1] + await storage.delete_file(object_name) + except Exception as e: + print(f" Warning: Could not delete {artifact.filename} from storage: {e}") + + # Delete from database + db.delete(artifact) + + if (i + 1) % 10 == 0: + print(f" Deleted {i + 1}/{artifact_count} artifacts...") + + db.commit() + print(f"✓ Successfully deleted {artifact_count} artifacts") + else: print("No artifacts to delete.") - return - print(f"Found {count} artifacts to delete...") + # Clear tags + tags = db.query(Tag).all() + tag_count = len(tags) - # Delete from storage and database - for i, artifact in enumerate(artifacts): - try: - # Delete from storage - object_name = artifact.storage_path.split('/')[-1] - await storage.delete_file(object_name) - except Exception as e: - print(f" Warning: Could not delete {artifact.filename} from storage: {e}") - - # Delete from database - db.delete(artifact) - - if (i + 1) % 10 == 0: - print(f" Deleted {i + 1}/{count} artifacts...") - - db.commit() - print(f"✓ Successfully deleted {count} artifacts") + if tag_count > 0: + print(f"Found {tag_count} tags to delete...") + for tag in tags: + db.delete(tag) + db.commit() + print(f"✓ Successfully deleted {tag_count} tags") + else: + print("No tags to delete.") except Exception as e: db.rollback()