Add favorites feature with toggle filters, group management, color/icon pickers, dashboard widget, and star buttons across containers/docker sections with persistent storage and real-time UI updates
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
This commit is contained in:
@@ -28,6 +28,7 @@ from app.routes.docker import router as docker_router
|
||||
from app.routes.lint import router as lint_router
|
||||
from app.routes.terminal import router as terminal_router
|
||||
from app.routes.config import router as config_router
|
||||
from app.routes.favorites import router as favorites_router
|
||||
|
||||
# Router principal qui agrège tous les sous-routers
|
||||
api_router = APIRouter()
|
||||
@@ -53,6 +54,7 @@ api_router.include_router(alerts_router, prefix="/alerts", tags=["Alerts"])
|
||||
api_router.include_router(docker_router, prefix="/docker", tags=["Docker"])
|
||||
api_router.include_router(lint_router, prefix="/playbooks", tags=["Lint"])
|
||||
api_router.include_router(terminal_router, prefix="/terminal", tags=["Terminal"])
|
||||
api_router.include_router(favorites_router, prefix="/favorites", tags=["Favorites"])
|
||||
api_router.include_router(config_router, tags=["Config"])
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.dependencies import get_current_user, get_db
|
||||
from app.crud.docker_container import DockerContainerRepository
|
||||
from app.crud.favorites import FavoriteContainerRepository, FavoriteGroupRepository
|
||||
from app.crud.host import HostRepository
|
||||
from app.schemas.favorites import (
|
||||
FavoriteContainerCreate,
|
||||
FavoriteContainerOut,
|
||||
FavoriteContainersListResponse,
|
||||
FavoriteDockerContainerOut,
|
||||
FavoriteGroupCreate,
|
||||
FavoriteGroupOut,
|
||||
FavoriteGroupsListResponse,
|
||||
FavoriteGroupUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _resolve_user_id(user: dict) -> Optional[int]:
|
||||
# For API-key auth, we keep favorites in a shared (user_id NULL) namespace.
|
||||
if user.get("type") == "api_key":
|
||||
return None
|
||||
uid = user.get("user_id")
|
||||
if uid is None:
|
||||
return None
|
||||
try:
|
||||
return int(uid)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def _favorite_to_out(db: AsyncSession, favorite, docker_container, host) -> FavoriteContainerOut:
|
||||
dc = FavoriteDockerContainerOut(
|
||||
host_id=docker_container.host_id,
|
||||
host_name=host.name if host else "Unknown",
|
||||
host_ip=host.ip_address if host else "Unknown",
|
||||
host_docker_status=host.docker_status if host else None,
|
||||
container_id=docker_container.container_id,
|
||||
name=docker_container.name,
|
||||
image=docker_container.image,
|
||||
state=docker_container.state,
|
||||
status=docker_container.status,
|
||||
health=docker_container.health,
|
||||
ports=docker_container.ports,
|
||||
compose_project=docker_container.compose_project,
|
||||
)
|
||||
|
||||
return FavoriteContainerOut(
|
||||
id=favorite.id,
|
||||
group_id=favorite.group_id,
|
||||
created_at=favorite.created_at,
|
||||
docker_container=dc,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/groups", response_model=FavoriteGroupsListResponse)
|
||||
async def list_favorite_groups(
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
repo = FavoriteGroupRepository(db_session)
|
||||
groups = await repo.list_by_user(user_id)
|
||||
return FavoriteGroupsListResponse(groups=[FavoriteGroupOut.model_validate(g) for g in groups])
|
||||
|
||||
|
||||
@router.post("/groups", response_model=FavoriteGroupOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_favorite_group(
|
||||
payload: FavoriteGroupCreate,
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
repo = FavoriteGroupRepository(db_session)
|
||||
|
||||
if await repo.exists_name(payload.name, user_id):
|
||||
raise HTTPException(status_code=400, detail="Un groupe avec ce nom existe déjà")
|
||||
|
||||
group = await repo.create(
|
||||
user_id=user_id,
|
||||
name=payload.name,
|
||||
sort_order=payload.sort_order,
|
||||
color=payload.color,
|
||||
icon_key=payload.icon_key,
|
||||
)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(group)
|
||||
return FavoriteGroupOut.model_validate(group)
|
||||
|
||||
|
||||
@router.patch("/groups/{group_id}", response_model=FavoriteGroupOut)
|
||||
async def update_favorite_group(
|
||||
group_id: int,
|
||||
payload: FavoriteGroupUpdate,
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
repo = FavoriteGroupRepository(db_session)
|
||||
|
||||
group = await repo.get_for_user(group_id, user_id)
|
||||
if not group:
|
||||
raise HTTPException(status_code=404, detail="Groupe introuvable")
|
||||
|
||||
if payload.name and payload.name != group.name:
|
||||
if await repo.exists_name(payload.name, user_id):
|
||||
raise HTTPException(status_code=400, detail="Un groupe avec ce nom existe déjà")
|
||||
|
||||
group = await repo.update(
|
||||
group,
|
||||
name=payload.name,
|
||||
sort_order=payload.sort_order,
|
||||
color=payload.color,
|
||||
icon_key=payload.icon_key,
|
||||
)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(group)
|
||||
return FavoriteGroupOut.model_validate(group)
|
||||
|
||||
|
||||
@router.delete("/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_favorite_group(
|
||||
group_id: int,
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
repo = FavoriteGroupRepository(db_session)
|
||||
|
||||
group = await repo.get_for_user(group_id, user_id)
|
||||
if not group:
|
||||
raise HTTPException(status_code=404, detail="Groupe introuvable")
|
||||
|
||||
await repo.delete(group)
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
@router.get("/containers", response_model=FavoriteContainersListResponse)
|
||||
async def list_favorite_containers(
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
|
||||
fav_repo = FavoriteContainerRepository(db_session)
|
||||
container_repo = DockerContainerRepository(db_session)
|
||||
host_repo = HostRepository(db_session)
|
||||
|
||||
favorites = await fav_repo.list_by_user(user_id)
|
||||
|
||||
out: list[FavoriteContainerOut] = []
|
||||
for fav in favorites:
|
||||
docker_container = await container_repo.get(fav.docker_container_id)
|
||||
if not docker_container:
|
||||
# Should be cleaned by FK cascade, but keep the API robust.
|
||||
continue
|
||||
host = await host_repo.get(docker_container.host_id)
|
||||
out.append(await _favorite_to_out(db_session, fav, docker_container, host))
|
||||
|
||||
return FavoriteContainersListResponse(containers=out)
|
||||
|
||||
|
||||
@router.post("/containers", response_model=FavoriteContainerOut, status_code=status.HTTP_201_CREATED)
|
||||
async def add_favorite_container(
|
||||
payload: FavoriteContainerCreate,
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
|
||||
container_repo = DockerContainerRepository(db_session)
|
||||
fav_repo = FavoriteContainerRepository(db_session)
|
||||
group_repo = FavoriteGroupRepository(db_session)
|
||||
|
||||
docker_container = await container_repo.get_by_container_id(payload.host_id, payload.container_id)
|
||||
if not docker_container:
|
||||
raise HTTPException(status_code=404, detail="Container introuvable")
|
||||
|
||||
if payload.group_id is not None:
|
||||
group = await group_repo.get_for_user(payload.group_id, user_id)
|
||||
if not group:
|
||||
raise HTTPException(status_code=404, detail="Groupe introuvable")
|
||||
|
||||
existing = await fav_repo.get_by_docker_container_id(docker_container.id, user_id)
|
||||
if existing:
|
||||
# If already exists, treat as idempotent and optionally move group.
|
||||
if existing.group_id != payload.group_id:
|
||||
await fav_repo.update_group(existing, group_id=payload.group_id)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(existing)
|
||||
host_repo = HostRepository(db_session)
|
||||
host = await host_repo.get(docker_container.host_id)
|
||||
return await _favorite_to_out(db_session, existing, docker_container, host)
|
||||
|
||||
fav = await fav_repo.create(user_id=user_id, docker_container_db_id=docker_container.id, group_id=payload.group_id)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(fav)
|
||||
|
||||
host_repo = HostRepository(db_session)
|
||||
host = await host_repo.get(docker_container.host_id)
|
||||
return await _favorite_to_out(db_session, fav, docker_container, host)
|
||||
|
||||
|
||||
@router.delete("/containers/{favorite_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def remove_favorite_container(
|
||||
favorite_id: int,
|
||||
user: dict = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
user_id = _resolve_user_id(user)
|
||||
|
||||
fav_repo = FavoriteContainerRepository(db_session)
|
||||
fav = await fav_repo.get_for_user(favorite_id, user_id)
|
||||
if not fav:
|
||||
raise HTTPException(status_code=404, detail="Favori introuvable")
|
||||
|
||||
await fav_repo.delete(fav)
|
||||
await db_session.commit()
|
||||
Reference in New Issue
Block a user