Skip to content

Commit 4d667b7

Browse files
authored
add to top menu + auth check and form for not logged in users (#6832)
- adds TorchAgent to the top menu - Adds auth check when opening the /torchagent page - adds a google form to request access to torchagent if you don't have a github account / don't have PyTorch write permissions
1 parent fcdc5a5 commit 4d667b7

File tree

4 files changed

+215
-15
lines changed

4 files changed

+215
-15
lines changed

clickhouse_db_schema/misc.torchagent_feedback/schema.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ CREATE TABLE
22
misc.torchagent_feedback (
33
`user` String,
44
`session_id` String,
5-
`torchagent_feedback` String,
5+
`history_key` String,
66
`feedback` Int8,
77
`time_inserted` DateTime64 (0, 'UTC')
88
) ENGINE = SharedMergeTree ('/clickhouse/tables/{uuid}/{shard}', '{replica}')
99
PARTITION BY
1010
toYYYYMM (time_inserted)
1111
ORDER BY
12-
(user, session_id, time_inserted) SETTINGS index_granularity = 8192
12+
(user, session_id, history_key, time_inserted) SETTINGS index_granularity = 8192

torchci/components/NavBar.tsx

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@ const NavBarDropdown = ({
1414
}): JSX.Element => {
1515
const [dropdown, setDropdown] = useState(false);
1616
const dropdownStyle = dropdown ? { display: "block" } : {};
17+
const firstItemHref = items.length > 0 ? items[0].href : "#";
1718

1819
return (
1920
<li
2021
onMouseEnter={() => setDropdown(true)}
2122
onMouseLeave={() => setDropdown(false)}
2223
style={{ padding: 0 }}
2324
>
24-
<div className={styles.dropdowntitle}>{title}</div>
25+
<Link
26+
href={firstItemHref}
27+
prefetch={false}
28+
className={styles.dropdowntitle}
29+
>
30+
{title}
31+
</Link>
2532
<ul className={styles.dropdown} style={dropdownStyle}>
2633
{items.map((item: any) => (
2734
<li key={item.href}>
@@ -131,6 +138,36 @@ function NavBar() {
131138
},
132139
];
133140

141+
const metricsDropdown = [
142+
{
143+
name: "Metrics",
144+
href: "/metrics",
145+
},
146+
{
147+
name: (
148+
<span style={{ position: "relative" }}>
149+
TorchAgent
150+
<span
151+
style={{
152+
marginLeft: "4px",
153+
padding: "2px 6px",
154+
fontSize: "10px",
155+
fontWeight: "bold",
156+
backgroundColor: "#FF6B35",
157+
color: "white",
158+
borderRadius: "8px",
159+
textTransform: "uppercase",
160+
lineHeight: "1",
161+
}}
162+
>
163+
BETA
164+
</span>
165+
</span>
166+
),
167+
href: "/torchagent",
168+
},
169+
];
170+
134171
return (
135172
<div className={styles.navbar}>
136173
<div>
@@ -145,6 +182,11 @@ function NavBar() {
145182
MiniHUD
146183
</Link>
147184
</li>
185+
<li>
186+
<Link prefetch={false} href="/hud/pytorch/pytorch/main">
187+
PyTorch
188+
</Link>
189+
</li>
148190
<li>
149191
<Link prefetch={false} href="/hud/pytorch/executorch/main">
150192
ExecuTorch
@@ -181,11 +223,7 @@ function NavBar() {
181223
Requests
182224
</Link>
183225
</li>
184-
<li>
185-
<Link prefetch={false} href="/metrics">
186-
Metrics
187-
</Link>
188-
</li>
226+
<NavBarDropdown title="Metrics" items={metricsDropdown} />
189227
<li>
190228
<Link prefetch={false} href="/kpis">
191229
KPIs

torchci/components/TorchAgentPage.tsx

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
useMediaQuery,
99
useTheme,
1010
} from "@mui/material";
11-
import { useSession } from "next-auth/react";
11+
import { signIn, useSession } from "next-auth/react";
1212
import { useCallback, useEffect, useRef, useState } from "react";
1313
import AISpinner from "./AISpinner";
1414
import { ChatHistorySidebar } from "./TorchAgentPage/ChatHistorySidebar";
@@ -75,7 +75,7 @@ export const TorchAgentPage = () => {
7575
const isMobile = useMediaQuery(theme.breakpoints.down("lg")); // Below 1200px
7676

7777
// Constants
78-
const typingSpeed = 30;
78+
const typingSpeed = 3; // ms per character
7979
const sidebarWidth = 300;
8080

8181
const featureRequestUrl =
@@ -106,6 +106,9 @@ export const TorchAgentPage = () => {
106106
const [error, setError] = useState("");
107107
const [debugVisible, setDebugVisible] = useState(false);
108108
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
109+
const [permissionState, setPermissionState] = useState<
110+
"unchecked" | "checking" | "sufficient" | "insufficient"
111+
>("unchecked");
109112

110113
const [chatHistory, setChatHistory] = useState<ChatSession[]>([]);
111114
const [selectedSession, setSelectedSession] = useState<string | null>(null);
@@ -202,6 +205,38 @@ export const TorchAgentPage = () => {
202205
}
203206
}, [session.data?.user]);
204207

208+
const checkUserPermissions = useCallback(async () => {
209+
if (
210+
!session.data?.user ||
211+
hasAuthCookie() ||
212+
permissionState !== "unchecked"
213+
)
214+
return;
215+
216+
setPermissionState("checking");
217+
try {
218+
// Make a simple API call to check permissions
219+
const response = await fetch("/api/torchagent-check-permissions", {
220+
method: "GET",
221+
headers: {
222+
"Content-Type": "application/json",
223+
},
224+
});
225+
226+
if (response.status === 403) {
227+
setPermissionState("insufficient");
228+
} else if (!response.ok) {
229+
// For 500 errors or other issues, also show insufficient permissions
230+
setPermissionState("insufficient");
231+
} else {
232+
setPermissionState("sufficient");
233+
}
234+
} catch (error) {
235+
console.error("Error checking permissions:", error);
236+
setPermissionState("insufficient");
237+
}
238+
}, [session.data?.user, permissionState]);
239+
205240
const loadChatSession = async (sessionId: string) => {
206241
// Cancel any active stream first
207242
if (fetchControllerRef.current && isLoading) {
@@ -326,8 +361,17 @@ export const TorchAgentPage = () => {
326361
useEffect(() => {
327362
if (session.data?.user) {
328363
fetchChatHistory();
364+
// Only check permissions if we haven't checked yet
365+
if (permissionState === "unchecked") {
366+
checkUserPermissions();
367+
}
329368
}
330-
}, [session.data?.user, fetchChatHistory]);
369+
}, [
370+
session.data?.user,
371+
fetchChatHistory,
372+
permissionState,
373+
checkUserPermissions,
374+
]);
331375

332376
useEffect(() => {
333377
if (!session.data?.user) return;
@@ -610,6 +654,8 @@ export const TorchAgentPage = () => {
610654
"Authentication required. Please sign in to continue."
611655
);
612656
} else if (response.status === 403) {
657+
// Set the insufficient permissions flag for authenticated users
658+
setPermissionState("insufficient");
613659
throw new Error(
614660
"Access denied. You need write permissions to pytorch/pytorch repository to use this tool."
615661
);
@@ -671,13 +717,15 @@ export const TorchAgentPage = () => {
671717

672718
const hasCookieAuth = hasAuthCookie();
673719

674-
if (session.status === "loading") {
720+
if (session.status === "loading" || permissionState === "checking") {
675721
return (
676722
<TorchAgentPageContainer>
677723
<QuerySection sx={{ padding: "20px", textAlign: "center" }}>
678724
<AISpinner />
679725
<Typography variant="h6" sx={{ mt: 2 }}>
680-
Checking authentication...
726+
{session.status === "loading"
727+
? "Checking authentication..."
728+
: "Checking permissions..."}
681729
</Typography>
682730
</QuerySection>
683731
</TorchAgentPageContainer>
@@ -700,9 +748,98 @@ export const TorchAgentPage = () => {
700748
You must be logged in with write permissions to pytorch/pytorch to
701749
access this tool.
702750
</Typography>
703-
<Typography variant="body2" color="text.secondary">
704-
Please sign in to continue.
751+
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
752+
Please sign in with GitHub to continue.
753+
</Typography>
754+
<Box
755+
sx={{
756+
display: "flex",
757+
flexDirection: "column",
758+
alignItems: "center",
759+
gap: 2,
760+
}}
761+
>
762+
<Button
763+
variant="contained"
764+
color="primary"
765+
size="large"
766+
onClick={() => signIn()}
767+
sx={{ minWidth: "200px" }}
768+
>
769+
Sign In
770+
</Button>
771+
<Typography
772+
variant="body2"
773+
color="text.secondary"
774+
component="a"
775+
href="https://forms.gle/SoLgaCucjJqc6F647"
776+
target="_blank"
777+
rel="noopener noreferrer"
778+
sx={{
779+
textDecoration: "underline",
780+
"&:hover": {
781+
textDecoration: "none",
782+
},
783+
}}
784+
>
785+
no GitHub account? request access here
786+
</Typography>
787+
</Box>
788+
</QuerySection>
789+
</TorchAgentPageContainer>
790+
);
791+
}
792+
793+
// Check if user is authenticated but has insufficient permissions
794+
if (
795+
session.data?.user &&
796+
!hasAuthCookie() &&
797+
permissionState === "insufficient"
798+
) {
799+
return (
800+
<TorchAgentPageContainer>
801+
<QuerySection sx={{ padding: "20px", textAlign: "center" }}>
802+
<Typography variant="h4" gutterBottom>
803+
Insufficient Permissions
804+
</Typography>
805+
<Typography variant="body1" sx={{ mb: 2 }}>
806+
You are signed in as{" "}
807+
<strong>{session.data.user.name || session.data.user.email}</strong>
808+
, but you need write permissions to pytorch/pytorch to access this
809+
tool.
705810
</Typography>
811+
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
812+
Please request access to continue using TorchAgent.
813+
</Typography>
814+
<Box
815+
sx={{
816+
display: "flex",
817+
gap: 2,
818+
justifyContent: "center",
819+
flexWrap: "wrap",
820+
}}
821+
>
822+
<Button
823+
variant="contained"
824+
color="primary"
825+
component="a"
826+
href="https://forms.gle/SoLgaCucjJqc6F647"
827+
target="_blank"
828+
rel="noopener noreferrer"
829+
>
830+
Request Access
831+
</Button>
832+
<Button
833+
variant="outlined"
834+
color="secondary"
835+
onClick={() => {
836+
setPermissionState("unchecked");
837+
checkUserPermissions();
838+
}}
839+
>
840+
Try Again
841+
</Button>
842+
</Box>
706843
</QuerySection>
707844
</TorchAgentPageContainer>
708845
);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextApiRequest, NextApiResponse } from "next";
2+
import { getAuthorizedUsername } from "../../lib/getAuthorizedUsername";
3+
import { authOptions } from "./auth/[...nextauth]";
4+
5+
export default async function handler(
6+
req: NextApiRequest,
7+
res: NextApiResponse
8+
) {
9+
// Only allow GET method
10+
if (req.method !== "GET") {
11+
return res.status(405).json({ error: "Method not allowed" });
12+
}
13+
14+
const username = await getAuthorizedUsername(req, res, authOptions);
15+
if (!username) {
16+
// getAuthorizedUsername already sent the appropriate error response
17+
return;
18+
}
19+
20+
// If we get here, the user has sufficient permissions
21+
res.status(200).json({
22+
authorized: true,
23+
username: username === "grafana-bypass-user" ? "system" : username,
24+
});
25+
}

0 commit comments

Comments
 (0)