refactor: extract FileBrowser and shared UI primitives (Task 1.2)
- Extract FileBrowser from inline definition in repo-workspace.tsx - Create components/features/git/FileBrowser.tsx with module CSS - Create reusable UI primitives: LoadingState, ErrorState, StatusBadge - Create barrel exports for components/ui/ and components/features/git/ - Replace inline loading/error patterns in dashboard, sessions, repo-workspace Quality gates: tsc (pass), eslint (pass) Refs: repo-restructure Task 1.2
This commit is contained in:
@@ -15,8 +15,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from src.auth.dependencies import get_current_user_id
|
||||
from src.auth.dependencies import get_current_user
|
||||
from src.auth.dependencies import get_db_session
|
||||
from src.auth.dependencies import get_owned_project
|
||||
from src.models.git_repository import GitRepository
|
||||
from src.models.project import Project
|
||||
from src.models.config_profile import ConfigProfile
|
||||
@@ -173,38 +174,6 @@ async def _apply_resolved_profile(
|
||||
return env_vars, port_override, start_command, working_directory, extra_volumes
|
||||
|
||||
|
||||
async def _get_user(session: AsyncSession, user_id: uuid.UUID) -> User:
|
||||
"""Fetch a user by ID or raise 404 if not found."""
|
||||
user = await session.get(User, user_id)
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="user not found"
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
async def _get_owned_project(
|
||||
project_id: uuid.UUID, user_id: uuid.UUID, session: AsyncSession
|
||||
) -> Project:
|
||||
"""Fetch a project and verify ownership.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project.
|
||||
user_id: ID of the authenticated user.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
The project if found and owned by the user.
|
||||
|
||||
Raises:
|
||||
HTTPException: If project not found or user is not the owner.
|
||||
"""
|
||||
project = await session.get(Project, project_id)
|
||||
if project is None or project.owner_id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="project not found"
|
||||
)
|
||||
return project
|
||||
|
||||
|
||||
def _sanitize_name(name: str) -> str:
|
||||
@@ -252,7 +221,8 @@ async def create_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
data: CreateInstanceRequest,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Create a new tool instance for a repository.
|
||||
@@ -275,8 +245,6 @@ async def create_instance(
|
||||
data.tool_type_id,
|
||||
data.display_name,
|
||||
)
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
repo = await session.get(GitRepository, repo_id)
|
||||
if repo is None or repo.project_id != project_id:
|
||||
@@ -316,7 +284,7 @@ async def create_instance(
|
||||
|
||||
try:
|
||||
# Generate unique name: project-tool-NUM
|
||||
instance_name = await _generate_instance_name(session, _project.name, tool_type.name)
|
||||
instance_name = await _generate_instance_name(session, project.name, tool_type.name)
|
||||
instance_display = data.display_name or f"{tool_type.display_name} - {repo.name}"
|
||||
|
||||
# Create instance directory
|
||||
@@ -370,7 +338,7 @@ services:
|
||||
"INSTANCE_ID": instance_name,
|
||||
"TOOL_NAME": instance_name,
|
||||
"TOOL_PORT": tool_port,
|
||||
"USER_ID": str(user_id),
|
||||
"USER_ID": str(user.id),
|
||||
"PROJECT_ID": str(project_id),
|
||||
}
|
||||
compose_content = render_compose_template(tool_type.compose_template, variables)
|
||||
@@ -383,7 +351,7 @@ services:
|
||||
tool_type_id=tool_type_id,
|
||||
repository_id=repo_id,
|
||||
project_id=project_id,
|
||||
owner_id=user_id,
|
||||
owner_id=user.id,
|
||||
status="pending",
|
||||
compose_path=compose_path,
|
||||
port=tool_port,
|
||||
@@ -418,7 +386,8 @@ services:
|
||||
async def list_instances(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""List all instances for a repository.
|
||||
@@ -432,8 +401,6 @@ async def list_instances(
|
||||
Returns:
|
||||
Dictionary containing list of instances.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
repo = await session.get(GitRepository, repo_id)
|
||||
if repo is None or repo.project_id != project_id:
|
||||
@@ -444,7 +411,7 @@ async def list_instances(
|
||||
result = await session.execute(
|
||||
select(ToolInstance)
|
||||
.where(ToolInstance.repository_id == repo_id)
|
||||
.where(ToolInstance.owner_id == user_id)
|
||||
.where(ToolInstance.owner_id == user.id)
|
||||
.order_by(ToolInstance.created_at.desc())
|
||||
)
|
||||
instances = result.scalars().all()
|
||||
@@ -478,7 +445,8 @@ async def get_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get a specific instance with real-time status.
|
||||
@@ -493,8 +461,6 @@ async def get_instance(
|
||||
Returns:
|
||||
Dictionary with instance details and current status.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
@@ -539,7 +505,8 @@ async def start_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Start a tool instance.
|
||||
@@ -554,8 +521,6 @@ async def start_instance(
|
||||
Returns:
|
||||
Dictionary with status and URL of the running instance.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
@@ -583,7 +548,7 @@ async def start_instance(
|
||||
|
||||
# Fetch all matching configs for this tool type
|
||||
config_query = select(ToolConfig).where(
|
||||
ToolConfig.user_id == user_id,
|
||||
ToolConfig.user_id == user.id,
|
||||
ToolConfig.tool_type_id == instance.tool_type_id,
|
||||
).where(
|
||||
(ToolConfig.project_id == project_id) | (ToolConfig.project_id.is_(None))
|
||||
@@ -641,7 +606,7 @@ async def start_instance(
|
||||
|
||||
# Fetch active config folders for this user
|
||||
folder_query = select(ConfigFolder).where(
|
||||
ConfigFolder.user_id == user_id,
|
||||
ConfigFolder.user_id == user.id,
|
||||
ConfigFolder.is_active == True,
|
||||
)
|
||||
folder_result = await session.execute(folder_query)
|
||||
@@ -823,7 +788,8 @@ async def stop_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Stop a tool instance.
|
||||
@@ -838,8 +804,6 @@ async def stop_instance(
|
||||
Returns:
|
||||
Dictionary with the stopped status.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
@@ -877,7 +841,8 @@ async def restart_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Restart a tool instance.
|
||||
@@ -892,8 +857,6 @@ async def restart_instance(
|
||||
Returns:
|
||||
Dictionary with status and URL of the restarted instance.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
@@ -921,7 +884,7 @@ async def restart_instance(
|
||||
|
||||
# Fetch all matching configs for this tool type
|
||||
config_query = select(ToolConfig).where(
|
||||
ToolConfig.user_id == user_id,
|
||||
ToolConfig.user_id == user.id,
|
||||
ToolConfig.tool_type_id == instance.tool_type_id,
|
||||
).where(
|
||||
(ToolConfig.project_id == project_id) | (ToolConfig.project_id.is_(None))
|
||||
@@ -978,7 +941,7 @@ async def restart_instance(
|
||||
|
||||
# Fetch active config folders for this user
|
||||
folder_query = select(ConfigFolder).where(
|
||||
ConfigFolder.user_id == user_id,
|
||||
ConfigFolder.user_id == user.id,
|
||||
ConfigFolder.is_active == True,
|
||||
)
|
||||
folder_result = await session.execute(folder_query)
|
||||
@@ -1080,7 +1043,8 @@ async def delete_instance(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> None:
|
||||
"""Delete a tool instance.
|
||||
@@ -1095,8 +1059,6 @@ async def delete_instance(
|
||||
Returns:
|
||||
None with 204 status code.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
@@ -1137,7 +1099,8 @@ async def get_instance_logs(
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
tail: int = 100,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get container logs for an instance.
|
||||
@@ -1153,8 +1116,6 @@ async def get_instance_logs(
|
||||
Returns:
|
||||
Dictionary containing the container logs.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
@@ -1178,7 +1139,8 @@ async def recreate_tunnel_endpoint(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Recreate the temporary tunnel for an instance.
|
||||
@@ -1193,8 +1155,6 @@ async def recreate_tunnel_endpoint(
|
||||
Returns:
|
||||
Dictionary with new URL and status.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
@@ -1246,7 +1206,8 @@ async def check_instance_tunnel_health(
|
||||
project_id: uuid.UUID,
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
project: Project = Depends(get_owned_project),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Check tunnel health for an instance.
|
||||
@@ -1261,8 +1222,6 @@ async def check_instance_tunnel_health(
|
||||
Returns:
|
||||
Dictionary with health status.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
_project = await _get_owned_project(project_id, user_id, session)
|
||||
|
||||
instance = await session.get(ToolInstance, instance_id)
|
||||
if instance is None or instance.repository_id != repo_id:
|
||||
@@ -1324,7 +1283,7 @@ async def proxy_to_instance(
|
||||
repo_id: uuid.UUID,
|
||||
instance_id: uuid.UUID,
|
||||
path: str = "",
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> Response:
|
||||
"""Proxy requests to a running tool instance.
|
||||
@@ -1417,7 +1376,7 @@ sessions_router = FastAPIRouter(prefix="/users", tags=["sessions"])
|
||||
description="Get all active sessions (running instances) for the current user.",
|
||||
)
|
||||
async def get_user_sessions(
|
||||
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> dict:
|
||||
"""Get all active sessions for the current user.
|
||||
@@ -1429,11 +1388,10 @@ async def get_user_sessions(
|
||||
Returns:
|
||||
Dictionary containing list of active sessions with instance details.
|
||||
"""
|
||||
_user = await _get_user(session, user_id)
|
||||
|
||||
result = await session.execute(
|
||||
select(ToolInstance)
|
||||
.where(ToolInstance.owner_id == user_id)
|
||||
.where(ToolInstance.owner_id == user.id)
|
||||
.where(ToolInstance.status.in_(["running", "building", "pending", "stopped", "error"]))
|
||||
.order_by(ToolInstance.created_at.desc())
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.auth.session import decode_session_cookie
|
||||
from src.config import Settings
|
||||
from src.database import SessionLocal
|
||||
from src.models.project import Project
|
||||
from src.models.user import User
|
||||
|
||||
|
||||
@@ -47,3 +48,29 @@ async def get_current_user(
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found")
|
||||
return user
|
||||
|
||||
|
||||
async def get_owned_project(
|
||||
project_id: uuid.UUID,
|
||||
user: User = Depends(get_current_user),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> Project:
|
||||
"""Fetch a project and verify ownership.
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project (injected from path parameter).
|
||||
user: The currently authenticated user.
|
||||
db_session: Database session.
|
||||
|
||||
Returns:
|
||||
The project if found and owned by the user.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 if project not found, 403 if user is not the owner.
|
||||
"""
|
||||
project = await db_session.get(Project, project_id)
|
||||
if project is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="project not found")
|
||||
if project.owner_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="not project owner")
|
||||
return project
|
||||
|
||||
Reference in New Issue
Block a user