feat(auth,ui): support github sign-in in the UI (#2545)

# What does this PR do?
Uses NextAuth to add github sign in support.

## Test Plan
Start server with auth configured as in
https://github.com/meta-llama/llama-stack/pull/2509


https://github.com/user-attachments/assets/61ff7442-f601-4b39-8686-5d0afb3b45ac
This commit is contained in:
ehhuang 2025-07-08 11:02:57 -07:00 committed by GitHub
parent c8bac888af
commit daf660c4ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 577 additions and 81 deletions

View file

@ -12,24 +12,34 @@ jest.mock("next/navigation", () => ({
}),
}));
// Mock next-auth
jest.mock("next-auth/react", () => ({
useSession: () => ({
status: "authenticated",
data: { accessToken: "mock-token" },
}),
}));
// Mock helper functions
jest.mock("@/lib/truncate-text");
jest.mock("@/lib/format-message-content");
// Mock the client
jest.mock("@/lib/client", () => ({
client: {
chat: {
completions: {
list: jest.fn(),
},
// Mock the auth client hook
const mockClient = {
chat: {
completions: {
list: jest.fn(),
},
},
};
jest.mock("@/hooks/use-auth-client", () => ({
useAuthClient: () => mockClient,
}));
// Mock the usePagination hook
const mockLoadMore = jest.fn();
jest.mock("@/hooks/usePagination", () => ({
jest.mock("@/hooks/use-pagination", () => ({
usePagination: jest.fn(() => ({
data: [],
status: "idle",
@ -47,7 +57,7 @@ import {
} from "@/lib/format-message-content";
// Import the mocked hook
import { usePagination } from "@/hooks/usePagination";
import { usePagination } from "@/hooks/use-pagination";
const mockedUsePagination = usePagination as jest.MockedFunction<
typeof usePagination
>;

View file

@ -10,8 +10,7 @@ import {
extractTextFromContentPart,
extractDisplayableText,
} from "@/lib/format-message-content";
import { usePagination } from "@/hooks/usePagination";
import { client } from "@/lib/client";
import { usePagination } from "@/hooks/use-pagination";
interface ChatCompletionsTableProps {
/** Optional pagination configuration */
@ -32,12 +31,15 @@ function formatChatCompletionToRow(completion: ChatCompletion): LogTableRow {
export function ChatCompletionsTable({
paginationOptions,
}: ChatCompletionsTableProps) {
const fetchFunction = async (params: {
after?: string;
limit: number;
model?: string;
order?: string;
}) => {
const fetchFunction = async (
client: ReturnType<typeof import("@/hooks/use-auth-client").useAuthClient>,
params: {
after?: string;
limit: number;
model?: string;
order?: string;
},
) => {
const response = await client.chat.completions.list({
after: params.after,
limit: params.limit,

View file

@ -12,7 +12,7 @@ jest.mock("next/navigation", () => ({
}));
// Mock the useInfiniteScroll hook
jest.mock("@/hooks/useInfiniteScroll", () => ({
jest.mock("@/hooks/use-infinite-scroll", () => ({
useInfiniteScroll: jest.fn((onLoadMore, options) => {
const ref = React.useRef(null);

View file

@ -4,7 +4,7 @@ import { useRouter } from "next/navigation";
import { useRef } from "react";
import { truncateText } from "@/lib/truncate-text";
import { PaginationStatus } from "@/lib/types";
import { useInfiniteScroll } from "@/hooks/useInfiniteScroll";
import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
import {
Table,
TableBody,

View file

@ -0,0 +1,7 @@
"use client";
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
export function SessionProvider({ children }: { children: React.ReactNode }) {
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
}

View file

@ -12,21 +12,31 @@ jest.mock("next/navigation", () => ({
}),
}));
// Mock next-auth
jest.mock("next-auth/react", () => ({
useSession: () => ({
status: "authenticated",
data: { accessToken: "mock-token" },
}),
}));
// Mock helper functions
jest.mock("@/lib/truncate-text");
// Mock the client
jest.mock("@/lib/client", () => ({
client: {
responses: {
list: jest.fn(),
},
// Mock the auth client hook
const mockClient = {
responses: {
list: jest.fn(),
},
};
jest.mock("@/hooks/use-auth-client", () => ({
useAuthClient: () => mockClient,
}));
// Mock the usePagination hook
const mockLoadMore = jest.fn();
jest.mock("@/hooks/usePagination", () => ({
jest.mock("@/hooks/use-pagination", () => ({
usePagination: jest.fn(() => ({
data: [],
status: "idle",
@ -40,7 +50,7 @@ jest.mock("@/hooks/usePagination", () => ({
import { truncateText as originalTruncateText } from "@/lib/truncate-text";
// Import the mocked hook
import { usePagination } from "@/hooks/usePagination";
import { usePagination } from "@/hooks/use-pagination";
const mockedUsePagination = usePagination as jest.MockedFunction<
typeof usePagination
>;

View file

@ -6,8 +6,7 @@ import {
UsePaginationOptions,
} from "@/lib/types";
import { LogsTable, LogTableRow } from "@/components/logs/logs-table";
import { usePagination } from "@/hooks/usePagination";
import { client } from "@/lib/client";
import { usePagination } from "@/hooks/use-pagination";
import type { ResponseListResponse } from "llama-stack-client/resources/responses/responses";
import {
isMessageInput,
@ -125,12 +124,15 @@ function formatResponseToRow(response: OpenAIResponse): LogTableRow {
}
export function ResponsesTable({ paginationOptions }: ResponsesTableProps) {
const fetchFunction = async (params: {
after?: string;
limit: number;
model?: string;
order?: string;
}) => {
const fetchFunction = async (
client: ReturnType<typeof import("@/hooks/use-auth-client").useAuthClient>,
params: {
after?: string;
limit: number;
model?: string;
order?: string;
},
) => {
const response = await client.responses.list({
after: params.after,
limit: params.limit,

View file

@ -0,0 +1,25 @@
"use client";
import { User } from "lucide-react";
import Link from "next/link";
import { useSession } from "next-auth/react";
import { Button } from "./button";
export function SignInButton() {
const { data: session, status } = useSession();
return (
<Button variant="ghost" size="sm" asChild>
<Link href="/auth/signin" className="flex items-center">
<User className="mr-2 h-4 w-4" />
<span>
{status === "loading"
? "Loading..."
: session
? session.user?.email || "Signed In"
: "Sign In"}
</span>
</Link>
</Button>
);
}