diff --git a/app/(main)/configurations/prompt-editor/page.tsx b/app/(main)/configurations/prompt-editor/page.tsx index 7e4b4e0..029569d 100644 --- a/app/(main)/configurations/prompt-editor/page.tsx +++ b/app/(main)/configurations/prompt-editor/page.tsx @@ -368,10 +368,7 @@ function PromptEditorContent() { }; return ( -
+
-
-

+

+

Loading configuration...

@@ -469,7 +455,6 @@ function PromptEditorContent() { /> ) : (
- {/* Split View: Prompt (left) + Config (right) */}
>({}); const [isActive, setIsActive] = useState(true); - const [visibleFields, setVisibleFields] = useState>(new Set()); const [existingCredential, setExistingCredential] = useState(null); - // Load credentials once we have an API key + // Load credentials once authenticated useEffect(() => { if (!isAuthenticated) return; loadCredentials(); - }, [apiKeys]); + }, [isAuthenticated, apiKeys]); // Re-populate form when provider or credentials change useEffect(() => { @@ -64,7 +63,6 @@ export default function CredentialsPage() { }); setFormValues(blank); } - setVisibleFields(new Set()); }, [selectedProvider, credentials]); const loadCredentials = async () => { @@ -151,7 +149,6 @@ export default function CredentialsPage() { setFormValues(blank); setIsActive(true); } - setVisibleFields(new Set()); }; const handleDelete = async () => { @@ -178,68 +175,29 @@ export default function CredentialsPage() { setFormValues((prev) => ({ ...prev, [key]: value })); }; - const handleToggleVisibility = (key: string) => { - setVisibleFields((prev) => { - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); - } else { - next.add(key); - } - return next; - }); - }; - return ( -
+
-
-
-

- Credentials -

-

- Manage provider credentials -

-
-
+
-
{!isAuthenticated ? ( -
+
Please log in to manage credentials.
) : ( @@ -251,10 +209,8 @@ export default function CredentialsPage() { isLoading={isLoading} isSaving={isSaving} isDeleting={isDeleting} - visibleFields={visibleFields} onChange={handleFieldChange} onActiveChange={setIsActive} - onToggleVisibility={handleToggleVisibility} onSave={handleSave} onCancel={handleCancel} onDelete={handleDelete} diff --git a/app/(main)/settings/onboarding/page.tsx b/app/(main)/settings/onboarding/page.tsx index f453e60..79c50e0 100644 --- a/app/(main)/settings/onboarding/page.tsx +++ b/app/(main)/settings/onboarding/page.tsx @@ -1,7 +1,6 @@ "use client"; import { useState, useEffect, useCallback } from "react"; -import { useRouter } from "next/navigation"; import SettingsSidebar from "@/app/components/settings/SettingsSidebar"; import PageHeader from "@/app/components/PageHeader"; import { useAuth } from "@/app/lib/context/AuthContext"; @@ -13,6 +12,7 @@ import { ProjectList, StepIndicator, UserList, + OnboardingCredentials, } from "@/app/components/settings/onboarding"; import { Organization, @@ -21,9 +21,14 @@ import { OnboardResponseData, } from "@/app/lib/types/onboarding"; import { apiFetch } from "@/app/lib/apiClient"; -import { colors } from "@/app/lib/colors"; import { ArrowLeftIcon } from "@/app/components/icons"; import { DEFAULT_PAGE_LIMIT } from "@/app/lib/constants"; +import TabNavigation from "@/app/components/TabNavigation"; + +const PROJECT_TABS = [ + { id: "users", label: "Users" }, + { id: "credentials", label: "Credentials" }, +]; type View = "loading" | "list" | "projects" | "users" | "form" | "success"; @@ -59,8 +64,7 @@ function OrganizationListSkeleton() { } export default function OnboardingPage() { - const router = useRouter(); - const { activeKey, currentUser, isHydrated, isAuthenticated } = useAuth(); + const { activeKey } = useAuth(); const [view, setView] = useState("loading"); const [selectedOrg, setSelectedOrg] = useState(null); const [selectedProject, setSelectedProject] = useState(null); @@ -69,6 +73,7 @@ export default function OnboardingPage() { const [onboardData, setOnboardData] = useState( null, ); + const [activeProjectTab, setActiveProjectTab] = useState("users"); const { items: organizations, @@ -97,18 +102,6 @@ export default function OnboardingPage() { } }, [isLoadingOrgs, organizations.length]); - // Redirect if no API key or not a superuser - useEffect(() => { - if (!isHydrated) return; - if (!isAuthenticated) { - router.replace("/"); - return; - } - if (currentUser && !currentUser.is_superuser) { - router.replace("/settings/credentials"); - } - }, [isHydrated, activeKey, currentUser, router]); - const fetchProjects = useCallback( async (org: Organization) => { setSelectedOrg(org); @@ -172,17 +165,14 @@ export default function OnboardingPage() { }; return ( -
+
@@ -211,11 +201,47 @@ export default function OnboardingPage() { )} {view === "users" && selectedOrg && selectedProject && ( - +
+ + +
+
+

+ {selectedProject.name} +

+

+ {selectedOrg.name} +

+
+
+ + + + {activeProjectTab === "users" && ( + + )} + + {activeProjectTab === "credentials" && ( + + )} +
)} {view === "form" && ( diff --git a/app/api/auth/google/route.ts b/app/api/auth/google/route.ts index 7dc39ad..b9d1bd0 100644 --- a/app/api/auth/google/route.ts +++ b/app/api/auth/google/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { setRoleCookieFromBody } from "@/app/lib/authCookie"; /** Proxy Google login token to backend. Forwards Set-Cookie headers back to the browser. */ export async function POST(request: Request) { @@ -31,6 +32,10 @@ export async function POST(request: Request) { }); } + if (status >= 200 && status < 300) { + setRoleCookieFromBody(response, data); + } + return response; } catch { return NextResponse.json( diff --git a/app/api/auth/invite/route.ts b/app/api/auth/invite/route.ts index 96984c1..e1fd42d 100644 --- a/app/api/auth/invite/route.ts +++ b/app/api/auth/invite/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { setRoleCookieFromBody } from "@/app/lib/authCookie"; export async function GET(request: NextRequest) { try { @@ -25,6 +26,10 @@ export async function GET(request: NextRequest) { res.headers.append("Set-Cookie", cookie); } + if (status >= 200 && status < 300) { + setRoleCookieFromBody(res, data); + } + return res; } catch { return NextResponse.json( diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts index 3b5994f..5111c38 100644 --- a/app/api/auth/logout/route.ts +++ b/app/api/auth/logout/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { clearRoleCookie } from "@/app/lib/authCookie"; export async function POST(request: NextRequest) { const { status, data, headers } = await apiClient( @@ -15,5 +16,7 @@ export async function POST(request: NextRequest) { res.headers.append("Set-Cookie", cookie); } + clearRoleCookie(res); + return res; } diff --git a/app/api/auth/magic-link/verify/route.ts b/app/api/auth/magic-link/verify/route.ts index fc436f5..70475a4 100644 --- a/app/api/auth/magic-link/verify/route.ts +++ b/app/api/auth/magic-link/verify/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { setRoleCookieFromBody } from "@/app/lib/authCookie"; export async function GET(request: NextRequest) { try { @@ -25,6 +26,10 @@ export async function GET(request: NextRequest) { res.headers.append("Set-Cookie", cookie); } + if (status >= 200 && status < 300) { + setRoleCookieFromBody(res, data); + } + return res; } catch { return NextResponse.json( diff --git a/app/api/credentials/org/[orgId]/[projectId]/provider/[provider]/route.ts b/app/api/credentials/org/[orgId]/[projectId]/provider/[provider]/route.ts new file mode 100644 index 0000000..583ed96 --- /dev/null +++ b/app/api/credentials/org/[orgId]/[projectId]/provider/[provider]/route.ts @@ -0,0 +1,40 @@ +import { apiClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +type Params = { + params: Promise<{ orgId: string; projectId: string; provider: string }>; +}; + +export async function GET(request: NextRequest, { params }: Params) { + const { orgId, projectId, provider } = await params; + try { + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}/provider/${provider}`, + ); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function DELETE(request: NextRequest, { params }: Params) { + const { orgId, projectId, provider } = await params; + try { + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}/provider/${provider}`, + { method: "DELETE" }, + ); + if (status === 204) return new NextResponse(null, { status: 204 }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/api/credentials/org/[orgId]/[projectId]/route.ts b/app/api/credentials/org/[orgId]/[projectId]/route.ts new file mode 100644 index 0000000..d98638b --- /dev/null +++ b/app/api/credentials/org/[orgId]/[projectId]/route.ts @@ -0,0 +1,56 @@ +import { apiClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +type Params = { params: Promise<{ orgId: string; projectId: string }> }; + +export async function GET(request: NextRequest, { params }: Params) { + const { orgId, projectId } = await params; + try { + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}`, + ); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function PATCH(request: NextRequest, { params }: Params) { + const { orgId, projectId } = await params; + try { + const body = await request.json(); + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}`, + { method: "PATCH", body: JSON.stringify(body) }, + ); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function DELETE(request: NextRequest, { params }: Params) { + const { orgId, projectId } = await params; + try { + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}`, + { method: "DELETE" }, + ); + if (status === 204) return new NextResponse(null, { status: 204 }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/api/users/me/route.ts b/app/api/users/me/route.ts index f910e1c..6b9fe0f 100644 --- a/app/api/users/me/route.ts +++ b/app/api/users/me/route.ts @@ -1,10 +1,17 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { setRoleCookieFromBody } from "@/app/lib/authCookie"; export async function GET(request: NextRequest) { try { const { status, data } = await apiClient(request, "/api/v1/users/me"); - return NextResponse.json(data, { status }); + const res = NextResponse.json(data, { status }); + + if (status >= 200 && status < 300) { + setRoleCookieFromBody(res, data); + } + + return res; } catch { return NextResponse.json( { error: "Failed to connect to backend" }, diff --git a/app/components/Field.tsx b/app/components/Field.tsx index f6a3a99..7d90b0b 100644 --- a/app/components/Field.tsx +++ b/app/components/Field.tsx @@ -48,7 +48,7 @@ export default function Field({ + +
+
+
); } - -function getToastStyles(type: ToastType) { - switch (type) { - case "success": - return { - bg: "#f0fdf4", - border: "#86efac", - text: "#15803d", - icon: "#16a34a", - }; - case "error": - return { - bg: "#fef2f2", - border: "#fca5a5", - text: "#b91c1c", - icon: "#dc2626", - }; - case "warning": - return { - bg: "#fffbeb", - border: "#fcd34d", - text: "#b45309", - icon: "#f59e0b", - }; - case "info": - default: - return { - bg: "#eff6ff", - border: "#93c5fd", - text: "#1e40af", - icon: "#3b82f6", - }; - } -} diff --git a/app/components/icons/common/ErrorCircleIcon.tsx b/app/components/icons/common/ErrorCircleIcon.tsx new file mode 100644 index 0000000..a0f604e --- /dev/null +++ b/app/components/icons/common/ErrorCircleIcon.tsx @@ -0,0 +1,23 @@ +interface IconProps { + className?: string; + style?: React.CSSProperties; +} + +export default function ErrorCircleIcon({ className, style }: IconProps) { + return ( + + + + ); +} diff --git a/app/components/icons/document/CloseIcon.tsx b/app/components/icons/document/CloseIcon.tsx index af939c2..59b151c 100644 --- a/app/components/icons/document/CloseIcon.tsx +++ b/app/components/icons/document/CloseIcon.tsx @@ -1,8 +1,9 @@ interface IconProps { className?: string; + style?: React.CSSProperties; } -export default function CloseIcon({ className }: IconProps) { +export default function CloseIcon({ className, style }: IconProps) { return ( { - const params = new URLSearchParams(); - if (currentConfigId && currentConfigVersion) { - params.set("config", currentConfigId); - params.set("version", currentConfigVersion.toString()); - } - if (datasetId) params.set("dataset", datasetId); - if (experimentName) params.set("experiment", experimentName); - - router.push(`/evaluations?${params.toString()}`); - }; - return ( - - {fromEvaluations ? : } - {fromEvaluations ? "Back to Evaluation" : "Run Evaluation"} - - } - > +
+
+ +
+
+ ); +} diff --git a/app/components/settings/SettingsSidebar.tsx b/app/components/settings/SettingsSidebar.tsx index e2cc889..e0598e5 100644 --- a/app/components/settings/SettingsSidebar.tsx +++ b/app/components/settings/SettingsSidebar.tsx @@ -6,6 +6,7 @@ import Image from "next/image"; import { ArrowLeftIcon, KeyIcon, SlidersIcon } from "@/app/components/icons"; import { SETTINGS_NAV } from "@/app/lib/navConfig"; import { useAuth } from "@/app/lib/context/AuthContext"; +import { useApp } from "@/app/lib/context/AppContext"; import { Branding, UserMenuPopover } from "@/app/components/user-menu"; const iconMap: Record = { @@ -17,6 +18,7 @@ export default function SettingsSidebar() { const router = useRouter(); const pathname = usePathname(); const { currentUser, googleProfile, isAuthenticated, logout } = useAuth(); + const { sidebarCollapsed } = useApp(); const [showUserMenu, setShowUserMenu] = useState(false); const userMenuRef = useRef(null); @@ -41,7 +43,9 @@ export default function SettingsSidebar() { .toUpperCase(); return ( -