From b56a53c5b67f7c23c91efbe65eae3c1ce1315830 Mon Sep 17 00:00:00 2001 From: XinweiHe Date: Sun, 8 Feb 2026 21:28:36 -0800 Subject: [PATCH 1/6] update --- backend/rest/routers/deps.py | 3 +- backend/shared/__init__.py | 0 backend/shared/enums.py | 24 ++++++++++++++++ backend/worker/transformer.py | 26 +++++++++-------- frontend/packages/core/package.json | 3 +- frontend/packages/core/pnpm-lock.yaml | 22 +++++++++++++++ frontend/packages/core/prisma/schema.prisma | 22 +++++++++------ frontend/packages/core/src/constants.ts | 19 +++++++++++++ frontend/packages/core/src/index.ts | 4 +++ frontend/packages/core/src/schemas.ts | 7 +++++ frontend/packages/core/src/types/index.ts | 12 ++++---- .../[projectId]/api-keys/[keyId]/route.ts | 6 ++-- .../projects/[projectId]/api-keys/route.ts | 4 +-- .../[workspaceId]/invites/[inviteId]/route.ts | 4 +-- .../workspaces/[workspaceId]/invites/route.ts | 8 +++--- .../[workspaceId]/members/[userId]/route.ts | 16 +++++------ .../workspaces/[workspaceId]/members/route.ts | 9 +++--- .../projects/[projectId]/route.ts | 6 ++-- .../[workspaceId]/projects/route.ts | 4 +-- .../app/api/workspaces/[workspaceId]/route.ts | 6 ++-- frontend/ui/src/app/api/workspaces/route.ts | 6 ++-- .../[projectId]/traces/[traceId]/page.tsx | 3 +- .../workspace/components/GeneralTab.tsx | 3 +- .../workspace/components/MembersTab.tsx | 12 ++++---- .../traces/components/SpanInfoPanel.tsx | 3 +- .../traces/components/SpanTreeView.tsx | 7 +++-- .../ui/src/features/traces/utils/index.ts | 3 +- .../ui/src/features/workspaces/utils/index.ts | 4 +-- frontend/ui/src/lib/auth-helpers.ts | 2 +- frontend/ui/src/types/api.ts | 13 +++++---- traceroot-py/traceroot/constants.py | 11 ++++++-- traceroot-py/traceroot/decorators.py | 28 +++++++++---------- 32 files changed, 199 insertions(+), 101 deletions(-) create mode 100644 backend/shared/__init__.py create mode 100644 backend/shared/enums.py create mode 100644 frontend/packages/core/pnpm-lock.yaml create mode 100644 frontend/packages/core/src/constants.ts create mode 100644 frontend/packages/core/src/schemas.ts diff --git a/backend/rest/routers/deps.py b/backend/rest/routers/deps.py index d38ff3a3..e4e26a50 100644 --- a/backend/rest/routers/deps.py +++ b/backend/rest/routers/deps.py @@ -5,6 +5,7 @@ import httpx from fastapi import Depends, Header, HTTPException, status +from shared.enums import MemberRole # Configuration for internal API (Python → Next.js) TRACEROOT_UI_URL = os.getenv("TRACEROOT_UI_URL", "http://localhost:3000") @@ -76,7 +77,7 @@ async def get_project_access( return ProjectAccessInfo( project_id=project_id, user_id=x_user_id, - role=data.get("role", "VIEWER"), + role=data.get("role", MemberRole.VIEWER), ) diff --git a/backend/shared/__init__.py b/backend/shared/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/shared/enums.py b/backend/shared/enums.py new file mode 100644 index 00000000..e3052b60 --- /dev/null +++ b/backend/shared/enums.py @@ -0,0 +1,24 @@ +from enum import StrEnum + + +class SpanKind(StrEnum): + LLM = "LLM" + AGENT = "AGENT" + TOOL = "TOOL" + SPAN = "SPAN" + + +class SpanStatus(StrEnum): + OK = "OK" + ERROR = "ERROR" + + +class TraceStatus(StrEnum): + OK = "ok" + ERROR = "error" + + +class MemberRole(StrEnum): + VIEWER = "VIEWER" + MEMBER = "MEMBER" + ADMIN = "ADMIN" diff --git a/backend/worker/transformer.py b/backend/worker/transformer.py index 51d43fbf..6c052801 100644 --- a/backend/worker/transformer.py +++ b/backend/worker/transformer.py @@ -37,6 +37,8 @@ from datetime import UTC, datetime from typing import Any +from shared.enums import SpanKind, SpanStatus + logger = logging.getLogger(__name__) @@ -147,19 +149,19 @@ def get_span_kind(attrs: dict[str, Any], otel_kind: int | str | None) -> str: """ # Check explicit type attribute (handle None values) explicit_type = (attrs.get("traceroot.span.type") or "").upper() - if explicit_type in ("LLM", "SPAN", "AGENT", "TOOL"): + if explicit_type in (SpanKind.LLM, SpanKind.SPAN, SpanKind.AGENT, SpanKind.TOOL): return explicit_type # Check OpenInference semantic conventions (handle None values) openinference_type = (attrs.get("openinference.span.kind") or "").upper() - if openinference_type == "LLM": - return "LLM" - elif openinference_type == "AGENT": - return "AGENT" - elif openinference_type == "TOOL": - return "TOOL" + if openinference_type == SpanKind.LLM: + return SpanKind.LLM + elif openinference_type == SpanKind.AGENT: + return SpanKind.AGENT + elif openinference_type == SpanKind.TOOL: + return SpanKind.TOOL elif openinference_type == "CHAIN": - return "SPAN" + return SpanKind.SPAN # Default based on presence of LLM-related attributes if ( @@ -167,9 +169,9 @@ def get_span_kind(attrs: dict[str, Any], otel_kind: int | str | None) -> str: or attrs.get("llm.model_name") or attrs.get("traceroot.llm.model") ): - return "LLM" + return SpanKind.LLM - return "SPAN" + return SpanKind.SPAN def _extract_user_id(attrs: dict[str, Any]) -> str | None: @@ -266,7 +268,7 @@ def transform_otel_to_clickhouse( "span_end_time": end_time, "name": span_name, "span_kind": span_kind, - "status": "OK", + "status": SpanStatus.OK, "environment": environment, } @@ -370,7 +372,7 @@ def transform_otel_to_clickhouse( status_code = status.get("code", 0) # Handle both int (0, 1, 2) and string ("STATUS_CODE_ERROR") formats if status_code == 2 or status_code == "STATUS_CODE_ERROR": - span_record["status"] = "ERROR" + span_record["status"] = SpanStatus.ERROR span_record["status_message"] = status.get("message") spans.append(span_record) diff --git a/frontend/packages/core/package.json b/frontend/packages/core/package.json index f37a3ef1..c2aecafa 100644 --- a/frontend/packages/core/package.json +++ b/frontend/packages/core/package.json @@ -19,7 +19,8 @@ "clean": "rm -rf dist" }, "dependencies": { - "@prisma/client": "^5.22.0" + "@prisma/client": "^5.22.0", + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^22.19.9", diff --git a/frontend/packages/core/pnpm-lock.yaml b/frontend/packages/core/pnpm-lock.yaml new file mode 100644 index 00000000..8996f0fa --- /dev/null +++ b/frontend/packages/core/pnpm-lock.yaml @@ -0,0 +1,22 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + zod: + specifier: ^4.3.6 + version: 4.3.6 + +packages: + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + zod@4.3.6: {} diff --git a/frontend/packages/core/prisma/schema.prisma b/frontend/packages/core/prisma/schema.prisma index 464ff339..e65613e3 100644 --- a/frontend/packages/core/prisma/schema.prisma +++ b/frontend/packages/core/prisma/schema.prisma @@ -7,6 +7,12 @@ datasource db { url = env("DATABASE_URL") } +enum MemberRole { + VIEWER + MEMBER + ADMIN +} + // Access Keys - API authentication for SDK/trace ingestion model AccessKey { id String @id @db.VarChar @@ -25,10 +31,10 @@ model AccessKey { // Invites - Pending workspace invitations model Invite { - id String @id @db.VarChar - email String @db.VarChar - workspaceId String @map("workspace_id") @db.VarChar - role String @db.VarChar + id String @id @db.VarChar + email String @db.VarChar + workspaceId String @map("workspace_id") @db.VarChar + role MemberRole invitedByUserId String? @map("invited_by_user_id") @db.VarChar createTime DateTime @default(now()) @map("create_time") @db.Timestamp(6) updateTime DateTime @default(now()) @map("update_time") @db.Timestamp(6) @@ -42,10 +48,10 @@ model Invite { // Workspace Members - User membership in workspaces model WorkspaceMember { - id String @id @db.VarChar - workspaceId String @map("workspace_id") @db.VarChar - userId String @map("user_id") @db.VarChar - role String @db.VarChar + id String @id @db.VarChar + workspaceId String @map("workspace_id") @db.VarChar + userId String @map("user_id") @db.VarChar + role MemberRole createTime DateTime @default(now()) @map("create_time") @db.Timestamp(6) updateTime DateTime @default(now()) @map("update_time") @db.Timestamp(6) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade, onUpdate: NoAction) diff --git a/frontend/packages/core/src/constants.ts b/frontend/packages/core/src/constants.ts new file mode 100644 index 00000000..fd09a15d --- /dev/null +++ b/frontend/packages/core/src/constants.ts @@ -0,0 +1,19 @@ +// Role — re-exported from Prisma (runtime object + type) +export { MemberRole as Role } from "@prisma/client"; + +// SpanKind — ClickHouse values +export const SpanKind = { + LLM: "LLM", + AGENT: "AGENT", + TOOL: "TOOL", + SPAN: "SPAN", +} as const; +export type SpanKind = (typeof SpanKind)[keyof typeof SpanKind]; + +// SpanStatus — ClickHouse values +export const SpanStatus = { OK: "OK", ERROR: "ERROR" } as const; +export type SpanStatus = (typeof SpanStatus)[keyof typeof SpanStatus]; + +// TraceStatus — lowercase, computed at query time +export const TraceStatus = { OK: "ok", ERROR: "error" } as const; +export type TraceStatus = (typeof TraceStatus)[keyof typeof TraceStatus]; diff --git a/frontend/packages/core/src/index.ts b/frontend/packages/core/src/index.ts index 350bab10..75b792e1 100644 --- a/frontend/packages/core/src/index.ts +++ b/frontend/packages/core/src/index.ts @@ -13,5 +13,9 @@ export type { Account, } from "@prisma/client"; +// Constants & Zod schemas +export * from "./constants.js"; +export * from "./schemas.js"; + // Shared types export * from "./types/index.js"; diff --git a/frontend/packages/core/src/schemas.ts b/frontend/packages/core/src/schemas.ts new file mode 100644 index 00000000..59aaec41 --- /dev/null +++ b/frontend/packages/core/src/schemas.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; +import { Role, SpanKind, SpanStatus, TraceStatus } from "./constants.js"; + +export const RoleSchema = z.enum(Role); +export const SpanKindSchema = z.enum(SpanKind); +export const SpanStatusSchema = z.enum(SpanStatus); +export const TraceStatusSchema = z.enum(TraceStatus); diff --git a/frontend/packages/core/src/types/index.ts b/frontend/packages/core/src/types/index.ts index 5b93e494..1ebe8aab 100644 --- a/frontend/packages/core/src/types/index.ts +++ b/frontend/packages/core/src/types/index.ts @@ -1,13 +1,15 @@ -// Role Types -export const ROLES = ["VIEWER", "MEMBER", "ADMIN"] as const; -export type Role = (typeof ROLES)[number]; +import { Role } from "../constants.js"; + +export type { Role }; + +const ROLE_ORDER = [Role.VIEWER, Role.MEMBER, Role.ADMIN] as const; /** * Check if a role meets the minimum required role. */ export function hasMinRole(userRole: Role, minRole: Role): boolean { - const userIndex = ROLES.indexOf(userRole); - const minIndex = ROLES.indexOf(minRole); + const userIndex = ROLE_ORDER.indexOf(userRole); + const minIndex = ROLE_ORDER.indexOf(minRole); return userIndex >= minIndex; } diff --git a/frontend/ui/src/app/api/projects/[projectId]/api-keys/[keyId]/route.ts b/frontend/ui/src/app/api/projects/[projectId]/api-keys/[keyId]/route.ts index 893dc1b9..ae1a5da1 100644 --- a/frontend/ui/src/app/api/projects/[projectId]/api-keys/[keyId]/route.ts +++ b/frontend/ui/src/app/api/projects/[projectId]/api-keys/[keyId]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role } from "@traceroot/core"; import { requireAuth, requireProjectAccess, @@ -22,7 +22,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const accessResult = await requireProjectAccess(user.id, projectId, "MEMBER"); + const accessResult = await requireProjectAccess(user.id, projectId, Role.MEMBER); if (accessResult.error) return accessResult.error; // Check access key exists and belongs to this project @@ -76,7 +76,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const accessResult = await requireProjectAccess(user.id, projectId, "ADMIN"); + const accessResult = await requireProjectAccess(user.id, projectId, Role.ADMIN); if (accessResult.error) return accessResult.error; // Check access key exists and belongs to this project diff --git a/frontend/ui/src/app/api/projects/[projectId]/api-keys/route.ts b/frontend/ui/src/app/api/projects/[projectId]/api-keys/route.ts index 68fb684c..0f77bca3 100644 --- a/frontend/ui/src/app/api/projects/[projectId]/api-keys/route.ts +++ b/frontend/ui/src/app/api/projects/[projectId]/api-keys/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role } from "@traceroot/core"; import { requireAuth, requireProjectAccess, @@ -60,7 +60,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const accessResult = await requireProjectAccess(user.id, projectId, "MEMBER"); + const accessResult = await requireProjectAccess(user.id, projectId, Role.MEMBER); if (accessResult.error) return accessResult.error; let body: unknown; diff --git a/frontend/ui/src/app/api/workspaces/[workspaceId]/invites/[inviteId]/route.ts b/frontend/ui/src/app/api/workspaces/[workspaceId]/invites/[inviteId]/route.ts index 958e75e1..c559d501 100644 --- a/frontend/ui/src/app/api/workspaces/[workspaceId]/invites/[inviteId]/route.ts +++ b/frontend/ui/src/app/api/workspaces/[workspaceId]/invites/[inviteId]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { prisma } from "@traceroot/core"; +import { prisma, Role } from "@traceroot/core"; import { requireAuth, requireWorkspaceMembership, errorResponse } from "@/lib/auth-helpers"; type RouteParams = { params: Promise<{ workspaceId: string; inviteId: string }> }; @@ -12,7 +12,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; // Check invite exists and belongs to this workspace diff --git a/frontend/ui/src/app/api/workspaces/[workspaceId]/invites/route.ts b/frontend/ui/src/app/api/workspaces/[workspaceId]/invites/route.ts index 9db53ab0..856b9b51 100644 --- a/frontend/ui/src/app/api/workspaces/[workspaceId]/invites/route.ts +++ b/frontend/ui/src/app/api/workspaces/[workspaceId]/invites/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role, RoleSchema } from "@traceroot/core"; import { requireAuth, requireWorkspaceMembership, @@ -11,7 +11,7 @@ import { sendInviteEmail } from "@/lib/email"; const createInviteSchema = z.object({ email: z.string().email("Invalid email"), - role: z.enum(["VIEWER", "MEMBER", "ADMIN"]), + role: RoleSchema, }); type RouteParams = { params: Promise<{ workspaceId: string }> }; @@ -24,7 +24,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; const invites = await prisma.invite.findMany({ @@ -62,7 +62,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; let body: unknown; diff --git a/frontend/ui/src/app/api/workspaces/[workspaceId]/members/[userId]/route.ts b/frontend/ui/src/app/api/workspaces/[workspaceId]/members/[userId]/route.ts index 5913f7ba..bc5c3071 100644 --- a/frontend/ui/src/app/api/workspaces/[workspaceId]/members/[userId]/route.ts +++ b/frontend/ui/src/app/api/workspaces/[workspaceId]/members/[userId]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role, RoleSchema } from "@traceroot/core"; import { requireAuth, requireWorkspaceMembership, @@ -9,7 +9,7 @@ import { } from "@/lib/auth-helpers"; const updateRoleSchema = z.object({ - role: z.enum(["VIEWER", "MEMBER", "ADMIN"]), + role: RoleSchema, }); type RouteParams = { params: Promise<{ workspaceId: string; userId: string }> }; @@ -22,7 +22,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; const { membership: callerMembership } = membershipResult; @@ -55,11 +55,11 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { } // Cannot demote yourself from ADMIN if you're the last admin - if (user.id === targetUserId && targetMembership.role === "ADMIN" && newRole !== "ADMIN") { + if (user.id === targetUserId && targetMembership.role === Role.ADMIN && newRole !== Role.ADMIN) { const adminCount = await prisma.workspaceMember.count({ where: { workspaceId, - role: "ADMIN", + role: Role.ADMIN, }, }); @@ -99,7 +99,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; const { membership: callerMembership } = membershipResult; @@ -118,11 +118,11 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { } // Cannot remove yourself if you're the last admin - if (user.id === targetUserId && targetMembership.role === "ADMIN") { + if (user.id === targetUserId && targetMembership.role === Role.ADMIN) { const adminCount = await prisma.workspaceMember.count({ where: { workspaceId, - role: "ADMIN", + role: Role.ADMIN, }, }); diff --git a/frontend/ui/src/app/api/workspaces/[workspaceId]/members/route.ts b/frontend/ui/src/app/api/workspaces/[workspaceId]/members/route.ts index 80b75c13..e11db565 100644 --- a/frontend/ui/src/app/api/workspaces/[workspaceId]/members/route.ts +++ b/frontend/ui/src/app/api/workspaces/[workspaceId]/members/route.ts @@ -1,17 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role, RoleSchema } from "@traceroot/core"; import { requireAuth, requireWorkspaceMembership, errorResponse, successResponse, - Role, } from "@/lib/auth-helpers"; const addMemberSchema = z.object({ userId: z.string().min(1, "User ID is required"), - role: z.enum(["VIEWER", "MEMBER", "ADMIN"]), + role: RoleSchema, }); type RouteParams = { params: Promise<{ workspaceId: string }> }; @@ -46,7 +45,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { user_id: m.user.id, email: m.user.email, name: m.user.name, - role: m.role as Role, + role: m.role, create_time: m.createTime, })); @@ -61,7 +60,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; let body: unknown; diff --git a/frontend/ui/src/app/api/workspaces/[workspaceId]/projects/[projectId]/route.ts b/frontend/ui/src/app/api/workspaces/[workspaceId]/projects/[projectId]/route.ts index 1577ec76..d49b9858 100644 --- a/frontend/ui/src/app/api/workspaces/[workspaceId]/projects/[projectId]/route.ts +++ b/frontend/ui/src/app/api/workspaces/[workspaceId]/projects/[projectId]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role } from "@traceroot/core"; import { requireAuth, requireWorkspaceMembership, @@ -76,7 +76,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; // Check project exists and belongs to workspace @@ -147,7 +147,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; // Check project exists and belongs to workspace diff --git a/frontend/ui/src/app/api/workspaces/[workspaceId]/projects/route.ts b/frontend/ui/src/app/api/workspaces/[workspaceId]/projects/route.ts index af909a28..92258f72 100644 --- a/frontend/ui/src/app/api/workspaces/[workspaceId]/projects/route.ts +++ b/frontend/ui/src/app/api/workspaces/[workspaceId]/projects/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role } from "@traceroot/core"; import { requireAuth, requireWorkspaceMembership, @@ -62,7 +62,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "MEMBER"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.MEMBER); if (membershipResult.error) return membershipResult.error; let body: unknown; diff --git a/frontend/ui/src/app/api/workspaces/[workspaceId]/route.ts b/frontend/ui/src/app/api/workspaces/[workspaceId]/route.ts index fadef9df..8158750d 100644 --- a/frontend/ui/src/app/api/workspaces/[workspaceId]/route.ts +++ b/frontend/ui/src/app/api/workspaces/[workspaceId]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role } from "@traceroot/core"; import { requireAuth, requireWorkspaceMembership, @@ -70,7 +70,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; let body: unknown; @@ -110,7 +110,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { if (authResult.error) return authResult.error; const { user } = authResult; - const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN"); + const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN); if (membershipResult.error) return membershipResult.error; // Delete workspace (cascades to projects, memberships, invites, access keys) diff --git a/frontend/ui/src/app/api/workspaces/route.ts b/frontend/ui/src/app/api/workspaces/route.ts index 23c2e132..db078d79 100644 --- a/frontend/ui/src/app/api/workspaces/route.ts +++ b/frontend/ui/src/app/api/workspaces/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { prisma } from "@traceroot/core"; +import { prisma, Role } from "@traceroot/core"; import { requireAuth, errorResponse, successResponse } from "@/lib/auth-helpers"; const createWorkspaceSchema = z.object({ @@ -80,7 +80,7 @@ export async function POST(request: NextRequest) { id: membershipId, workspaceId, userId: user.id, - role: "ADMIN", + role: Role.ADMIN, }, }); @@ -91,7 +91,7 @@ export async function POST(request: NextRequest) { { id: workspace.id, name: workspace.name, - role: "ADMIN", + role: Role.ADMIN, member_count: 1, project_count: 0, projects: [], diff --git a/frontend/ui/src/app/projects/[projectId]/traces/[traceId]/page.tsx b/frontend/ui/src/app/projects/[projectId]/traces/[traceId]/page.tsx index 834285f6..89d2b03e 100644 --- a/frontend/ui/src/app/projects/[projectId]/traces/[traceId]/page.tsx +++ b/frontend/ui/src/app/projects/[projectId]/traces/[traceId]/page.tsx @@ -7,6 +7,7 @@ import { ArrowLeft, Clock, Cpu, DollarSign } from "lucide-react"; import { Button } from "@/components/ui/button"; import { ProjectBreadcrumb } from "@/features/projects/components"; import { formatDuration, formatDate, cn } from "@/lib/utils"; +import { SpanStatus } from "@traceroot/core"; import type { Span, TraceDetail } from "@/types/api"; import { useTrace } from "@/features/traces/hooks"; import { SpanKindBadge } from "@/features/traces/components"; @@ -235,7 +236,7 @@ function SpanInfoPanel({ span }: { span: Span }) {
Loading workspace...
; } - const isAdmin = workspace?.role === "ADMIN"; + const isAdmin = workspace?.role === Role.ADMIN; return (
diff --git a/frontend/ui/src/features/settings/workspace/components/MembersTab.tsx b/frontend/ui/src/features/settings/workspace/components/MembersTab.tsx index eca1e065..0abcdf77 100644 --- a/frontend/ui/src/features/settings/workspace/components/MembersTab.tsx +++ b/frontend/ui/src/features/settings/workspace/components/MembersTab.tsx @@ -21,6 +21,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Role } from "@traceroot/core"; import { getWorkspace, getMembers, @@ -31,7 +32,6 @@ import { cancelInvite, type Member, type Invite, - type Role, } from "@/lib/api"; interface MembersTabProps { @@ -43,7 +43,7 @@ export function MembersTab({ workspaceId }: MembersTabProps) { const [showInviteMember, setShowInviteMember] = useState(false); const [newMemberEmail, setNewMemberEmail] = useState(""); - const [newMemberRole, setNewMemberRole] = useState("MEMBER"); + const [newMemberRole, setNewMemberRole] = useState(Role.MEMBER); const [editingMember, setEditingMember] = useState<{ userId: string; role: Role } | null>(null); const { data: workspace } = useQuery({ @@ -68,7 +68,7 @@ export function MembersTab({ workspaceId }: MembersTabProps) { queryClient.invalidateQueries({ queryKey: ["invites", workspaceId] }); setShowInviteMember(false); setNewMemberEmail(""); - setNewMemberRole("MEMBER"); + setNewMemberRole(Role.MEMBER); }, }); @@ -107,8 +107,8 @@ export function MembersTab({ workspaceId }: MembersTabProps) { } }; - const roleOptions: Role[] = ["ADMIN", "MEMBER", "VIEWER"]; - const canManageMembers = workspace?.role === "ADMIN"; + const roleOptions: Role[] = [Role.ADMIN, Role.MEMBER, Role.VIEWER]; + const canManageMembers = workspace?.role === Role.ADMIN; return (
@@ -182,7 +182,7 @@ export function MembersTab({ workspaceId }: MembersTabProps) {