Bruno Charest 661d005fc7
Some checks failed
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
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
2025-12-23 14:56:31 -05:00

226 lines
7.8 KiB
Python

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()