diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ef78befd85..a197ade4c5 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -626,6 +626,13 @@ class ApiMethods { return response.data; }; + getUser = async (userIdOrName: string): Promise => { + const response = await this.axios.get( + `/api/v2/users/${encodeURIComponent(userIdOrName)}`, + ); + return response.data; + }; + /** * Get users for workspace owner selection. Requires * permission to create workspaces for other users in the diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index c0c81c4701..753b053cce 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -33,6 +33,15 @@ export function usersKey(req: UsersRequest) { return ["users", req] as const; } +export const userByNameKey = (username: string) => ["user", username] as const; + +export const userByName = (username: string): UseQueryOptions => { + return { + queryKey: userByNameKey(username), + queryFn: () => API.getUser(username), + }; +}; + export function paginatedUsers( searchParams: URLSearchParams, ): UsePaginatedQueryOptions { diff --git a/site/src/pages/AgentsPage/ChatModelAdminPanel/ModelsSection.tsx b/site/src/pages/AgentsPage/ChatModelAdminPanel/ModelsSection.tsx index ab381af9e6..3ea00aaeff 100644 --- a/site/src/pages/AgentsPage/ChatModelAdminPanel/ModelsSection.tsx +++ b/site/src/pages/AgentsPage/ChatModelAdminPanel/ModelsSection.tsx @@ -19,7 +19,8 @@ import { StarIcon, TriangleAlertIcon, } from "lucide-react"; -import { type FC, type ReactNode, useState } from "react"; +import { type FC, type ReactNode, useMemo } from "react"; +import { useLocation, useNavigate, useSearchParams } from "react-router"; import { cn } from "utils/cn"; import { SectionHeader } from "../SectionHeader"; import type { ProviderState } from "./ChatModelAdminPanel"; @@ -72,7 +73,61 @@ export const ModelsSection: FC = ({ onUpdateModel, onDeleteModel, }) => { - const [view, setView] = useState({ mode: "list" }); + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + const location = useLocation(); + + // Whether the current form entry was pushed by an in-app click + // (as opposed to a direct-entry URL like a bookmark or shared link). + // When true, navigate(-1) is safe; otherwise we fall back to + // clearing params with replace to avoid leaving the app. + const canGoBack = + (location.state as { pushed?: boolean } | null)?.pushed === true; + + // Derive the current view from URL search params so that + // browser back/forward navigation works as expected. + const view: ModelView = useMemo(() => { + const editModelId = searchParams.get("model"); + if (editModelId) { + const model = modelConfigs.find((m) => m.id === editModelId); + return model ? { mode: "edit", model } : { mode: "list" }; + } + const addProvider = searchParams.get("newModel"); + if (addProvider) { + return { mode: "add", provider: addProvider }; + } + return { mode: "list" }; + }, [searchParams, modelConfigs]); + + // Clear model-related search params and return to the list. + const clearModelView = () => { + setSearchParams((prev) => { + const next = new URLSearchParams(prev); + next.delete("model"); + next.delete("newModel"); + return next; + }); + }; + + // Navigate back to the list after a destructive or + // completion action (create/delete) where the form entry + // is stale. Uses navigate(-1) when safe, otherwise clears + // the params with replace. + const exitModelView = () => { + if (canGoBack) { + navigate(-1); + } else { + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + next.delete("model"); + next.delete("newModel"); + return next; + }, + { replace: true }, + ); + } + }; // When the form is open it takes over the full panel. if (view.mode === "add" || view.mode === "edit") { @@ -106,18 +161,18 @@ export const ModelsSection: FC = ({ isDeleting={isDeleting} onCreateModel={async (req) => { await onCreateModel(req); - setView({ mode: "list" }); + exitModelView(); }} onUpdateModel={async (id, req) => { await onUpdateModel(id, req); - setView({ mode: "list" }); + clearModelView(); }} - onCancel={() => setView({ mode: "list" })} + onCancel={clearModelView} onDeleteModel={ editingModel ? async (id) => { await onDeleteModel(id); - setView({ mode: "list" }); + exitModelView(); } : undefined } @@ -147,7 +202,10 @@ export const ModelsSection: FC = ({ key={ps.provider} onClick={() => { onSelectedProviderChange(ps.provider); - setView({ mode: "add", provider: ps.provider }); + setSearchParams( + { newModel: ps.provider }, + { state: { pushed: true } }, + ); }} className="gap-2" > @@ -240,7 +298,12 @@ export const ModelsSection: FC = ({ {/* Clickable row content */} + ); + + if (selectedUserQuery.isLoading) { + return ( +
+
+ {backButton} + {header} +
+
+ +
+
+ ); + } + + if (selectedUserQuery.isError || !selectedUser) { + return ( +
+
+ {backButton} + {header} +
+
+

+ {getErrorMessage( + selectedUserQuery.error, + "Failed to load user profile.", + )} +

+ +
+
+ ); + } + return (
- + {backButton} {header}
@@ -198,11 +260,10 @@ const UsageContent: FC = ({ now }) => { imgFallbackText={selectedUser.username} />
-
User ID: {selectedUser.user_id}
+
User ID: {selectedUser.id}
{dateRange.rangeLabel}
- = ({ now }) => { { + setSearchParams({ user: u.user_id }); + }} /> ))}