diff --git a/ui/litellm-dashboard/src/app/onboarding/page.tsx b/ui/litellm-dashboard/src/app/onboarding/page.tsx index bd5a109662..4e2f890318 100644 --- a/ui/litellm-dashboard/src/app/onboarding/page.tsx +++ b/ui/litellm-dashboard/src/app/onboarding/page.tsx @@ -29,6 +29,7 @@ export default function Onboarding() { const searchParams = useSearchParams()!; const token = getCookie('token'); const inviteID = searchParams.get("invitation_id"); + const action = searchParams.get("action"); const [accessToken, setAccessToken] = useState(null); const [defaultUserEmail, setDefaultUserEmail] = useState(""); const [userEmail, setUserEmail] = useState(""); @@ -117,27 +118,29 @@ export default function Onboarding() {
🚅 LiteLLM - Sign up - Claim your user account to login to Admin UI. + {action === "reset_password" ? "Reset Password" : "Sign up"} + {action === "reset_password" ? "Reset your password to access Admin UI." : "Claim your user account to login to Admin UI."} - - - SSO is under the Enterprise Tier. + {action !== "reset_password" && ( + + + SSO is under the Enterprise Tier. - - - - - + + + + + + )}
- Sign Up + {action === "reset_password" ? "Reset Password" : "Sign Up"}
diff --git a/ui/litellm-dashboard/src/components/onboarding_link.tsx b/ui/litellm-dashboard/src/components/onboarding_link.tsx index 0f07ee6299..865fae32fb 100644 --- a/ui/litellm-dashboard/src/components/onboarding_link.tsx +++ b/ui/litellm-dashboard/src/components/onboarding_link.tsx @@ -23,6 +23,7 @@ interface OnboardingProps { >; baseUrl: string; invitationLinkData: InvitationLink | null; + modalType?: "invitation" | "resetPassword"; } export default function OnboardingModal({ @@ -30,6 +31,7 @@ export default function OnboardingModal({ setIsInvitationLinkModalVisible, baseUrl, invitationLinkData, + modalType = "invitation", }: OnboardingProps) { const { Title, Paragraph } = Typography; const handleInvitationOk = () => { @@ -41,6 +43,9 @@ export default function OnboardingModal({ }; const getInvitationUrl = () => { + if (!baseUrl) { + return ""; + } const baseUrlObj = new URL(baseUrl); const basePath = baseUrlObj.pathname; // This will be "/litellm" or "" const path = basePath && basePath !== "/" ? `${basePath}/ui` : 'ui'; @@ -48,13 +53,17 @@ export default function OnboardingModal({ if (invitationLinkData?.has_user_setup_sso) { return new URL(path, baseUrl).toString(); } - const url = new URL(`${path}?invitation_id=${invitationLinkData?.id}`, baseUrl).toString(); + let urlPath = `${path}?invitation_id=${invitationLinkData?.id}`; + if (modalType === "resetPassword") { + urlPath += "&action=reset_password"; + } + const url = new URL(urlPath, baseUrl).toString(); return url; }; return ( - Copy and send the generated link to onboard this user to the proxy. + {modalType === "invitation" + ? "Copy and send the generated link to onboard this user to the proxy." + : "Copy and send the generated link to the user to reset their password."}
User ID {invitationLinkData?.user_id}
- Invitation Link + {modalType === "invitation" ? "Invitation Link" : "Reset Password Link"} {getInvitationUrl()} @@ -79,7 +90,11 @@ export default function OnboardingModal({ text={getInvitationUrl()} onCopy={() => message.success("Copied!")} > - +
diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index 590d16891b..f0ccc0bd3e 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -17,10 +17,14 @@ import { getPossibleUserRoles, userListCall, UserListResponse, + invitationCreateCall, + getProxyBaseUrl, } from "./networking"; import { Button } from "@tremor/react"; import CreateUser from "./create_user_button"; import EditUserModal from "./edit_user"; +import OnboardingModal from "./onboarding_link"; +import { InvitationLink } from "./onboarding_link"; import { userDeleteCall } from "./networking"; import { columns } from "./view_users/columns"; @@ -88,6 +92,12 @@ const ViewUserDashboard: React.FC = ({ const [filters, setFilters] = useState(initialFilters); const [debouncedFilters, setDebouncedFilters, debouncer] = useDebouncedState(filters, { wait: 300 }) const [showFilters, setShowFilters] = useState(false); + const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = + useState(false); + const [invitationLinkData, setInvitationLinkData] = + useState(null); + const [baseUrl, setBaseUrl] = useState(null); + const handleDelete = (userId: string) => { setUserToDelete(userId); setIsDeleteModalOpen(true); @@ -99,6 +109,10 @@ const ViewUserDashboard: React.FC = ({ } }, [debouncer]) + useEffect(() => { + setBaseUrl(getProxyBaseUrl()); + }, []); + const updateFilters = (update: Partial) => { setFilters((previousFilters) => { const newFilters = {...previousFilters, ...update }; @@ -111,6 +125,20 @@ const ViewUserDashboard: React.FC = ({ updateFilters({ sort_by: sortBy, sort_order: sortOrder }); }; + const handleResetPassword = async (userId: string) => { + if (!accessToken) { + message.error("Access token not found"); + return; + } + try { + message.success("Generating password reset link..."); + const data = await invitationCreateCall(accessToken, userId); + setInvitationLinkData(data); + setIsInvitationLinkModalVisible(true); + } catch (error) { + message.error("Failed to generate password reset link"); + } + }; const confirmDelete = async () => { if (userToDelete && accessToken) { @@ -226,7 +254,8 @@ const ViewUserDashboard: React.FC = ({ setSelectedUser(user); setEditModalVisible(true); }, - handleDelete + handleDelete, + handleResetPassword ); return ( @@ -523,6 +552,14 @@ const ViewUserDashboard: React.FC = ({
)} + + ); }; diff --git a/ui/litellm-dashboard/src/components/view_users/columns.tsx b/ui/litellm-dashboard/src/components/view_users/columns.tsx index 3a5f9c3cbb..61ba16b66c 100644 --- a/ui/litellm-dashboard/src/components/view_users/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_users/columns.tsx @@ -2,12 +2,13 @@ import { ColumnDef } from "@tanstack/react-table"; import { Badge, Grid, Icon } from "@tremor/react"; import { Tooltip } from "antd"; import { UserInfo } from "./types"; -import { PencilAltIcon, TrashIcon, InformationCircleIcon } from "@heroicons/react/outline"; +import { PencilAltIcon, TrashIcon, InformationCircleIcon, RefreshIcon } from "@heroicons/react/outline"; export const columns = ( possibleUIRoles: Record>, handleEdit: (user: UserInfo) => void, handleDelete: (userId: string) => void, + handleResetPassword: (userId: string) => void ): ColumnDef[] => [ { header: "User ID", @@ -110,16 +111,27 @@ export const columns = ( header: "", cell: ({ row }) => (
- handleEdit(row.original)} - /> - handleDelete(row.original.user_id)} - /> + + handleEdit(row.original)} + /> + + + handleDelete(row.original.user_id)} + /> + + + handleResetPassword(row.original.user_id)} + /> +
), }, diff --git a/ui/litellm-dashboard/src/components/view_users/user_info_view.tsx b/ui/litellm-dashboard/src/components/view_users/user_info_view.tsx index 03881fd28b..a3f1255e00 100644 --- a/ui/litellm-dashboard/src/components/view_users/user_info_view.tsx +++ b/ui/litellm-dashboard/src/components/view_users/user_info_view.tsx @@ -13,11 +13,12 @@ import { Title, Badge, } from "@tremor/react"; -import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline"; -import { userInfoCall, userDeleteCall, userUpdateUserCall, modelAvailableCall } from "../networking"; +import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline"; +import { userInfoCall, userDeleteCall, userUpdateUserCall, modelAvailableCall, invitationCreateCall, getProxyBaseUrl } from "../networking"; import { message } from "antd"; import { rolesWithWriteAccess } from '../../utils/roles'; import { UserEditView } from "../user_edit_view"; +import OnboardingModal, { InvitationLink } from "../onboarding_link"; interface UserInfoViewProps { userId: string; @@ -51,6 +52,13 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o const [isLoading, setIsLoading] = useState(true); const [isEditing, setIsEditing] = useState(false); const [userModels, setUserModels] = useState([]); + const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false); + const [invitationLinkData, setInvitationLinkData] = useState(null); + const [baseUrl, setBaseUrl] = useState(null); + + React.useEffect(() => { + setBaseUrl(getProxyBaseUrl()); + }, []); React.useEffect(() => { console.log(`userId: ${userId}, userRole: ${userRole}, accessToken: ${accessToken}`) @@ -75,6 +83,21 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o fetchData(); }, [accessToken, userId, userRole]); + const handleResetPassword = async () => { + if (!accessToken) { + message.error("Access token not found"); + return; + } + try { + message.success("Generating password reset link..."); + const data = await invitationCreateCall(accessToken, userId); + setInvitationLinkData(data); + setIsInvitationLinkModalVisible(true); + } catch (error) { + message.error("Failed to generate password reset link"); + } + }; + const handleDelete = async () => { try { if (!accessToken) return; @@ -164,14 +187,24 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o {userData.user_id} {userRole && rolesWithWriteAccess.includes(userRole) && ( - +
+ + +
)} @@ -381,6 +414,13 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o + ); } \ No newline at end of file