diff --git a/apps/api/src/api/workspaces.py b/apps/api/src/api/workspaces.py index fa80dec..1c20c89 100644 --- a/apps/api/src/api/workspaces.py +++ b/apps/api/src/api/workspaces.py @@ -17,6 +17,54 @@ from src.services.workspace_manager import WorkspaceHasInstancesError, Workspace logger = logging.getLogger(__name__) router = APIRouter(prefix="/projects/{project_id}/repositories/{repo_id}/workspaces") +all_workspaces_router = APIRouter(prefix="/workspaces") + + +@all_workspaces_router.get("/") +async def list_all_workspaces( + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> list[dict]: + """List all workspaces for the current user across all repos.""" + instance_count = ( + select(func.count(ToolInstance.id)) + .where(ToolInstance.workspace_id == Workspace.id) + .correlate(Workspace) + .scalar_subquery() + ) + + result = await session.execute( + select( + Workspace, + GitRepository.name.label("repo_name"), + GitRepository.project_id, + instance_count.label("instance_count"), + ) + .join(GitRepository, Workspace.repo_id == GitRepository.id) + .where(Workspace.user_id == user_id) + .order_by(Workspace.created_at.desc()) + ) + rows = result.all() + + return [ + { + "id": str(ws.id), + "name": ws.name, + "repo_id": str(ws.repo_id), + "repo_name": repo_name or "", + "project_id": str(project_id) if project_id else "", + "project_name": "", # Could join with Project if needed + "user_id": str(ws.user_id), + "branch": ws.branch, + "path": ws.path, + "status": ws.status, + "last_sync_at": ws.last_sync_at.isoformat() if ws.last_sync_at else None, + "created_at": ws.created_at.isoformat() if ws.created_at else None, + "updated_at": ws.updated_at.isoformat() if ws.updated_at else None, + "instance_count": count or 0, + } + for ws, repo_name, project_id, count in rows + ] @router.get("/") @@ -54,6 +102,7 @@ async def list_workspaces( "name": ws.name, "repo_id": str(ws.repo_id), "repo_name": repo.name, + "project_id": str(repo.project_id) if repo.project_id else "", "project_name": repo.project.name if repo.project else "", "user_id": str(ws.user_id), "branch": ws.branch, diff --git a/apps/api/src/main.py b/apps/api/src/main.py index c1e2b89..b10e897 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -24,7 +24,7 @@ from src.api.tool_types import router as tool_types_router from src.api.notifications import router as notifications_router from src.api.user_config import router as user_config_router from src.api.users import router as users_router -from src.api.workspaces import router as workspaces_router +from src.api.workspaces import all_workspaces_router, router as workspaces_router from src.config import Settings from src.models.notification import Notification # noqa: F401 – Alembic model discovery from src.models.terminal_session import TerminalSessionModel # noqa: F401 – Alembic model discovery @@ -160,5 +160,6 @@ app.include_router(instance_proxy_router) app.include_router(terminal_router) app.include_router(events_router) app.include_router(notifications_router) +app.include_router(all_workspaces_router) app.include_router(workspaces_router) app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") diff --git a/apps/web/src/api/workspaces.ts b/apps/web/src/api/workspaces.ts index d72fc78..b452132 100644 --- a/apps/web/src/api/workspaces.ts +++ b/apps/web/src/api/workspaces.ts @@ -22,6 +22,11 @@ export async function listWorkspaces( return response.data; } +export async function listAllWorkspaces(): Promise { + const response = await apiClient.get("/workspaces/"); + return response.data; +} + export async function createWorkspace( projectId: string, repoId: string, diff --git a/apps/web/src/hooks/use-workspaces.ts b/apps/web/src/hooks/use-workspaces.ts index f27969f..2ad6c0a 100644 --- a/apps/web/src/hooks/use-workspaces.ts +++ b/apps/web/src/hooks/use-workspaces.ts @@ -1,7 +1,7 @@ /** Hook for fetching workspaces. */ import { useCallback, useEffect, useState } from "react"; -import { listWorkspaces } from "../api/workspaces"; +import { listAllWorkspaces, listWorkspaces } from "../api/workspaces"; import type { Workspace } from "../types/workspace"; export interface UseWorkspacesResult { @@ -12,8 +12,8 @@ export interface UseWorkspacesResult { } export function useWorkspaces( - projectId: string, - repoId: string, + projectId?: string, + repoId?: string, ): UseWorkspacesResult { const [workspaces, setWorkspaces] = useState([]); const [loading, setLoading] = useState(true); @@ -23,7 +23,10 @@ export function useWorkspaces( setLoading(true); setError(null); try { - const data = await listWorkspaces(projectId, repoId); + const data = + projectId && repoId + ? await listWorkspaces(projectId, repoId) + : await listAllWorkspaces(); setWorkspaces(data); } catch (err) { setError( diff --git a/apps/web/src/pages/workspaces.tsx b/apps/web/src/pages/workspaces.tsx index 9a1f681..bf22f14 100644 --- a/apps/web/src/pages/workspaces.tsx +++ b/apps/web/src/pages/workspaces.tsx @@ -13,29 +13,25 @@ import type { Workspace } from "../types/workspace"; export function WorkspacesPage() { const [showCreate, setShowCreate] = useState(false); const [startWorkspace, setStartWorkspace] = useState(null); + const [createTarget, setCreateTarget] = useState<{ projectId: string; repoId: string } | null>(null); - // TODO: Get projectId and repoId from URL params or context - const projectId = "default-project"; - const repoId = "default-repo"; - - const { workspaces, loading, error, refresh } = useWorkspaces( - projectId, - repoId, - ); + const { workspaces, loading, error, refresh } = useWorkspaces(); const actions = useWorkspaceActions(); const handleCreate = async (data: { name: string; branch: string }) => { - await actions.create(projectId, repoId, data); + if (!createTarget) return; + await actions.create(createTarget.projectId, createTarget.repoId, data); setShowCreate(false); + setCreateTarget(null); await refresh(); }; const handleDelete = async (workspace: Workspace) => { - await actions.delete(projectId, repoId, workspace, refresh); + await actions.delete(workspace.project_id, workspace.repo_id, workspace, refresh); }; const handleSync = async (workspace: Workspace) => { - await actions.sync(projectId, repoId, workspace, refresh); + await actions.sync(workspace.project_id, workspace.repo_id, workspace, refresh); }; const handleStartTool = async ( @@ -45,8 +41,8 @@ export function WorkspacesPage() { if (!startWorkspace) return; try { const instance = await createInstance( - projectId, - repoId, + startWorkspace.project_id, + startWorkspace.repo_id, toolTypeId, `${startWorkspace.name} - ${toolTypeId}`, undefined, @@ -56,7 +52,7 @@ export function WorkspacesPage() { [], startWorkspace.id, ); - await startInstance(projectId, repoId, instance.id, configProfileId); + await startInstance(startWorkspace.project_id, startWorkspace.repo_id, instance.id, configProfileId); setStartWorkspace(null); await refresh(); } catch (err) { @@ -76,23 +72,31 @@ export function WorkspacesPage() { > - + {error &&
{error}
} - {showCreate && ( + {showCreate && createTarget && ( setShowCreate(false)} + onCancel={() => { setShowCreate(false); setCreateTarget(null); }} /> )} @@ -101,12 +105,7 @@ export function WorkspacesPage() { ) : workspaces.length === 0 ? (

No workspaces yet.

- +

Navigate to a project to create your first workspace.

) : (
diff --git a/apps/web/src/types/workspace.ts b/apps/web/src/types/workspace.ts index 801d0bb..8051129 100644 --- a/apps/web/src/types/workspace.ts +++ b/apps/web/src/types/workspace.ts @@ -5,6 +5,7 @@ export interface Workspace { name: string; repo_id: string; repo_name: string; + project_id: string; project_name: string; user_id: string; branch: string;