feat: workspace CRUD — control plane routes + dashboard UI#84
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 54 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (9)
📝 WalkthroughWalkthroughThis PR introduces workspace management capabilities across the control plane and dashboard. The control plane now exposes CRUD API endpoints for workspaces backed by an in-memory store, while the dashboard provides UI components for listing, creating, updating, and deleting workspaces with form-based configuration. Changes
Sequence DiagramsequenceDiagram
participant User as User (Dashboard)
participant Form as WorkspaceForm
participant API as Dashboard API Client
participant Server as Control Plane API
participant Store as Workspace Store
User->>Form: Click "Create Workspace"
Form->>Form: Populate form state
User->>Form: Submit workspace data
Form->>API: POST /v1/workspaces (create) or PATCH /v1/workspaces/:id (update)
API->>Server: Send HTTP request with workspace payload
Server->>Store: Check if workspace exists (update) or name is unique (create)
alt Create - Duplicate Name
Store-->>Server: Name already exists
Server-->>API: 409 WORKSPACE_ALREADY_EXISTS
else Valid Request
Store->>Store: Create or update workspace with UUID and timestamps
Store-->>Server: Workspace object
Server-->>API: 200 OK with workspace
end
API-->>Form: Response (success or error)
Form->>Form: Show toast notification
Form->>User: Close form, refresh list
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Deploying getpaws with
|
| Latest commit: |
1c00227
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ee806777.getpaws-6m4.pages.dev |
| Branch Preview URL: | https://feat-workspace-ui-and-routes.getpaws-6m4.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
apps/control-plane/src/app.ts (1)
988-996: Consider adding audit logging for workspace operations.Daemon CRUD operations emit audit events (e.g.,
daemon.created,daemon.updated,daemon.deleted), but workspace operations do not. For consistency and operational visibility, consider adding similar audit entries.📝 Example audit logging for create
const workspace = workspaceStore.create({ id: randomUUID(), ...body, repos: body.repos, settings: body.settings ?? {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); + auditStore.append({ + category: 'workspace', + action: 'workspace.created', + severity: 'info', + resourceType: 'workspace', + resourceId: workspace.id, + }); return c.json(workspace, 201);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/control-plane/src/app.ts` around lines 988 - 996, Add an audit event emission when a workspace is created: after calling workspaceStore.create(...) and before returning c.json(...), call the existing audit/event logging utility used for daemon events (emitAuditEvent or similar) to publish an event like "workspace.created" with the workspace id, name/payload (workspace), actor/context (e.g., request user or c.get('user') if available) and timestamps; update the same pattern in the handler that performs workspaceStore.update and workspaceStore.delete to emit "workspace.updated" and "workspace.deleted" respectively so workspace operations mirror daemon.* audit entries.apps/dashboard/src/api/client.ts (1)
578-635: Consider a workspace API factory that injects the request function.These new methods call global
fetchdirectly, which makes isolated testing harder. A smallcreateWorkspaceApi(request = fetch)factory keeps behavior identical and improves testability.As per coding guidelines, "Use dependency injection for testability. Inject
request,exec, and other dependencies rather than relying on globals."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/api/client.ts` around lines 578 - 635, Refactor the workspace API to accept a request function via a small factory (e.g., export a createWorkspaceApi(request = fetch) that returns the methods listWorkspaces, getWorkspace, createWorkspace, updateWorkspace, and deleteWorkspace) so tests can inject a mock request; each method should use the injected request instead of the global fetch and preserve current behavior (including encodeURIComponent for id, apiKeyHeaders() usage, error parsing logic in createWorkspace/updateWorkspace, and returned shapes). Update any existing callers/tests to call createWorkspaceApi() or pass a mocked request when needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/control-plane/src/app.ts`:
- Around line 972-973: The workspace CRUD endpoints under /v1/workspaces are
missing the auth middleware registration; update the auth middleware
registration block (the same place where daemons, sessions, and snapshots are
protected around the existing middleware setup) to attach the same
authentication middleware (e.g., ensureAuthenticated / authMiddleware used for
daemons/sessions/snapshots) to the workspace routes (the router or handler that
registers the workspace CRUD endpoints, such as the workspacesRoutes or router
handling /v1/workspaces) so all create/read/update/delete workspace endpoints
are protected.
In `@apps/dashboard/src/api/client.ts`:
- Line 580: Replace the generic throws (e.g., "throw new Error(`Failed to fetch
workspaces: ${res.status}`)" and the other throw sites at the indicated
locations) with a typed workspace error that carries an error code so callers
can handle them programmatically; instantiate and throw the project-specific
error class (e.g., DaemonsError or WorkspaceError) with a distinct code like
WORKSPACE_FETCH_FAILED or WORKSPACE_NOT_FOUND and include the original
message/status as context. Locate the throw sites in
apps/dashboard/src/api/client.ts that use the local res variable and the
fetch/parse branches and replace each "new Error(...)" with "new
DaemonsError('WORKSPACE_<SPECIFIC_CODE>', `<descriptive message including
${res.status} or details>`)" (or the established WorkspaceError class used
across the codebase), preserving original message text as the error detail.
Ensure import of the error class is added if missing and use unique error codes
per failure case (e.g., WORKSPACE_FETCH_FAILED, WORKSPACE_PARSE_FAILED,
WORKSPACE_DELETE_FAILED) to match coding guidelines.
- Around line 578-582: The API functions listWorkspaces, getWorkspace,
createWorkspace, and updateWorkspace currently send/receive raw JSON, so add Zod
schemas for the Workspace shape and for list/individual responses and use them
to validate: parse and validate response bodies with schema.parse(resJson) and
throw a descriptive error on parse failures; for createWorkspace and
updateWorkspace validate the request payload with the corresponding Zod input
schema before calling fetch and send the validated object as the body; derive
TypeScript types via z.infer<> and replace direct returns with the parsed/typed
results to ensure runtime safety and clear error messages (reference the
functions listWorkspaces, getWorkspace, createWorkspace, updateWorkspace and the
central Workspace schema you add).
In `@apps/dashboard/src/pages/Workspaces.tsx`:
- Around line 24-32: The file defines a local Workspace interface that diverges
from the API client’s canonical type (missing rootDir, settings, updatedAt and
using a loose role type); replace the local interface declaration with an import
of the API client's Workspace type and update any usages (e.g., the local
Workspace interface and the repos.role annotation) to use the imported Workspace
to ensure type parity and stricter role union types.
---
Nitpick comments:
In `@apps/control-plane/src/app.ts`:
- Around line 988-996: Add an audit event emission when a workspace is created:
after calling workspaceStore.create(...) and before returning c.json(...), call
the existing audit/event logging utility used for daemon events (emitAuditEvent
or similar) to publish an event like "workspace.created" with the workspace id,
name/payload (workspace), actor/context (e.g., request user or c.get('user') if
available) and timestamps; update the same pattern in the handler that performs
workspaceStore.update and workspaceStore.delete to emit "workspace.updated" and
"workspace.deleted" respectively so workspace operations mirror daemon.* audit
entries.
In `@apps/dashboard/src/api/client.ts`:
- Around line 578-635: Refactor the workspace API to accept a request function
via a small factory (e.g., export a createWorkspaceApi(request = fetch) that
returns the methods listWorkspaces, getWorkspace, createWorkspace,
updateWorkspace, and deleteWorkspace) so tests can inject a mock request; each
method should use the injected request instead of the global fetch and preserve
current behavior (including encodeURIComponent for id, apiKeyHeaders() usage,
error parsing logic in createWorkspace/updateWorkspace, and returned shapes).
Update any existing callers/tests to call createWorkspaceApi() or pass a mocked
request when needed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e4445733-fe57-4586-935a-cc4aaf1ac90c
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
apps/control-plane/package.jsonapps/control-plane/src/app.tsapps/control-plane/tsconfig.jsonapps/dashboard/src/api/client.tsapps/dashboard/src/components/CommandPalette.tsxapps/dashboard/src/components/Layout.tsxapps/dashboard/src/components/WorkspaceForm.tsxapps/dashboard/src/pages/Workspaces.tsxapps/dashboard/src/router.tsx
| // --- Workspaces --- | ||
|
|
There was a problem hiding this comment.
Missing auth middleware for workspace routes.
The workspace CRUD endpoints lack auth middleware registration. Other /v1/* domain routes (e.g., daemons, sessions, snapshots) have explicit auth middleware applied in lines 551-573. Without this, workspace routes are unprotected.
🔒 Proposed fix to add auth middleware for workspace routes
Add these lines in the auth middleware registration section (around line 573):
app.use('/v1/mcp/*', authMiddleware(authConfig));
app.use('/v1/mcp', authMiddleware(authConfig));
app.use('/v1/settings', authMiddleware(authConfig));
app.use('/v1/settings/*', authMiddleware(authConfig));
+app.use('/v1/workspaces', authMiddleware(authConfig));
+app.use('/v1/workspaces/*', authMiddleware(authConfig));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/control-plane/src/app.ts` around lines 972 - 973, The workspace CRUD
endpoints under /v1/workspaces are missing the auth middleware registration;
update the auth middleware registration block (the same place where daemons,
sessions, and snapshots are protected around the existing middleware setup) to
attach the same authentication middleware (e.g., ensureAuthenticated /
authMiddleware used for daemons/sessions/snapshots) to the workspace routes (the
router or handler that registers the workspace CRUD endpoints, such as the
workspacesRoutes or router handling /v1/workspaces) so all
create/read/update/delete workspace endpoints are protected.
| export async function listWorkspaces(): Promise<{ workspaces: Workspace[] }> { | ||
| const res = await fetch('/v1/workspaces', { headers: apiKeyHeaders() }); | ||
| if (!res.ok) throw new Error(`Failed to fetch workspaces: ${res.status}`); | ||
| return res.json(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the file
find . -type f -name "client.ts" | grep -E "apps/dashboard.*api"Repository: arek-e/paws
Length of output: 89
🏁 Script executed:
# Get file size to understand how much to read
wc -l apps/dashboard/src/api/client.tsRepository: arek-e/paws
Length of output: 91
🏁 Script executed:
# Read the relevant section of the file (around lines 578-627)
sed -n '570,635p' apps/dashboard/src/api/client.tsRepository: arek-e/paws
Length of output: 2076
🏁 Script executed:
# Check if Zod is already imported in the file
grep -n "zod\|Zod" apps/dashboard/src/api/client.ts | head -20Repository: arek-e/paws
Length of output: 37
🏁 Script executed:
# Check for the Workspace type definition
grep -n "type Workspace\|interface Workspace" apps/dashboard/src/api/client.tsRepository: arek-e/paws
Length of output: 124
🏁 Script executed:
# Get the full Workspace and WorkspaceRepo type definitions
sed -n '554,575p' apps/dashboard/src/api/client.tsRepository: arek-e/paws
Length of output: 515
🏁 Script executed:
# Check if Zod is used elsewhere in the dashboard project
find apps/dashboard -type f \( -name "*.ts" -o -name "*.tsx" \) -exec grep -l "from 'zod'\|from \"zod\"" {} \;Repository: arek-e/paws
Length of output: 37
🏁 Script executed:
# Look at the imports at the top of the client.ts file to understand dependencies
head -30 apps/dashboard/src/api/client.tsRepository: arek-e/paws
Length of output: 950
Add Zod validation for workspace API request/response payloads.
The listWorkspaces(), getWorkspace(), createWorkspace(), and updateWorkspace() functions return unvalidated JSON responses, and createWorkspace() and updateWorkspace() submit request bodies without validation. This creates runtime brittleness if the API schema drifts from the TypeScript interface definitions.
Proposed fix
+import { z } from 'zod';
+
+const WorkspaceRepoSchema = z.object({
+ name: z.string(),
+ role: z.enum(['primary', 'reference']).optional(),
+ branch: z.string().optional(),
+});
+
+const WorkspaceSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ description: z.string().optional(),
+ type: z.enum(['monorepo', 'multi-repo']),
+ repos: z.array(WorkspaceRepoSchema),
+ rootDir: z.string().optional(),
+ settings: z
+ .object({
+ language: z.string().optional(),
+ packageManager: z.string().optional(),
+ testCommand: z.string().optional(),
+ buildCommand: z.string().optional(),
+ })
+ .optional(),
+ daemonCount: z.number().optional(),
+ createdAt: z.string(),
+ updatedAt: z.string().optional(),
+});
+
+const WorkspaceListSchema = z.object({
+ workspaces: z.array(WorkspaceSchema),
+});
+
export async function listWorkspaces(): Promise<{ workspaces: Workspace[] }> {
const res = await fetch('/v1/workspaces', { headers: apiKeyHeaders() });
if (!res.ok) throw new Error(`Failed to fetch workspaces: ${res.status}`);
- return res.json();
+ return WorkspaceListSchema.parse(await res.json());
}Per coding guidelines: "Use Zod for all external data validation: API requests, config files, and environment variables."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/api/client.ts` around lines 578 - 582, The API functions
listWorkspaces, getWorkspace, createWorkspace, and updateWorkspace currently
send/receive raw JSON, so add Zod schemas for the Workspace shape and for
list/individual responses and use them to validate: parse and validate response
bodies with schema.parse(resJson) and throw a descriptive error on parse
failures; for createWorkspace and updateWorkspace validate the request payload
with the corresponding Zod input schema before calling fetch and send the
validated object as the body; derive TypeScript types via z.infer<> and replace
direct returns with the parsed/typed results to ensure runtime safety and clear
error messages (reference the functions listWorkspaces, getWorkspace,
createWorkspace, updateWorkspace and the central Workspace schema you add).
|
|
||
| export async function listWorkspaces(): Promise<{ workspaces: Workspace[] }> { | ||
| const res = await fetch('/v1/workspaces', { headers: apiKeyHeaders() }); | ||
| if (!res.ok) throw new Error(`Failed to fetch workspaces: ${res.status}`); |
There was a problem hiding this comment.
Replace generic Error throws with typed workspace error codes.
Line 580, Line 588, Line 602-605, Line 621-624, and Line 634 throw generic Error, which removes machine-readable error semantics for callers.
Proposed fix
+type WorkspaceErrorCode =
+ | 'WORKSPACE_LIST_FAILED'
+ | 'WORKSPACE_GET_FAILED'
+ | 'WORKSPACE_CREATE_FAILED'
+ | 'WORKSPACE_UPDATE_FAILED'
+ | 'WORKSPACE_DELETE_FAILED';
+
+type WorkspaceApiError = Error & {
+ code: WorkspaceErrorCode;
+ status: number;
+};
+
+function createWorkspaceApiError(
+ code: WorkspaceErrorCode,
+ status: number,
+ message: string,
+): WorkspaceApiError {
+ return Object.assign(new Error(message), { code, status });
+}
...
- if (!res.ok) throw new Error(`Failed to fetch workspaces: ${res.status}`);
+ if (!res.ok) {
+ throw createWorkspaceApiError('WORKSPACE_LIST_FAILED', res.status, `Failed to fetch workspaces: ${res.status}`);
+ }As per coding guidelines, "Use typed errors with error codes (e.g., DaemonsError), not generic Error."
Also applies to: 588-588, 602-605, 621-624, 634-634
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/api/client.ts` at line 580, Replace the generic throws
(e.g., "throw new Error(`Failed to fetch workspaces: ${res.status}`)" and the
other throw sites at the indicated locations) with a typed workspace error that
carries an error code so callers can handle them programmatically; instantiate
and throw the project-specific error class (e.g., DaemonsError or
WorkspaceError) with a distinct code like WORKSPACE_FETCH_FAILED or
WORKSPACE_NOT_FOUND and include the original message/status as context. Locate
the throw sites in apps/dashboard/src/api/client.ts that use the local res
variable and the fetch/parse branches and replace each "new Error(...)" with
"new DaemonsError('WORKSPACE_<SPECIFIC_CODE>', `<descriptive message including
${res.status} or details>`)" (or the established WorkspaceError class used
across the codebase), preserving original message text as the error detail.
Ensure import of the error class is added if missing and use unique error codes
per failure case (e.g., WORKSPACE_FETCH_FAILED, WORKSPACE_PARSE_FAILED,
WORKSPACE_DELETE_FAILED) to match coding guidelines.
| interface Workspace { | ||
| id: string; | ||
| name: string; | ||
| description?: string; | ||
| type: 'monorepo' | 'multi-repo'; | ||
| repos: Array<{ name: string; role?: string; branch?: string }>; | ||
| daemonCount?: number; | ||
| createdAt: string; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Import Workspace type from the API client instead of redefining locally.
The local interface is missing fields (rootDir, settings, updatedAt) and uses a weaker type for role (string vs 'primary' | 'reference'). The API client already exports a complete Workspace interface that should be used for type consistency.
♻️ Proposed fix
-import { deleteWorkspace, listWorkspaces } from '../api/client.js';
+import { deleteWorkspace, listWorkspaces, type Workspace } from '../api/client.js';
import { WorkspaceForm } from '../components/WorkspaceForm.js';
import { RelativeTime } from '../components/RelativeTime.js';
// ... other imports
-interface Workspace {
- id: string;
- name: string;
- description?: string;
- type: 'monorepo' | 'multi-repo';
- repos: Array<{ name: string; role?: string; branch?: string }>;
- daemonCount?: number;
- createdAt: string;
-}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/pages/Workspaces.tsx` around lines 24 - 32, The file
defines a local Workspace interface that diverges from the API client’s
canonical type (missing rootDir, settings, updatedAt and using a loose role
type); replace the local interface declaration with an import of the API
client's Workspace type and update any usages (e.g., the local Workspace
interface and the repos.role annotation) to use the imported Workspace to ensure
type parity and stricter role union types.
- Wire workspace domain into control plane: 5 OpenAPI endpoints (create, list, get, update, delete) with in-memory store - Dashboard workspace page: list view with type badges, repo info, create/edit/delete actions - WorkspaceForm: slide-out form with monorepo/multi-repo type selector, repo picker, root directory, branch, and project settings - Workspace added to sidebar nav and command palette Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
f81fbf5 to
1c00227
Compare
Summary
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit