This commit is contained in:
jesus-berri 2026-10-20 01:21:09 +00:00 • committed by GitHub
commit 4c3c91be05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 339 additions and 25 deletions

View file

@ -1,5 +1,7 @@
import { useAccessGroupDetails } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails";
import { AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
import { useUserLookup } from "@/app/(dashboard)/hooks/users/useUsers";
import { DEFAULT_PROXY_ADMIN_USER_ID } from "@/utils/sentinels";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
@ -7,6 +9,7 @@ import { renderWithProviders } from "../../../../../tests/test-utils";
import { AccessGroupDetail } from "./AccessGroupsDetailsPage";
vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails");
vi.mock("@/app/(dashboard)/hooks/users/useUsers");
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("./AccessGroupsModal/AccessGroupEditModal", () => ({
AccessGroupEditModal: ({ visible, onCancel }: { visible: boolean; onCancel: () => void }) =>
@ -18,6 +21,7 @@ vi.mock("./AccessGroupsModal/AccessGroupEditModal", () => ({
}));
const mockUseAccessGroupDetails = vi.mocked(useAccessGroupDetails);
const mockUseUserLookup = vi.mocked(useUserLookup);
const baseMockReturnValue = {
data: undefined,
@ -89,6 +93,7 @@ describe("AccessGroupDetail", () => {
...baseMockReturnValue,
data: createMockAccessGroup(),
} as ReturnType<typeof useAccessGroupDetails>);
mockUseUserLookup.mockReturnValue({ data: null } as ReturnType<typeof useUserLookup>);
});
it("should render the component", () => {
@ -144,6 +149,64 @@ describe("AccessGroupDetail", () => {
expect(screen.getByText("A test access group")).toBeInTheDocument();
});
describe("user references", () => {
it("shows a user's alias as a linked name and reveals the id in a tooltip", async () => {
const user = userEvent.setup();
mockUseUserLookup.mockReturnValue({
data: { user_alias: "Alice", user_email: "alice@example.com" },
} as ReturnType<typeof useUserLookup>);
renderWith({ created_by: "user-123" });
const link = screen.getByRole("link", { name: "Alice" });
expect(link).toHaveAttribute("href", expect.stringContaining("/users?user=user-123"));
expect(screen.queryByText("user-123")).not.toBeInTheDocument();
await user.hover(link);
expect(await screen.findByText("user-123")).toBeInTheDocument();
});
it("shows the email when the user has no alias", () => {
mockUseUserLookup.mockReturnValue({
data: { user_alias: null, user_email: "alice@example.com" },
} as ReturnType<typeof useUserLookup>);
renderWith({ updated_by: "user-456" });
expect(screen.getByRole("link", { name: "alice@example.com" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=user-456"),
);
});
it("falls back to the email when the alias contains only whitespace", () => {
mockUseUserLookup.mockReturnValue({
data: { user_alias: " ", user_email: "alice@example.com" },
} as ReturnType<typeof useUserLookup>);
renderWith({ created_by: "user-789" });
expect(screen.getByRole("link", { name: "alice@example.com" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=user-789"),
);
});
it("links the user id when lookup has no user details", () => {
renderWith({ updated_by: "missing-user" });
expect(screen.getByRole("link", { name: "missing-user" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=missing-user"),
);
});
it("shows the default admin badge without looking up its user id", () => {
renderWith({ created_by: DEFAULT_PROXY_ADMIN_USER_ID });
expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument();
expect(mockUseUserLookup).toHaveBeenCalledWith(null);
});
});
it("should display em dash when description is empty", () => {
renderWith({ description: null });

View file

@ -10,9 +10,9 @@ import {
UsersIcon,
} from "lucide-react";
import { useState } from "react";
import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag";
import { BadgeLink } from "@/components/shared/BadgeLink";
import CopyButton from "@/components/shared/CopyButton";
import { UserReference } from "@/components/shared/UserReference";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@ -177,7 +177,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
{accessGroup.created_by && (
<>
<span>by</span>
<DefaultProxyAdminTag userId={accessGroup.created_by} />
<UserReference userId={accessGroup.created_by} />
</>
)}
</dd>
@ -187,7 +187,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr
{accessGroup.updated_by && (
<>
<span>by</span>
<DefaultProxyAdminTag userId={accessGroup.updated_by} />
<UserReference userId={accessGroup.updated_by} />
</>
)}
</dd>

View file

@ -3,10 +3,12 @@ import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import React, { ReactNode } from "react";
import { useInfiniteUsers, useUserEmailLookup, useUserLookup } from "./useUsers";
import { userListCall } from "@/components/networking";
import type { UserListResponse } from "@/components/networking";
import { userGetInfoV2, userListCall } from "@/components/networking";
import type { UserInfoV2Response, UserListResponse } from "@/components/networking";
import { ApiError } from "@/lib/http/client";
vi.mock("@/components/networking", () => ({
userGetInfoV2: vi.fn(),
userListCall: vi.fn(),
}));
@ -56,6 +58,23 @@ const buildUserListResponse = (page: number, totalPages: number, userCount = 2):
})),
});
const buildUserInfoV2Response = (userId: string): UserInfoV2Response => ({
user_id: userId,
user_email: "alice@example.com",
user_alias: "Alice",
user_role: "Internal User",
spend: 0,
max_budget: null,
models: [],
budget_duration: null,
budget_reset_at: null,
metadata: null,
created_at: null,
updated_at: null,
sso_user_id: null,
teams: [],
});
describe("useInfiniteUsers", () => {
let queryClient: QueryClient;
@ -299,22 +318,60 @@ describe("useUserLookup", () => {
React.createElement(QueryClientProvider, { client: queryClient }, children);
it("fetches exactly the requested user by id", async () => {
vi.mocked(userListCall).mockResolvedValue(buildUserListResponse(1, 1, 1));
const exactUser = buildUserInfoV2Response("user-1-0");
vi.mocked(userGetInfoV2).mockResolvedValue(exactUser);
const { result } = renderHook(() => useUserLookup("user-1-0"), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(userListCall).toHaveBeenCalledWith("test-access-token", ["user-1-0"], 1, 1);
expect(userGetInfoV2).toHaveBeenCalledWith("test-access-token", "user-1-0");
expect(userListCall).not.toHaveBeenCalled();
expect(result.current.data).toMatchObject(exactUser);
expect(result.current.data?.user_id).toBe("user-1-0");
});
it("resolves to null when the proxy returns a different user than requested", async () => {
vi.mocked(userListCall).mockResolvedValue(buildUserListResponse(1, 1, 1));
it("falls back to the exact user list match when the user info endpoint returns a 404", async () => {
const response = buildUserListResponse(1, 1);
const exactUser = { ...response.users[1], user_id: "bob", user_alias: "Bob" };
vi.mocked(userListCall).mockResolvedValue({
...response,
page_size: 100,
users: [{ ...response.users[0], user_id: "bob2" }, exactUser],
});
vi.mocked(userGetInfoV2).mockRejectedValue(new ApiError("Not Found", 404, {}));
const { result } = renderHook(() => useUserLookup("someone-else"), { wrapper });
const { result } = renderHook(() => useUserLookup("bob"), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toMatchObject(exactUser);
expect(userListCall).toHaveBeenCalledWith("test-access-token", ["bob"], 1, 100);
});
it("resolves to null when the user list contains only a substring match", async () => {
const response = buildUserListResponse(1, 1);
vi.mocked(userListCall).mockResolvedValue({
...response,
page_size: 100,
users: [{ ...response.users[0], user_id: "bob2" }],
});
vi.mocked(userGetInfoV2).mockRejectedValue(new ApiError("Not Found", 404, {}));
const { result } = renderHook(() => useUserLookup("bob"), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toBeNull();
expect(userListCall).toHaveBeenCalledWith("test-access-token", ["bob"], 1, 100);
});
it("rethrows errors other than 404", async () => {
const serverError = new ApiError("Internal Server Error", 500, {});
vi.mocked(userGetInfoV2).mockRejectedValue(serverError);
const { result } = renderHook(() => useUserLookup("user-1-0"), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toBe(serverError);
expect(userListCall).not.toHaveBeenCalled();
});
it("does not query without a user id", async () => {
@ -322,7 +379,7 @@ describe("useUserLookup", () => {
await new Promise((resolve) => setTimeout(resolve, 20));
expect(result.current.fetchStatus).toBe("idle");
expect(userListCall).not.toHaveBeenCalled();
expect(userGetInfoV2).not.toHaveBeenCalled();
});
it("does not query for a non-admin role", async () => {
@ -332,7 +389,7 @@ describe("useUserLookup", () => {
await new Promise((resolve) => setTimeout(resolve, 20));
expect(result.current.fetchStatus).toBe("idle");
expect(userListCall).not.toHaveBeenCalled();
expect(userGetInfoV2).not.toHaveBeenCalled();
});
});

View file

@ -1,14 +1,43 @@
import { userListCall, UserInfo, UserListResponse } from "@/components/networking";
import { userGetInfoV2, userListCall } from "@/components/networking";
import type { UserInfo, UserInfoV2Response, UserListResponse } from "@/components/networking";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { createQueryKeys } from "../common/queryKeysFactory";
import { canListUsers } from "@/utils/roles";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { ApiError } from "@/lib/http/client";
const infiniteUsersKeys = createQueryKeys("infiniteUsers");
const userLookupKeys = createQueryKeys("userLookup");
const DEFAULT_PAGE_SIZE = 50;
const mapUserInfoV2ToUserInfo = (user: UserInfoV2Response): UserInfo => {
const userInfo: UserInfo = {
user_id: user.user_id,
user_email: user.user_email,
user_alias: user.user_alias,
user_role: user.user_role,
spend: user.spend,
max_budget: user.max_budget,
models: user.models,
budget_duration: user.budget_duration,
budget_reset_at: user.budget_reset_at,
metadata: user.metadata,
created_at: user.created_at,
updated_at: user.updated_at,
sso_user_id: user.sso_user_id,
teams: user.teams,
tpm_limit: user.tpm_limit,
rpm_limit: user.rpm_limit,
allowed_cache_controls: [],
key_count: 0,
model_max_budget: null,
model_spend: null,
policies: [],
};
return userInfo;
};
export const useInfiniteUsers = (pageSize: number = DEFAULT_PAGE_SIZE, searchEmail?: string) => {
const { accessToken, userRole } = useAuthorized();
return useInfiniteQuery<UserListResponse>({
@ -61,8 +90,15 @@ export const useUserLookup = (userId: string | null) => {
return useQuery<UserInfo | null>({
queryKey: userLookupKeys.detail(userId ?? ""),
queryFn: async () => {
const response = await userListCall(accessToken!, [userId!], 1, 1);
return response.users.find((user) => user.user_id === userId) ?? null;
try {
return mapUserInfoV2ToUserInfo(await userGetInfoV2(accessToken!, userId!));
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
const response = await userListCall(accessToken!, [userId!], 1, USER_LIST_MAX_PAGE_SIZE);
return response.users.find((user) => user.user_id === userId) ?? null;
}
throw error;
}
},
enabled: Boolean(accessToken) && Boolean(userId) && canListUsers(userRole),
});

View file

@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen } from "../../../../../tests/test-utils";
import { ProjectDetail } from "./ProjectDetailsPage";
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
import { useUserLookup } from "@/app/(dashboard)/hooks/users/useUsers";
const mockUseProjectDetails = vi.fn();
vi.mock("@/app/(dashboard)/hooks/projects/useProjectDetails", () => ({
@ -24,12 +25,18 @@ vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
useUISettings: () => mockUseUISettings(),
}));
vi.mock("./ProjectModals/EditProjectModal", () => ({
EditProjectModal: ({ isOpen }: { isOpen: boolean }) => (isOpen ? <div data-testid="edit-modal" /> : null),
vi.mock("@/app/(dashboard)/hooks/users/useUsers", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/app/(dashboard)/hooks/users/useUsers")>();
return { ...actual, useUserLookup: vi.fn() };
});
vi.mock("next/navigation", async (importOriginal) => ({
...(await importOriginal<typeof import("next/navigation")>()),
useRouter: () => ({ push: vi.fn() }),
}));
vi.mock("@/components/common_components/DefaultProxyAdminTag", () => ({
default: ({ userId }: { userId: string }) => <span>{userId}</span>,
vi.mock("./ProjectModals/EditProjectModal", () => ({
EditProjectModal: ({ isOpen }: { isOpen: boolean }) => (isOpen ? <div data-testid="edit-modal" /> : null),
}));
vi.mock("./ProjectKeysSection", () => ({
@ -69,9 +76,11 @@ const yAxisTickLabels = (container: HTMLElement) =>
describe("ProjectDetail", () => {
const onBack = vi.fn();
const mockUseUserLookup = vi.mocked(useUserLookup);
beforeEach(() => {
vi.clearAllMocks();
mockUseUserLookup.mockReturnValue({ data: null } as ReturnType<typeof useUserLookup>);
mockUseTeam.mockReturnValue({ data: undefined, isLoading: false });
mockUseAuthorized.mockReturnValue({ userId: "admin-user", userRole: "Admin", isViewOnly: false });
mockUseUISettings.mockReturnValue({ data: { values: {} } });
@ -113,6 +122,21 @@ describe("ProjectDetail", () => {
expect(screen.getByText("My Project")).toBeInTheDocument();
});
it("shows the creator alias as a link to the user page", () => {
mockUseUserLookup.mockImplementation(
(userId) =>
({
data: userId === "user-1" ? { user_alias: "Alice", user_email: "alice@example.com" } : null,
}) as ReturnType<typeof useUserLookup>,
);
renderWithProviders(<ProjectDetail projectId="proj-1" onBack={onBack} />);
expect(screen.getByRole("link", { name: "Alice" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=user-1"),
);
});
it("should display the project alias as the page title", () => {
renderWithProviders(<ProjectDetail projectId="proj-1" onBack={onBack} />);
expect(screen.getByRole("heading", { name: "My Project" })).toBeInTheDocument();

View file

@ -6,8 +6,8 @@ import { isUserTeamAdminForSingleTeam } from "@/utils/roles";
import { BarChart } from "@/components/shared/charts";
import { ArrowLeftIcon, DollarSignIcon, EditIcon, UsersIcon } from "lucide-react";
import { useMemo, useState } from "react";
import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag";
import CopyButton from "@/components/shared/CopyButton";
import { UserReference } from "@/components/shared/UserReference";
import { StatusBadge } from "@/components/shared/table_cells/status_badge";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@ -116,7 +116,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
{project.created_by && (
<>
<span>by</span>
<DefaultProxyAdminTag userId={project.created_by} />
<UserReference userId={project.created_by} />
</>
)}
</dd>
@ -126,7 +126,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
{project.updated_by && (
<>
<span>by</span>
<DefaultProxyAdminTag userId={project.updated_by} />
<UserReference userId={project.updated_by} />
</>
)}
</dd>

View file

@ -4,6 +4,7 @@ import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import PromptInfoView from "./prompt_info";
import * as networking from "@/components/networking";
import { useUserLookup } from "@/app/(dashboard)/hooks/users/useUsers";
vi.mock("@/components/networking", () => ({
getPromptInfo: vi.fn(),
@ -17,6 +18,22 @@ vi.mock("./prompt_editor_view/PromptCodeSnippets", () => ({
),
}));
vi.mock("@/app/(dashboard)/hooks/users/useUsers", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/app/(dashboard)/hooks/users/useUsers")>();
return { ...actual, useUserLookup: vi.fn() };
});
vi.mock("next/navigation", async (importOriginal) => ({
...(await importOriginal<typeof import("next/navigation")>()),
useRouter: () => ({ push: vi.fn() }),
}));
const mockUseUserLookup = vi.mocked(useUserLookup);
beforeEach(() => {
mockUseUserLookup.mockReturnValue({ data: null } as ReturnType<typeof useUserLookup>);
});
const promptWithoutTemplate = {
prompt_spec: {
prompt_id: "support-reply",
@ -58,6 +75,25 @@ describe("PromptInfoView environment scoping", () => {
await screen.findByRole("tab", { name: "Raw JSON" });
expect(networking.getPromptInfo).toHaveBeenCalledWith("sk-test", "support-reply", undefined);
});
it("shows the creator alias as a link to the user page", async () => {
mockUseUserLookup.mockReturnValue({
data: { user_alias: "Alice", user_email: "alice@example.com" },
} as ReturnType<typeof useUserLookup>);
vi.mocked(networking.getPromptInfo)
.mockReset()
.mockResolvedValue({
...promptWithoutTemplate,
prompt_spec: { ...promptWithoutTemplate.prompt_spec, created_by: "prompt-user-1" },
});
render(<PromptInfoView promptId="support-reply" onClose={vi.fn()} accessToken="sk-test" isAdmin={true} />);
expect(await screen.findByRole("link", { name: "Alice" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=prompt-user-1"),
);
});
});
describe("PromptInfoView code snippets", () => {

View file

@ -14,6 +14,7 @@ import {
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { ArrowLeft, CheckIcon, CopyIcon, Pencil, Trash2 } from "lucide-react";
import { toast } from "@/lib/toast";
import { UserReference } from "@/components/shared/UserReference";
import PromptCodeSnippets from "./prompt_editor_view/PromptCodeSnippets";
import { extractModel, extractTemplateVariables, getBasePromptId, getCurrentVersion } from "./prompt_utils";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@ -329,7 +330,9 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({
<Card className="block p-6">
<p>Created By</p>
<div className="mt-2">
<h3 className="text-sm font-medium">{promptData.created_by || "-"}</h3>
<h3 className="text-sm font-medium">
{promptData.created_by ? <UserReference userId={promptData.created_by} /> : "-"}
</h3>
</div>
</Card>

View file

@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import ModelInfoView from "./model_info_view";
import { toast } from "@/lib/toast";
import * as networking from "./networking";
import { useUserLookup } from "@/app/(dashboard)/hooks/users/useUsers";
vi.mock("./model_info/KubernetesPodsCard", () => ({
default: ({ modelId }: { modelId: string }) => <div data-testid="kubernetes-pods-card">{modelId}</div>,
}));
@ -50,6 +51,16 @@ vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
useTeams: () => mockUseTeams(),
}));
vi.mock("@/app/(dashboard)/hooks/users/useUsers", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/app/(dashboard)/hooks/users/useUsers")>();
return { ...actual, useUserLookup: vi.fn() };
});
vi.mock("next/navigation", async (importOriginal) => ({
...(await importOriginal<typeof import("next/navigation")>()),
useRouter: () => ({ push: vi.fn() }),
}));
const mockUsePtuCostAttributionEnabled = vi.fn();
vi.mock("@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled", () => ({
usePtuCostAttributionEnabled: () => mockUsePtuCostAttributionEnabled(),
@ -109,6 +120,7 @@ describe("ModelInfoView", () => {
},
});
vi.clearAllMocks();
vi.mocked(useUserLookup).mockReturnValue({ data: null } as ReturnType<typeof useUserLookup>);
mockUsePtuCostAttributionEnabled.mockReturnValue(false);
mockUseTeams.mockReturnValue({ data: undefined, isLoading: false, error: null });
@ -1459,6 +1471,18 @@ describe("ModelInfoView", () => {
});
});
it("shows the creator alias as a link to the user page", async () => {
vi.mocked(useUserLookup).mockReturnValue({
data: { user_alias: "Alice", user_email: "alice@example.com" },
} as ReturnType<typeof useUserLookup>);
render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { wrapper });
expect(await screen.findByRole("link", { name: "Alice" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=123"),
);
});
describe("team alias", () => {
const teamModel = {
...defaultModelData,

View file

@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { UserReference } from "@/components/shared/UserReference";
import { applyPtuModelInfo } from "../utils/ptuModelInfo";
import { usePtuCostAttributionEnabled } from "@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled";
import { ArrowLeft, CheckIcon, CopyIcon, Info } from "lucide-react";
@ -677,7 +678,12 @@ export default function ModelInfoView({
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
Created By {modelData.model_info.created_by || "Not Set"}
Created By{" "}
{modelData.model_info.created_by ? (
<UserReference userId={modelData.model_info.created_by} />
) : (
"Not Set"
)}
</div>
</div>

View file

@ -7,6 +7,7 @@ import { vi, test, expect, beforeEach, describe, type Mock } from "vitest";
import { renderWithProviders, testQueryClient } from "../../../tests/test-utils";
import OrganizationInfoView from "./organization_view";
import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { useUserLookup } from "@/app/(dashboard)/hooks/users/useUsers";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
@ -37,7 +38,13 @@ vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
},
}));
vi.mock("@/app/(dashboard)/hooks/users/useUsers", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/app/(dashboard)/hooks/users/useUsers")>();
return { ...actual, useUserLookup: vi.fn() };
});
const mockUseOrganization = vi.mocked(useOrganization);
const mockUseUserLookup = vi.mocked(useUserLookup);
// Mock noisy/heavy child components to keep this test focused on render
vi.mock("../object_permissions_view", () => ({
@ -103,6 +110,7 @@ const mockOrg = {
beforeEach(() => {
mockUseOrganization.mockReset();
mockUseUserLookup.mockReturnValue({ data: null } as ReturnType<typeof useUserLookup>);
});
test("renders organization view after loading data", async () => {
@ -123,6 +131,32 @@ test("renders organization view after loading data", async () => {
expect(orgName).toBeInTheDocument();
});
test("shows the creator alias as a link to the user page", async () => {
mockUseOrganization.mockReturnValue({
data: { ...mockOrg, created_by: "org-user-1" },
isLoading: false,
} as unknown as ReturnType<typeof useOrganization>);
mockUseUserLookup.mockReturnValue({
data: { user_alias: "Alice", user_email: "alice@example.com" },
} as ReturnType<typeof useUserLookup>);
renderWithProviders(
<OrganizationInfoView
organizationId="org_123"
onClose={() => {}}
accessToken="test-token"
is_org_admin={false}
is_proxy_admin={false}
userModels={[]}
/>,
);
expect(await screen.findByRole("link", { name: "Alice" })).toHaveAttribute(
"href",
expect.stringContaining("/users?user=org-user-1"),
);
});
test("should display empty state when organization has no members", async () => {
mockUseOrganization.mockReturnValue({ data: mockOrg, isLoading: false } as any);

View file

@ -5,6 +5,7 @@ import { useUrlTab } from "@/hooks/useUrlTab";
import { useVisitedTabs } from "@/hooks/useVisitedTabs";
import { MoneyCell } from "@/components/shared/table_cells";
import CopyButton from "@/components/shared/CopyButton";
import { UserReference } from "@/components/shared/UserReference";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@ -186,7 +187,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
<div className="mt-2 text-sm text-foreground">
<p>Created: {new Date(orgData.created_at).toLocaleDateString()}</p>
<p>Updated: {new Date(orgData.updated_at).toLocaleDateString()}</p>
<p>Created By: {orgData.created_by}</p>
<p>Created By: {orgData.created_by && <UserReference userId={orgData.created_by} />}</p>
</div>
</CardContent>
</Card>

View file

@ -0,0 +1,30 @@
"use client";
import { useUserLookup } from "@/app/(dashboard)/hooks/users/useUsers";
import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag";
import { EntityLink } from "@/components/shared/EntityLink";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { DEFAULT_PROXY_ADMIN_USER_ID } from "@/utils/sentinels";
import { userDetailHref } from "@/utils/entityLinks";
export function UserReference({ userId }: { userId: string }) {
const isDefaultAdmin = userId === DEFAULT_PROXY_ADMIN_USER_ID;
const { data: user } = useUserLookup(isDefaultAdmin ? null : userId);
if (isDefaultAdmin) {
return <DefaultProxyAdminTag userId={userId} />;
}
const name = user?.user_alias?.trim() || user?.user_email?.trim() || null;
const href = userDetailHref(userId);
if (!name) {
return <EntityLink href={href}>{userId}</EntityLink>;
}
return (
<SimpleTooltip content={userId}>
<EntityLink href={href}>{name}</EntityLink>
</SimpleTooltip>
);
}