Add tags prominence and SIM source grouping features

Database changes:
- Add sim_source_id column to artifacts table for grouping multiple artifacts
- Create Alembic migration (001_add_sim_source_id) for schema update
- Add Alembic env.py for migration support with environment-based DB URLs

API enhancements:
- Add sim_source_id parameter to upload endpoint
- Add sim_source_id filter to query endpoint
- Add new /grouped-by-sim-source endpoint for getting artifacts by group
- Update all API documentation to include sim_source_id

UI improvements:
- Make tags required field and more prominent in upload form
- Add tags display directly in artifacts table (below filename)
- Add SIM Source ID field in upload form with helper text for grouping
- Update table to show sim_source_id (falls back to test_suite if null)
- Tags now displayed as inline badges in main table view

Seed data updates:
- Generate sim_source_id for 70% of artifacts to demonstrate grouping
- Multiple artifacts can share same sim_source_id
- Improved seed data variety with tag combinations

Features:
- Tags are now prominently displayed in both table and detail views
- Multiple artifacts can be grouped by SIM source ID
- Users can filter/query by sim_source_id
- Backward compatible - existing artifacts without sim_source_id still work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-15 09:30:25 -05:00
parent 6eab60987e
commit 21347d8c65
7 changed files with 140 additions and 11 deletions

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from typing import List, Optional
from typing import List, Optional, Dict
import uuid
import json
import io
@@ -36,6 +36,7 @@ async def upload_artifact(
test_suite: Optional[str] = Form(None),
test_config: Optional[str] = Form(None),
test_result: Optional[str] = Form(None),
sim_source_id: Optional[str] = Form(None),
custom_metadata: Optional[str] = Form(None),
description: Optional[str] = Form(None),
tags: Optional[str] = Form(None),
@@ -51,6 +52,7 @@ async def upload_artifact(
- **test_suite**: Test suite identifier
- **test_config**: JSON string of test configuration
- **test_result**: Test result (pass, fail, skip, error)
- **sim_source_id**: SIM source ID to group multiple artifacts
- **custom_metadata**: JSON string of additional metadata
- **description**: Text description of the artifact
- **tags**: JSON array of tags (as string)
@@ -88,6 +90,7 @@ async def upload_artifact(
test_suite=test_suite,
test_config=test_config_dict,
test_result=test_result,
sim_source_id=sim_source_id,
custom_metadata=metadata_dict,
description=description,
tags=tags_list,
@@ -194,6 +197,7 @@ async def query_artifacts(query: ArtifactQuery, db: Session = Depends(get_db)):
- **test_name**: Filter by test name
- **test_suite**: Filter by test suite
- **test_result**: Filter by test result
- **sim_source_id**: Filter by SIM source ID
- **tags**: Filter by tags (must contain all specified tags)
- **start_date**: Filter by creation date (from)
- **end_date**: Filter by creation date (to)
@@ -212,6 +216,8 @@ async def query_artifacts(query: ArtifactQuery, db: Session = Depends(get_db)):
q = q.filter(Artifact.test_suite == query.test_suite)
if query.test_result:
q = q.filter(Artifact.test_result == query.test_result)
if query.sim_source_id:
q = q.filter(Artifact.sim_source_id == query.sim_source_id)
if query.tags:
for tag in query.tags:
q = q.filter(Artifact.tags.contains([tag]))
@@ -240,3 +246,20 @@ async def list_artifacts(
Artifact.created_at.desc()
).offset(offset).limit(limit).all()
return artifacts
@router.get("/grouped-by-sim-source", response_model=Dict[str, List[ArtifactResponse]])
async def get_artifacts_grouped_by_sim_source(
db: Session = Depends(get_db)
):
"""Get all artifacts grouped by SIM source ID"""
from collections import defaultdict
artifacts = db.query(Artifact).order_by(Artifact.created_at.desc()).all()
grouped = defaultdict(list)
for artifact in artifacts:
sim_source = artifact.sim_source_id or "ungrouped"
grouped[sim_source].append(artifact)
return dict(grouped)