forked from phoenix/litellm-mirror
feat(create_user_button.tsx): allow admin to invite user to proxy via invite-links
makes it easier for proxy admin to debug what different roles can/can't do
This commit is contained in:
parent
f790d41e7f
commit
e78cf92610
13 changed files with 423 additions and 171 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -15,9 +15,9 @@ model_list:
|
||||||
# rpm: 10
|
# rpm: 10
|
||||||
# model_name: gpt-3.5-turbo-fake-model
|
# model_name: gpt-3.5-turbo-fake-model
|
||||||
- litellm_params:
|
- litellm_params:
|
||||||
api_base: https://openai-gpt-4-test-v-1.openai.azure.com/
|
api_base: https://openai-gpt-4-test-v-1.openai.azure.com
|
||||||
api_key: os.environ/AZURE_API_KEY
|
api_key: os.environ/AZURE_API_KEY
|
||||||
api_version: '2023-05-15'
|
api_version: 2024-02-15-preview
|
||||||
model: azure/chatgpt-v-2
|
model: azure/chatgpt-v-2
|
||||||
model_name: gpt-3.5-turbo
|
model_name: gpt-3.5-turbo
|
||||||
- litellm_params:
|
- litellm_params:
|
||||||
|
@ -26,10 +26,10 @@ model_list:
|
||||||
- litellm_params:
|
- litellm_params:
|
||||||
api_base: https://openai-gpt-4-test-v-1.openai.azure.com/
|
api_base: https://openai-gpt-4-test-v-1.openai.azure.com/
|
||||||
api_key: os.environ/AZURE_API_KEY
|
api_key: os.environ/AZURE_API_KEY
|
||||||
api_version: '2023-05-15'
|
api_version: 2024-02-15-preview
|
||||||
model: azure/chatgpt-v-2
|
model: azure/chatgpt-v-2
|
||||||
drop_params: True
|
drop_params: True
|
||||||
model_name: gpt-3.5-turbo-drop-params
|
model_name: gpt-3.5-turbo
|
||||||
- model_name: tts
|
- model_name: tts
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/tts-1
|
model: openai/tts-1
|
||||||
|
@ -37,6 +37,7 @@ model_list:
|
||||||
litellm_params:
|
litellm_params:
|
||||||
api_base: https://openai-france-1234.openai.azure.com
|
api_base: https://openai-france-1234.openai.azure.com
|
||||||
api_key: os.environ/AZURE_FRANCE_API_KEY
|
api_key: os.environ/AZURE_FRANCE_API_KEY
|
||||||
|
api_version: 2024-02-15-preview
|
||||||
model: azure/gpt-turbo
|
model: azure/gpt-turbo
|
||||||
- model_name: text-embedding
|
- model_name: text-embedding
|
||||||
litellm_params:
|
litellm_params:
|
||||||
|
|
|
@ -190,6 +190,7 @@ class LiteLLMRoutes(enum.Enum):
|
||||||
"/model/info",
|
"/model/info",
|
||||||
"/v2/model/info",
|
"/v2/model/info",
|
||||||
"/v2/key/info",
|
"/v2/key/info",
|
||||||
|
"/model_group/info",
|
||||||
]
|
]
|
||||||
|
|
||||||
# NOTE: ROUTES ONLY FOR MASTER KEY - only the Master Key should be able to Reset Spend
|
# NOTE: ROUTES ONLY FOR MASTER KEY - only the Master Key should be able to Reset Spend
|
||||||
|
@ -1343,6 +1344,12 @@ class InvitationModel(LiteLLMBase):
|
||||||
updated_by: str
|
updated_by: str
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationClaim(LiteLLMBase):
|
||||||
|
invitation_link: str
|
||||||
|
user_id: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class ConfigFieldInfo(LiteLLMBase):
|
class ConfigFieldInfo(LiteLLMBase):
|
||||||
field_name: str
|
field_name: str
|
||||||
field_value: Any
|
field_value: Any
|
||||||
|
|
|
@ -1311,7 +1311,9 @@ async def user_api_key_auth(
|
||||||
if user_id != valid_token.user_id:
|
if user_id != valid_token.user_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="key not allowed to access this user's info",
|
detail="key not allowed to access this user's info. user_id={}, key's user_id={}".format(
|
||||||
|
user_id, valid_token.user_id
|
||||||
|
),
|
||||||
)
|
)
|
||||||
elif route == "/model/info":
|
elif route == "/model/info":
|
||||||
# /model/info just shows models user has access to
|
# /model/info just shows models user has access to
|
||||||
|
@ -1406,7 +1408,7 @@ async def user_api_key_auth(
|
||||||
"/global/predict/spend/logs",
|
"/global/predict/spend/logs",
|
||||||
"/global/activity",
|
"/global/activity",
|
||||||
"/health/services",
|
"/health/services",
|
||||||
]
|
] + LiteLLMRoutes.info_routes.value
|
||||||
# check if the current route startswith any of the allowed routes
|
# check if the current route startswith any of the allowed routes
|
||||||
if (
|
if (
|
||||||
route is not None
|
route is not None
|
||||||
|
@ -9164,6 +9166,7 @@ async def new_user(data: NewUserRequest):
|
||||||
data_json["table_name"] = (
|
data_json["table_name"] = (
|
||||||
"user" # only create a user, don't create key if 'auto_create_key' set to False
|
"user" # only create a user, don't create key if 'auto_create_key' set to False
|
||||||
)
|
)
|
||||||
|
|
||||||
response = await generate_key_helper_fn(request_type="user", **data_json)
|
response = await generate_key_helper_fn(request_type="user", **data_json)
|
||||||
|
|
||||||
# Admin UI Logic
|
# Admin UI Logic
|
||||||
|
@ -12765,7 +12768,10 @@ async def login(request: Request):
|
||||||
_password = getattr(_user_row, "password", "unknown")
|
_password = getattr(_user_row, "password", "unknown")
|
||||||
|
|
||||||
# check if password == _user_row.password
|
# check if password == _user_row.password
|
||||||
if secrets.compare_digest(password, _password):
|
hash_password = hash_token(token=password)
|
||||||
|
if secrets.compare_digest(password, _password) or secrets.compare_digest(
|
||||||
|
hash_password, _password
|
||||||
|
):
|
||||||
if os.getenv("DATABASE_URL") is not None:
|
if os.getenv("DATABASE_URL") is not None:
|
||||||
response = await generate_key_helper_fn(
|
response = await generate_key_helper_fn(
|
||||||
request_type="key",
|
request_type="key",
|
||||||
|
@ -12928,6 +12934,92 @@ async def onboarding(invite_link: str):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/onboarding/claim_token", include_in_schema=False)
|
||||||
|
async def claim_onboarding_link(data: InvitationClaim):
|
||||||
|
"""
|
||||||
|
Special route. Allows UI link share user to update their password.
|
||||||
|
|
||||||
|
- Get the invite link
|
||||||
|
- Validate it's still 'valid'
|
||||||
|
- Check if user within initial session (prevents abuse)
|
||||||
|
- Get user from db
|
||||||
|
- Update user password
|
||||||
|
|
||||||
|
This route can only update user password.
|
||||||
|
"""
|
||||||
|
global prisma_client
|
||||||
|
### VALIDATE INVITE LINK ###
|
||||||
|
if prisma_client is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_obj = await prisma_client.db.litellm_invitationlink.find_unique(
|
||||||
|
where={"id": data.invitation_link}
|
||||||
|
)
|
||||||
|
if invite_obj is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401, detail={"error": "Invitation link does not exist in db."}
|
||||||
|
)
|
||||||
|
#### CHECK IF EXPIRED
|
||||||
|
# Extract the date part from both datetime objects
|
||||||
|
utc_now_date = litellm.utils.get_utc_datetime().date()
|
||||||
|
expires_at_date = invite_obj.expires_at.date()
|
||||||
|
if expires_at_date < utc_now_date:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401, detail={"error": "Invitation link has expired."}
|
||||||
|
)
|
||||||
|
|
||||||
|
#### CHECK IF CLAIMED
|
||||||
|
##### if claimed - check if within valid session (within 10 minutes of being claimed)
|
||||||
|
##### if unclaimed - reject
|
||||||
|
|
||||||
|
current_time = litellm.utils.get_utc_datetime()
|
||||||
|
|
||||||
|
if invite_obj.is_accepted == True:
|
||||||
|
time_difference = current_time - invite_obj.updated_at
|
||||||
|
|
||||||
|
# Check if the difference is within 10 minutes
|
||||||
|
if time_difference > timedelta(minutes=10):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail={
|
||||||
|
"error": "The invitation link has already been claimed. Please ask your admin for a new invite link."
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail={
|
||||||
|
"error": "The invitation link was never validated. Please file an issue, if this is not intended - https://github.com/BerriAI/litellm/issues."
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
#### CHECK IF VALID USER ID
|
||||||
|
if invite_obj.user_id != data.user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail={
|
||||||
|
"error": "Invalid invitation link. The user id submitted does not match the user id this link is attached to. Got={}, Expected={}".format(
|
||||||
|
data.user_id, invite_obj.user_id
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
### UPDATE USER OBJECT ###
|
||||||
|
hash_password = hash_token(token=data.password)
|
||||||
|
user_obj = await prisma_client.db.litellm_usertable.update(
|
||||||
|
where={"user_id": invite_obj.user_id}, data={"password": hash_password}
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_obj is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401, detail={"error": "User does not exist in db."}
|
||||||
|
)
|
||||||
|
|
||||||
|
return user_obj
|
||||||
|
|
||||||
|
|
||||||
@app.get("/get_image", include_in_schema=False)
|
@app.get("/get_image", include_in_schema=False)
|
||||||
def get_image():
|
def get_image():
|
||||||
"""Get logo to show on admin UI"""
|
"""Get logo to show on admin UI"""
|
||||||
|
|
|
@ -1,12 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { Suspense, useEffect, useState } from "react";
|
import React, { Suspense, useEffect, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Card, Title, Text, TextInput, Callout, Button, Grid, Col } from "@tremor/react";
|
import {
|
||||||
import { RiAlarmWarningLine, RiCheckboxCircleLine } from '@remixicon/react';
|
Card,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Callout,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Col,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import { RiAlarmWarningLine, RiCheckboxCircleLine } from "@remixicon/react";
|
||||||
import {
|
import {
|
||||||
invitationClaimCall,
|
invitationClaimCall,
|
||||||
userUpdateUserCall,
|
userUpdateUserCall,
|
||||||
getOnboardingCredentials,
|
getOnboardingCredentials,
|
||||||
|
claimOnboardingToken,
|
||||||
} from "@/components/networking";
|
} from "@/components/networking";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
import { Form, Button as Button2, message } from "antd";
|
import { Form, Button as Button2, message } from "antd";
|
||||||
|
@ -18,6 +28,7 @@ export default function Onboarding() {
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
const [defaultUserEmail, setDefaultUserEmail] = useState<string>("");
|
const [defaultUserEmail, setDefaultUserEmail] = useState<string>("");
|
||||||
const [userEmail, setUserEmail] = useState<string>("");
|
const [userEmail, setUserEmail] = useState<string>("");
|
||||||
|
const [userID, setUserID] = useState<string | null>(null);
|
||||||
const [loginUrl, setLoginUrl] = useState<string>("");
|
const [loginUrl, setLoginUrl] = useState<string>("");
|
||||||
const [jwtToken, setJwtToken] = useState<string>("");
|
const [jwtToken, setJwtToken] = useState<string>("");
|
||||||
|
|
||||||
|
@ -26,11 +37,10 @@ export default function Onboarding() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getOnboardingCredentials(inviteID).then((data) => {
|
getOnboardingCredentials(inviteID).then((data) => {
|
||||||
const login_url = data.login_url;
|
const login_url = data.login_url;
|
||||||
console.log("login_url:", login_url);
|
console.log("login_url:", login_url);
|
||||||
setLoginUrl(login_url);
|
setLoginUrl(login_url);
|
||||||
|
|
||||||
|
|
||||||
const token = data.token;
|
const token = data.token;
|
||||||
const decoded = jwtDecode(token) as { [key: string]: any };
|
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||||
setJwtToken(token);
|
setJwtToken(token);
|
||||||
|
@ -42,31 +52,44 @@ export default function Onboarding() {
|
||||||
const user_email = decoded.user_email;
|
const user_email = decoded.user_email;
|
||||||
setUserEmail(user_email);
|
setUserEmail(user_email);
|
||||||
|
|
||||||
|
const user_id = decoded.user_id;
|
||||||
|
setUserID(user_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
}, [inviteID]);
|
}, [inviteID]);
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = (formValues: Record<string, any>) => {
|
const handleSubmit = (formValues: Record<string, any>) => {
|
||||||
console.log("in handle submit. accessToken:", accessToken, "token:", jwtToken, "formValues:", formValues);
|
console.log(
|
||||||
|
"in handle submit. accessToken:",
|
||||||
|
accessToken,
|
||||||
|
"token:",
|
||||||
|
jwtToken,
|
||||||
|
"formValues:",
|
||||||
|
formValues
|
||||||
|
);
|
||||||
if (!accessToken || !jwtToken) {
|
if (!accessToken || !jwtToken) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
formValues.user_email = userEmail;
|
formValues.user_email = userEmail;
|
||||||
|
|
||||||
userUpdateUserCall(accessToken, formValues, null).then((data) => {
|
if (!userID || !inviteID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
claimOnboardingToken(
|
||||||
|
accessToken,
|
||||||
|
inviteID,
|
||||||
|
userID,
|
||||||
|
formValues.password
|
||||||
|
).then((data) => {
|
||||||
let litellm_dashboard_ui = "/ui/";
|
let litellm_dashboard_ui = "/ui/";
|
||||||
const user_id = data.data?.user_id || data.user_id;
|
const user_id = data.data?.user_id || data.user_id;
|
||||||
litellm_dashboard_ui += "?userID=" + user_id + "&token=" + jwtToken;
|
litellm_dashboard_ui += "?userID=" + user_id + "&token=" + jwtToken;
|
||||||
console.log("redirecting to:", litellm_dashboard_ui);
|
console.log("redirecting to:", litellm_dashboard_ui);
|
||||||
|
|
||||||
window.location.href = litellm_dashboard_ui;
|
window.location.href = litellm_dashboard_ui;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// redirect to login page
|
// redirect to login page
|
||||||
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-md mt-10">
|
<div className="mx-auto max-w-md mt-10">
|
||||||
|
@ -81,33 +104,26 @@ export default function Onboarding() {
|
||||||
icon={RiCheckboxCircleLine}
|
icon={RiCheckboxCircleLine}
|
||||||
color="sky"
|
color="sky"
|
||||||
>
|
>
|
||||||
<Grid numItems={2} className="flex justify-between items-center">
|
<Grid numItems={2} className="flex justify-between items-center">
|
||||||
<Col>
|
<Col>SSO is under the Enterprise Tirer.</Col>
|
||||||
SSO is under the Enterprise Tirer.
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col>
|
<Col>
|
||||||
<Button variant="primary" className="mb-2">
|
<Button variant="primary" className="mb-2">
|
||||||
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
||||||
Get Free Trial
|
Get Free Trial
|
||||||
</a>
|
</a>
|
||||||
|
</Button>
|
||||||
</Button>
|
</Col>
|
||||||
</Col>
|
</Grid>
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
className="mt-10 mb-5 mx-auto"
|
className="mt-10 mb-5 mx-auto"
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={handleSubmit}
|
onFinish={handleSubmit}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item label="Email Address" name="user_email">
|
||||||
label="Email Address"
|
|
||||||
name="user_email"
|
|
||||||
>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type="email"
|
type="email"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
@ -120,7 +136,9 @@ export default function Onboarding() {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
rules={[{ required: true, message: "password required to sign up" }]}
|
rules={[
|
||||||
|
{ required: true, message: "password required to sign up" },
|
||||||
|
]}
|
||||||
help="Create a password for your account"
|
help="Create a password for your account"
|
||||||
>
|
>
|
||||||
<TextInput placeholder="" type="password" className="max-w-md" />
|
<TextInput placeholder="" type="password" className="max-w-md" />
|
||||||
|
|
|
@ -33,6 +33,8 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
import { PencilAltIcon } from "@heroicons/react/outline";
|
import { PencilAltIcon } from "@heroicons/react/outline";
|
||||||
|
import OnboardingModal from "./onboarding_link";
|
||||||
|
import { InvitationLink } from "./onboarding_link";
|
||||||
interface AdminPanelProps {
|
interface AdminPanelProps {
|
||||||
searchParams: any;
|
searchParams: any;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
|
@ -40,17 +42,6 @@ interface AdminPanelProps {
|
||||||
showSSOBanner: boolean;
|
showSSOBanner: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InvitationLink {
|
|
||||||
id: string;
|
|
||||||
user_id: string;
|
|
||||||
is_accepted: boolean;
|
|
||||||
accepted_at: Date | null;
|
|
||||||
expires_at: Date;
|
|
||||||
created_at: Date;
|
|
||||||
created_by: string;
|
|
||||||
updated_at: Date;
|
|
||||||
updated_by: string;
|
|
||||||
}
|
|
||||||
import {
|
import {
|
||||||
userUpdateUserCall,
|
userUpdateUserCall,
|
||||||
Member,
|
Member,
|
||||||
|
@ -84,11 +75,15 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||||
useState(false);
|
useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<string, Record<string, string>>>(null);
|
const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<
|
||||||
|
string,
|
||||||
|
Record<string, string>
|
||||||
|
>>(null);
|
||||||
|
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
const [baseUrl, setBaseUrl] = useState(isLocal ? "http://localhost:4000" : "");
|
const [baseUrl, setBaseUrl] = useState(
|
||||||
|
isLocal ? "http://localhost:4000" : ""
|
||||||
|
);
|
||||||
|
|
||||||
let nonSssoUrl;
|
let nonSssoUrl;
|
||||||
try {
|
try {
|
||||||
|
@ -346,7 +341,6 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||||
setIsInvitationLinkModalVisible(true);
|
setIsInvitationLinkModalVisible(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const foundIndex = admins.findIndex((user) => {
|
const foundIndex = admins.findIndex((user) => {
|
||||||
console.log(
|
console.log(
|
||||||
`user.user_id=${user.user_id}; response.user_id=${response.user_id}`
|
`user.user_id=${user.user_id}; response.user_id=${response.user_id}`
|
||||||
|
@ -462,7 +456,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||||
? member["user_id"]
|
? member["user_id"]
|
||||||
: null}
|
: null}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell> {possibleUIRoles?.[member?.user_role]?.ui_label || "-"}</TableCell>
|
<TableCell>
|
||||||
|
{" "}
|
||||||
|
{possibleUIRoles?.[member?.user_role]?.ui_label ||
|
||||||
|
"-"}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Icon
|
<Icon
|
||||||
icon={PencilAltIcon}
|
icon={PencilAltIcon}
|
||||||
|
@ -509,39 +507,12 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||||
>
|
>
|
||||||
{addMemberForm(handleAdminCreate)}
|
{addMemberForm(handleAdminCreate)}
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<OnboardingModal
|
||||||
title="Invitation Link"
|
isInvitationLinkModalVisible={isInvitationLinkModalVisible}
|
||||||
visible={isInvitationLinkModalVisible}
|
setIsInvitationLinkModalVisible={setIsInvitationLinkModalVisible}
|
||||||
width={800}
|
baseUrl={baseUrl}
|
||||||
footer={null}
|
invitationLinkData={invitationLinkData}
|
||||||
onOk={handleAdminOk}
|
/>
|
||||||
onCancel={handleAdminCancel}
|
|
||||||
>
|
|
||||||
{/* {JSON.stringify(invitationLinkData)} */}
|
|
||||||
<Paragraph>
|
|
||||||
Copy and send the generated link to onboard this user to the
|
|
||||||
proxy.
|
|
||||||
</Paragraph>
|
|
||||||
<div className="flex justify-between pt-5 pb-2">
|
|
||||||
<Text className="text-base">User ID</Text>
|
|
||||||
<Text>{invitationLinkData?.user_id}</Text>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between pt-5 pb-2">
|
|
||||||
<Text>Invitation Link</Text>
|
|
||||||
<Text>
|
|
||||||
{baseUrl}/ui/onboarding?id={invitationLinkData?.id}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end mt-5">
|
|
||||||
<div></div>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={`${baseUrl}/ui/onboarding?id=${invitationLinkData?.id}`}
|
|
||||||
onCopy={() => message.success("Copied!")}
|
|
||||||
>
|
|
||||||
<Button variant="primary">Copy invitation link</Button>
|
|
||||||
</CopyToClipboard>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<Button
|
<Button
|
||||||
className="mb-5"
|
className="mb-5"
|
||||||
onClick={() => setIsAddMemberModalVisible(true)}
|
onClick={() => setIsAddMemberModalVisible(true)}
|
||||||
|
|
|
@ -1,25 +1,51 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button, Modal, Form, Input, message, Select, InputNumber } from "antd";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button as Button2, Text, TextInput } from "@tremor/react";
|
import {
|
||||||
import { userCreateCall, modelAvailableCall } from "./networking";
|
Button,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Select,
|
||||||
|
InputNumber,
|
||||||
|
Select as Select2,
|
||||||
|
} from "antd";
|
||||||
|
import { Button as Button2, Text, TextInput, SelectItem } from "@tremor/react";
|
||||||
|
import OnboardingModal from "./onboarding_link";
|
||||||
|
import { InvitationLink } from "./onboarding_link";
|
||||||
|
import {
|
||||||
|
userCreateCall,
|
||||||
|
modelAvailableCall,
|
||||||
|
invitationCreateCall,
|
||||||
|
} from "./networking";
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
interface CreateuserProps {
|
interface CreateuserProps {
|
||||||
userID: string;
|
userID: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
teams: any[] | null;
|
teams: any[] | null;
|
||||||
|
possibleUIRoles: null | Record<string, Record<string, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Createuser: React.FC<CreateuserProps> = ({
|
const Createuser: React.FC<CreateuserProps> = ({
|
||||||
userID,
|
userID,
|
||||||
accessToken,
|
accessToken,
|
||||||
teams,
|
teams,
|
||||||
|
possibleUIRoles,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [apiuser, setApiuser] = useState<string | null>(null);
|
const [apiuser, setApiuser] = useState<string | null>(null);
|
||||||
const [userModels, setUserModels] = useState<string[]>([]);
|
const [userModels, setUserModels] = useState<string[]>([]);
|
||||||
|
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] =
|
||||||
|
useState(false);
|
||||||
|
const [invitationLinkData, setInvitationLinkData] =
|
||||||
|
useState<InvitationLink | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
|
const [baseUrl, setBaseUrl] = useState(
|
||||||
|
isLocal ? "http://localhost:4000" : ""
|
||||||
|
);
|
||||||
// get all models
|
// get all models
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
@ -48,6 +74,14 @@ const Createuser: React.FC<CreateuserProps> = ({
|
||||||
|
|
||||||
fetchData(); // Call the function to fetch model data when the component mounts
|
fetchData(); // Call the function to fetch model data when the component mounts
|
||||||
}, []); // Empty dependency array to run only once
|
}, []); // Empty dependency array to run only once
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (router) {
|
||||||
|
const { protocol, host } = window.location;
|
||||||
|
const baseUrl = `${protocol}/${host}`;
|
||||||
|
setBaseUrl(baseUrl);
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
@ -67,6 +101,11 @@ const Createuser: React.FC<CreateuserProps> = ({
|
||||||
const response = await userCreateCall(accessToken, null, formValues);
|
const response = await userCreateCall(accessToken, null, formValues);
|
||||||
console.log("user create Response:", response);
|
console.log("user create Response:", response);
|
||||||
setApiuser(response["key"]);
|
setApiuser(response["key"]);
|
||||||
|
const user_id = response.data?.user_id || response.user_id;
|
||||||
|
invitationCreateCall(accessToken, user_id).then((data) => {
|
||||||
|
setInvitationLinkData(data);
|
||||||
|
setIsInvitationLinkModalVisible(true);
|
||||||
|
});
|
||||||
message.success("API user Created");
|
message.success("API user Created");
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
localStorage.removeItem("userData" + userID);
|
localStorage.removeItem("userData" + userID);
|
||||||
|
@ -88,12 +127,7 @@ const Createuser: React.FC<CreateuserProps> = ({
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
>
|
>
|
||||||
<Text className="mb-1">
|
<Text className="mb-1">Create a User who can own keys</Text>
|
||||||
Invite a user to login to the Admin UI and create Keys
|
|
||||||
</Text>
|
|
||||||
<Text className="mb-6">
|
|
||||||
<b>Note: SSO Setup Required for this</b>
|
|
||||||
</Text>
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
onFinish={handleCreate}
|
onFinish={handleCreate}
|
||||||
|
@ -104,6 +138,26 @@ const Createuser: React.FC<CreateuserProps> = ({
|
||||||
<Form.Item label="User Email" name="user_email">
|
<Form.Item label="User Email" name="user_email">
|
||||||
<TextInput placeholder="" />
|
<TextInput placeholder="" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label="User Role" name="user_role">
|
||||||
|
<Select2>
|
||||||
|
{possibleUIRoles &&
|
||||||
|
Object.entries(possibleUIRoles).map(
|
||||||
|
([role, { ui_label, description }]) => (
|
||||||
|
<SelectItem key={role} value={role} title={ui_label}>
|
||||||
|
<div className="flex">
|
||||||
|
{ui_label}{" "}
|
||||||
|
<p
|
||||||
|
className="ml-2"
|
||||||
|
style={{ color: "gray", fontSize: "12px" }}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select2>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label="Team ID" name="team_id">
|
<Form.Item label="Team ID" name="team_id">
|
||||||
<Select placeholder="Select Team ID" style={{ width: "100%" }}>
|
<Select placeholder="Select Team ID" style={{ width: "100%" }}>
|
||||||
{teams ? (
|
{teams ? (
|
||||||
|
@ -119,6 +173,7 @@ const Createuser: React.FC<CreateuserProps> = ({
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="Metadata" name="metadata">
|
<Form.Item label="Metadata" name="metadata">
|
||||||
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
|
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -128,25 +183,12 @@ const Createuser: React.FC<CreateuserProps> = ({
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
{apiuser && (
|
{apiuser && (
|
||||||
<Modal
|
<OnboardingModal
|
||||||
title="User Created Successfully"
|
isInvitationLinkModalVisible={isInvitationLinkModalVisible}
|
||||||
visible={isModalVisible}
|
setIsInvitationLinkModalVisible={setIsInvitationLinkModalVisible}
|
||||||
onOk={handleOk}
|
baseUrl={baseUrl}
|
||||||
onCancel={handleCancel}
|
invitationLinkData={invitationLinkData}
|
||||||
footer={null}
|
/>
|
||||||
>
|
|
||||||
<p>
|
|
||||||
User has been created to access your proxy. Please Ask them to Log
|
|
||||||
In.
|
|
||||||
</p>
|
|
||||||
<br></br>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<b>
|
|
||||||
Note: This Feature is only supported through SSO on the Admin UI
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -522,6 +522,12 @@ export const userInfoCall = async (
|
||||||
if (userRole == "App User" && userID) {
|
if (userRole == "App User" && userID) {
|
||||||
url = `${url}?user_id=${userID}`;
|
url = `${url}?user_id=${userID}`;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
(userRole == "Internal User" || userRole == "Internal Viewer") &&
|
||||||
|
userID
|
||||||
|
) {
|
||||||
|
url = `${url}?user_id=${userID}`;
|
||||||
|
}
|
||||||
console.log("in userInfoCall viewAll=", viewAll);
|
console.log("in userInfoCall viewAll=", viewAll);
|
||||||
if (viewAll && page_size && page != null && page != undefined) {
|
if (viewAll && page_size && page != null && page != undefined) {
|
||||||
url = `${url}?view_all=true&page=${page}&page_size=${page_size}`;
|
url = `${url}?view_all=true&page=${page}&page_size=${page_size}`;
|
||||||
|
@ -617,14 +623,15 @@ export const getTotalSpendCall = async (accessToken: String) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const getOnboardingCredentials = async (inviteUUID: String) => {
|
export const getOnboardingCredentials = async (inviteUUID: String) => {
|
||||||
/**
|
/**
|
||||||
* Get all models on proxy
|
* Get all models on proxy
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/onboarding/get_token` : `/onboarding/get_token`;
|
let url = proxyBaseUrl
|
||||||
url += `?invite_link=${inviteUUID}`
|
? `${proxyBaseUrl}/onboarding/get_token`
|
||||||
|
: `/onboarding/get_token`;
|
||||||
|
url += `?invite_link=${inviteUUID}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -648,6 +655,43 @@ export const getOnboardingCredentials = async (inviteUUID: String) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const claimOnboardingToken = async (
|
||||||
|
accessToken: string,
|
||||||
|
inviteUUID: string,
|
||||||
|
userID: string,
|
||||||
|
password: String
|
||||||
|
) => {
|
||||||
|
const url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/onboarding/claim_token`
|
||||||
|
: `/onboarding/claim_token`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
invitation_link: inviteUUID,
|
||||||
|
user_id: userID,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
message.error("Failed to delete team: " + errorData, 10);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
export const modelInfoCall = async (
|
export const modelInfoCall = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
userID: String,
|
userID: String,
|
||||||
|
@ -1024,15 +1068,12 @@ export const tagsSpendLogsCall = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const allTagNamesCall = async (
|
export const allTagNamesCall = async (accessToken: String) => {
|
||||||
accessToken: String,
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
let url = proxyBaseUrl
|
let url = proxyBaseUrl
|
||||||
? `${proxyBaseUrl}/global/spend/all_tag_names`
|
? `${proxyBaseUrl}/global/spend/all_tag_names`
|
||||||
: `/global/spend/all_tag_names`;
|
: `/global/spend/all_tag_names`;
|
||||||
|
|
||||||
|
|
||||||
console.log("in global/spend/all_tag_names call", url);
|
console.log("in global/spend/all_tag_names call", url);
|
||||||
const response = await fetch(`${url}`, {
|
const response = await fetch(`${url}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -1055,16 +1096,12 @@ export const allTagNamesCall = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const allEndUsersCall = async (accessToken: String) => {
|
||||||
export const allEndUsersCall = async (
|
|
||||||
accessToken: String,
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
let url = proxyBaseUrl
|
let url = proxyBaseUrl
|
||||||
? `${proxyBaseUrl}/global/all_end_users`
|
? `${proxyBaseUrl}/global/all_end_users`
|
||||||
: `/global/all_end_users`;
|
: `/global/all_end_users`;
|
||||||
|
|
||||||
|
|
||||||
console.log("in global/all_end_users call", url);
|
console.log("in global/all_end_users call", url);
|
||||||
const response = await fetch(`${url}`, {
|
const response = await fetch(`${url}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -1087,7 +1124,6 @@ export const allEndUsersCall = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const userSpendLogsCall = async (
|
export const userSpendLogsCall = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
token: String,
|
token: String,
|
||||||
|
@ -1377,13 +1413,11 @@ export const adminGlobalActivityPerModel = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const adminGlobalActivityExceptions = async (
|
export const adminGlobalActivityExceptions = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
startTime: String | undefined,
|
startTime: String | undefined,
|
||||||
endTime: String | undefined,
|
endTime: String | undefined,
|
||||||
modelGroup: String,
|
modelGroup: String
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let url = proxyBaseUrl
|
let url = proxyBaseUrl
|
||||||
|
@ -1429,7 +1463,7 @@ export const adminGlobalActivityExceptionsPerDeployment = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
startTime: String | undefined,
|
startTime: String | undefined,
|
||||||
endTime: String | undefined,
|
endTime: String | undefined,
|
||||||
modelGroup: String,
|
modelGroup: String
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let url = proxyBaseUrl
|
let url = proxyBaseUrl
|
||||||
|
@ -1666,9 +1700,7 @@ export const userGetAllUsersCall = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPossibleUserRoles = async (
|
export const getPossibleUserRoles = async (accessToken: String) => {
|
||||||
accessToken: String,
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const url = proxyBaseUrl
|
const url = proxyBaseUrl
|
||||||
? `${proxyBaseUrl}/user/available_roles`
|
? `${proxyBaseUrl}/user/available_roles`
|
||||||
|
|
86
ui/litellm-dashboard/src/components/onboarding_link.tsx
Normal file
86
ui/litellm-dashboard/src/components/onboarding_link.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Button as Button2,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select as Select2,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Typography,
|
||||||
|
} from "antd";
|
||||||
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
|
import { Text, Button } from "@tremor/react";
|
||||||
|
export interface InvitationLink {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
is_accepted: boolean;
|
||||||
|
accepted_at: Date | null;
|
||||||
|
expires_at: Date;
|
||||||
|
created_at: Date;
|
||||||
|
created_by: string;
|
||||||
|
updated_at: Date;
|
||||||
|
updated_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnboardingProps {
|
||||||
|
isInvitationLinkModalVisible: boolean;
|
||||||
|
setIsInvitationLinkModalVisible: React.Dispatch<
|
||||||
|
React.SetStateAction<boolean>
|
||||||
|
>;
|
||||||
|
baseUrl: string;
|
||||||
|
invitationLinkData: InvitationLink | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnboardingModal: React.FC<OnboardingProps> = ({
|
||||||
|
isInvitationLinkModalVisible,
|
||||||
|
setIsInvitationLinkModalVisible,
|
||||||
|
baseUrl,
|
||||||
|
invitationLinkData,
|
||||||
|
}) => {
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
const handleInvitationOk = () => {
|
||||||
|
setIsInvitationLinkModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInvitationCancel = () => {
|
||||||
|
setIsInvitationLinkModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Invitation Link"
|
||||||
|
visible={isInvitationLinkModalVisible}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
onOk={handleInvitationOk}
|
||||||
|
onCancel={handleInvitationCancel}
|
||||||
|
>
|
||||||
|
{/* {JSON.stringify(invitationLinkData)} */}
|
||||||
|
<Paragraph>
|
||||||
|
Copy and send the generated link to onboard this user to the proxy.
|
||||||
|
</Paragraph>
|
||||||
|
<div className="flex justify-between pt-5 pb-2">
|
||||||
|
<Text className="text-base">User ID</Text>
|
||||||
|
<Text>{invitationLinkData?.user_id}</Text>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between pt-5 pb-2">
|
||||||
|
<Text>Invitation Link</Text>
|
||||||
|
<Text>
|
||||||
|
{baseUrl}/ui/onboarding?id={invitationLinkData?.id}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end mt-5">
|
||||||
|
<div></div>
|
||||||
|
<CopyToClipboard
|
||||||
|
text={`${baseUrl}/ui/onboarding?id=${invitationLinkData?.id}`}
|
||||||
|
onCopy={() => message.success("Copied!")}
|
||||||
|
>
|
||||||
|
<Button variant="primary">Copy invitation link</Button>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OnboardingModal;
|
|
@ -101,7 +101,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
return "App User";
|
return "App User";
|
||||||
case "internal_user":
|
case "internal_user":
|
||||||
return "Internal User";
|
return "Internal User";
|
||||||
case "internal_viewer":
|
case "internal_user_viewer":
|
||||||
return "Internal Viewer";
|
return "Internal Viewer";
|
||||||
default:
|
default:
|
||||||
return "Unknown Role";
|
return "Unknown Role";
|
||||||
|
|
|
@ -25,15 +25,17 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
|
|
||||||
import {
|
import { message } from "antd";
|
||||||
message,
|
|
||||||
} from "antd";
|
|
||||||
|
|
||||||
import { userInfoCall, userUpdateUserCall, getPossibleUserRoles } from "./networking";
|
import {
|
||||||
|
userInfoCall,
|
||||||
|
userUpdateUserCall,
|
||||||
|
getPossibleUserRoles,
|
||||||
|
} from "./networking";
|
||||||
import { Badge, BadgeDelta, Button } from "@tremor/react";
|
import { Badge, BadgeDelta, Button } from "@tremor/react";
|
||||||
import RequestAccess from "./request_model_access";
|
import RequestAccess from "./request_model_access";
|
||||||
import CreateUser from "./create_user_button";
|
import CreateUser from "./create_user_button";
|
||||||
import EditUserModal from "./edit_user";
|
import EditUserModal from "./edit_user";
|
||||||
import Paragraph from "antd/es/skeleton/Paragraph";
|
import Paragraph from "antd/es/skeleton/Paragraph";
|
||||||
import {
|
import {
|
||||||
PencilAltIcon,
|
PencilAltIcon,
|
||||||
|
@ -67,14 +69,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
const [selectedItem, setSelectedItem] = useState<null | any>(null);
|
const [selectedItem, setSelectedItem] = useState<null | any>(null);
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
const [selectedUser, setSelectedUser] = useState(null);
|
const [selectedUser, setSelectedUser] = useState(null);
|
||||||
const [possibleUIRoles, setPossibleUIRoles] = useState<Record<string, Record<string, string>>>({});
|
const [possibleUIRoles, setPossibleUIRoles] = useState<
|
||||||
|
Record<string, Record<string, string>>
|
||||||
|
>({});
|
||||||
const defaultPageSize = 25;
|
const defaultPageSize = 25;
|
||||||
|
|
||||||
const handleEditCancel = async () => {
|
const handleEditCancel = async () => {
|
||||||
setSelectedUser(null);
|
setSelectedUser(null);
|
||||||
setEditModalVisible(false);
|
setEditModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditSubmit = async (editedUser: any) => {
|
const handleEditSubmit = async (editedUser: any) => {
|
||||||
console.log("inside handleEditSubmit:", editedUser);
|
console.log("inside handleEditSubmit:", editedUser);
|
||||||
|
|
||||||
|
@ -87,7 +91,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
message.success(`User ${editedUser.user_id} updated successfully`);
|
message.success(`User ${editedUser.user_id} updated successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("There was an error updating the user", error);
|
console.error("There was an error updating the user", error);
|
||||||
}
|
}
|
||||||
if (userData) {
|
if (userData) {
|
||||||
const updatedUserData = userData.map((user) =>
|
const updatedUserData = userData.map((user) =>
|
||||||
user.user_id === editedUser.user_id ? editedUser : user
|
user.user_id === editedUser.user_id ? editedUser : user
|
||||||
|
@ -119,13 +123,11 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
|
|
||||||
const availableUserRoles = await getPossibleUserRoles(accessToken);
|
const availableUserRoles = await getPossibleUserRoles(accessToken);
|
||||||
setPossibleUIRoles(availableUserRoles);
|
setPossibleUIRoles(availableUserRoles);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("There was an error fetching the model data", error);
|
console.error("There was an error fetching the model data", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (accessToken && token && userRole && userID) {
|
if (accessToken && token && userRole && userID) {
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
|
@ -174,10 +176,14 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<Grid className="gap-2 p-2 h-[90vh] w-full mt-8">
|
<Grid className="gap-2 p-2 h-[90vh] w-full mt-8">
|
||||||
<CreateUser userID={userID} accessToken={accessToken} teams={teams} />
|
<CreateUser
|
||||||
|
userID={userID}
|
||||||
|
accessToken={accessToken}
|
||||||
|
teams={teams}
|
||||||
|
possibleUIRoles={possibleUIRoles}
|
||||||
|
/>
|
||||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[90vh] mb-4">
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[90vh] mb-4">
|
||||||
<div className="mb-4 mt-1">
|
<div className="mb-4 mt-1"></div>
|
||||||
</div>
|
|
||||||
<TabGroup>
|
<TabGroup>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
|
@ -218,9 +224,8 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
user.key_aliases.filter(
|
user.key_aliases.filter(
|
||||||
(key: any) => key !== null
|
(key: any) => key !== null
|
||||||
).length
|
).length
|
||||||
|
|
||||||
}
|
}
|
||||||
Keys
|
Keys
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge size={"xs"} color={"gray"}>
|
<Badge size={"xs"} color={"gray"}>
|
||||||
|
@ -233,23 +238,24 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{/* <Text>{user.key_aliases.filter(key => key !== null).length} Keys</Text> */}
|
{/* <Text>{user.key_aliases.filter(key => key !== null).length} Keys</Text> */}
|
||||||
</Grid>
|
</Grid>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
<Icon
|
||||||
<Icon icon={PencilAltIcon} onClick= {() => {
|
icon={PencilAltIcon}
|
||||||
setSelectedUser(user)
|
onClick={() => {
|
||||||
setEditModalVisible(true)
|
setSelectedUser(user);
|
||||||
}}>View Keys</Icon>
|
setEditModalVisible(true);
|
||||||
{/*
|
}}
|
||||||
|
>
|
||||||
|
View Keys
|
||||||
|
</Icon>
|
||||||
|
{/*
|
||||||
<Icon icon={TrashIcon} onClick= {() => {
|
<Icon icon={TrashIcon} onClick= {() => {
|
||||||
setOpenDialogId(user.user_id)
|
setOpenDialogId(user.user_id)
|
||||||
setSelectedItem(user)
|
setSelectedItem(user)
|
||||||
}}>View Keys</Icon> */}
|
}}>View Keys</Icon> */}
|
||||||
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
@ -283,12 +289,12 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
<EditUserModal
|
<EditUserModal
|
||||||
visible={editModalVisible}
|
visible={editModalVisible}
|
||||||
possibleUIRoles={possibleUIRoles}
|
possibleUIRoles={possibleUIRoles}
|
||||||
onCancel={handleEditCancel}
|
onCancel={handleEditCancel}
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
onSubmit={handleEditSubmit}
|
onSubmit={handleEditSubmit}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
{renderPagination()}
|
{renderPagination()}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue