forked from phoenix/litellm-mirror
Merge pull request #2249 from BerriAI/litellm_ui_admin_viewer_fixes
build(ui): fix admin viewer issue
This commit is contained in:
commit
81c9283796
18 changed files with 326 additions and 130 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
||||||
<!DOCTYPE html><html id="__next_error__"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/ui/_next/static/chunks/webpack-12184ee6a95c1363.js" crossorigin=""/><script src="/ui/_next/static/chunks/fd9d1056-a85b2c176012d8e5.js" async="" crossorigin=""></script><script src="/ui/_next/static/chunks/69-e1b183dda365ec86.js" async="" crossorigin=""></script><script src="/ui/_next/static/chunks/main-app-096338c8e1915716.js" async="" crossorigin=""></script><title>🚅 LiteLLM</title><meta name="description" content="LiteLLM Proxy Admin UI"/><link rel="icon" href="/ui/favicon.ico" type="image/x-icon" sizes="16x16"/><meta name="next-size-adjust"/><script src="/ui/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js" crossorigin="" noModule=""></script></head><body><script src="/ui/_next/static/chunks/webpack-12184ee6a95c1363.js" crossorigin="" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/ui/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/ui/_next/static/css/a40ad0909dd7838e.css\",\"style\",{\"crossOrigin\":\"\"}]\n0:\"$L3\"\n"])</script><script>self.__next_f.push([1,"4:I[47690,[],\"\"]\n6:I[77831,[],\"\"]\n7:I[30280,[\"303\",\"static/chunks/303-d80f23087a9e6aec.js\",\"931\",\"static/chunks/app/page-8f65fc157f538dff.js\"],\"\"]\n8:I[5613,[],\"\"]\n9:I[31778,[],\"\"]\nb:I[48955,[],\"\"]\nc:[]\n"])</script><script>self.__next_f.push([1,"3:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/a40ad0909dd7838e.css\",\"precedence\":\"next\",\"crossOrigin\":\"\"}]],[\"$\",\"$L4\",null,{\"buildId\":\"kyOCJPBB9pyUfbMKCAXr-\",\"assetPrefix\":\"/ui\",\"initialCanonicalUrl\":\"/\",\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"__PAGE__\",{},[\"$L5\",[\"$\",\"$L6\",null,{\"propsForComponent\":{\"params\":{}},\"Component\":\"$7\",\"isStaticGeneration\":true}],null]]},[null,[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_c23dc8\",\"children\":[\"$\",\"$L8\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"loadingScripts\":\"$undefined\",\"hasLoading\":false,\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L9\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[],\"styles\":null}]}]}],null]],\"initialHead\":[false,\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"🚅 LiteLLM\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"LiteLLM Proxy Admin UI\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/ui/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"meta\",\"5\",{\"name\":\"next-size-adjust\"}]]\n5:null\n"])</script><script>self.__next_f.push([1,""])</script></body></html>
|
<!DOCTYPE html><html id="__next_error__"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/ui/_next/static/chunks/webpack-12184ee6a95c1363.js" crossorigin=""/><script src="/ui/_next/static/chunks/fd9d1056-a85b2c176012d8e5.js" async="" crossorigin=""></script><script src="/ui/_next/static/chunks/69-e1b183dda365ec86.js" async="" crossorigin=""></script><script src="/ui/_next/static/chunks/main-app-096338c8e1915716.js" async="" crossorigin=""></script><title>🚅 LiteLLM</title><meta name="description" content="LiteLLM Proxy Admin UI"/><link rel="icon" href="/ui/favicon.ico" type="image/x-icon" sizes="16x16"/><meta name="next-size-adjust"/><script src="/ui/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js" crossorigin="" noModule=""></script></head><body><script src="/ui/_next/static/chunks/webpack-12184ee6a95c1363.js" crossorigin="" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/ui/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/ui/_next/static/css/a40ad0909dd7838e.css\",\"style\",{\"crossOrigin\":\"\"}]\n0:\"$L3\"\n"])</script><script>self.__next_f.push([1,"4:I[47690,[],\"\"]\n6:I[77831,[],\"\"]\n7:I[30280,[\"303\",\"static/chunks/303-d80f23087a9e6aec.js\",\"931\",\"static/chunks/app/page-f9c7d1158cda8c5f.js\"],\"\"]\n8:I[5613,[],\"\"]\n9:I[31778,[],\"\"]\nb:I[48955,[],\"\"]\nc:[]\n"])</script><script>self.__next_f.push([1,"3:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/a40ad0909dd7838e.css\",\"precedence\":\"next\",\"crossOrigin\":\"\"}]],[\"$\",\"$L4\",null,{\"buildId\":\"Au9NvM222ys66B_S9303T\",\"assetPrefix\":\"/ui\",\"initialCanonicalUrl\":\"/\",\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"__PAGE__\",{},[\"$L5\",[\"$\",\"$L6\",null,{\"propsForComponent\":{\"params\":{}},\"Component\":\"$7\",\"isStaticGeneration\":true}],null]]},[null,[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_c23dc8\",\"children\":[\"$\",\"$L8\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"loadingScripts\":\"$undefined\",\"hasLoading\":false,\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L9\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[],\"styles\":null}]}]}],null]],\"initialHead\":[false,\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"🚅 LiteLLM\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"LiteLLM Proxy Admin UI\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/ui/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"meta\",\"5\",{\"name\":\"next-size-adjust\"}]]\n5:null\n"])</script><script>self.__next_f.push([1,""])</script></body></html>
|
|
@ -1,7 +1,7 @@
|
||||||
2:I[77831,[],""]
|
2:I[77831,[],""]
|
||||||
3:I[30280,["303","static/chunks/303-d80f23087a9e6aec.js","931","static/chunks/app/page-8f65fc157f538dff.js"],""]
|
3:I[30280,["303","static/chunks/303-d80f23087a9e6aec.js","931","static/chunks/app/page-f9c7d1158cda8c5f.js"],""]
|
||||||
4:I[5613,[],""]
|
4:I[5613,[],""]
|
||||||
5:I[31778,[],""]
|
5:I[31778,[],""]
|
||||||
0:["kyOCJPBB9pyUfbMKCAXr-",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","$L2",null,{"propsForComponent":{"params":{}},"Component":"$3","isStaticGeneration":true}],null]]},[null,["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_c23dc8","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/ui/_next/static/css/a40ad0909dd7838e.css","precedence":"next","crossOrigin":""}]],"$L6"]]]]
|
0:["Au9NvM222ys66B_S9303T",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","$L2",null,{"propsForComponent":{"params":{}},"Component":"$3","isStaticGeneration":true}],null]]},[null,["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_c23dc8","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/ui/_next/static/css/a40ad0909dd7838e.css","precedence":"next","crossOrigin":""}]],"$L6"]]]]
|
||||||
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"🚅 LiteLLM"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/ui/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"🚅 LiteLLM"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/ui/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||||
1:null
|
1:null
|
||||||
|
|
|
@ -783,6 +783,9 @@ async def user_api_key_auth(
|
||||||
"/v2/key/info",
|
"/v2/key/info",
|
||||||
"/models",
|
"/models",
|
||||||
"/v1/models",
|
"/v1/models",
|
||||||
|
"/global/spend/logs",
|
||||||
|
"/global/spend/keys",
|
||||||
|
"/global/spend/models",
|
||||||
]
|
]
|
||||||
# check if the current route startswith any of the allowed routes
|
# check if the current route startswith any of the allowed routes
|
||||||
if (
|
if (
|
||||||
|
@ -4469,31 +4472,42 @@ async def user_update(data: UpdateUserRequest):
|
||||||
non_default_values[k] = v
|
non_default_values[k] = v
|
||||||
|
|
||||||
## ADD USER, IF NEW ##
|
## ADD USER, IF NEW ##
|
||||||
if data.user_id is not None and len(data.user_id) == 0:
|
verbose_proxy_logger.debug(f"/user/update: Received data = {data}")
|
||||||
|
if data.user_id is not None and len(data.user_id) > 0:
|
||||||
non_default_values["user_id"] = data.user_id # type: ignore
|
non_default_values["user_id"] = data.user_id # type: ignore
|
||||||
await prisma_client.update_data(
|
verbose_proxy_logger.debug(f"In update user, user_id condition block.")
|
||||||
|
response = await prisma_client.update_data(
|
||||||
user_id=data.user_id,
|
user_id=data.user_id,
|
||||||
data=non_default_values,
|
data=non_default_values,
|
||||||
table_name="user",
|
table_name="user",
|
||||||
)
|
)
|
||||||
|
verbose_proxy_logger.debug(
|
||||||
|
f"received response from updating prisma client. response={response}"
|
||||||
|
)
|
||||||
elif data.user_email is not None:
|
elif data.user_email is not None:
|
||||||
non_default_values["user_id"] = str(uuid.uuid4())
|
non_default_values["user_id"] = str(uuid.uuid4())
|
||||||
non_default_values["user_email"] = data.user_email
|
non_default_values["user_email"] = data.user_email
|
||||||
## user email is not unique acc. to prisma schema -> future improvement
|
## user email is not unique acc. to prisma schema -> future improvement
|
||||||
### for now: check if it exists in db, if not - insert it
|
### for now: check if it exists in db, if not - insert it
|
||||||
existing_user_row = await prisma_client.get_data(
|
existing_user_rows = await prisma_client.get_data(
|
||||||
key_val={"user_email": data.user_email},
|
key_val={"user_email": data.user_email},
|
||||||
table_name="user",
|
table_name="user",
|
||||||
query_type="find_all",
|
query_type="find_all",
|
||||||
)
|
)
|
||||||
if existing_user_row is None or (
|
if existing_user_rows is None or (
|
||||||
isinstance(existing_user_row, list) and len(existing_user_row) == 0
|
isinstance(existing_user_rows, list) and len(existing_user_rows) == 0
|
||||||
):
|
):
|
||||||
await prisma_client.insert_data(
|
response = await prisma_client.insert_data(
|
||||||
data=non_default_values, table_name="user"
|
data=non_default_values, table_name="user"
|
||||||
)
|
)
|
||||||
|
elif isinstance(existing_user_rows, list) and len(existing_user_rows) > 0:
|
||||||
return non_default_values
|
for existing_user in existing_user_rows:
|
||||||
|
response = await prisma_client.update_data(
|
||||||
|
user_id=existing_user.user_id,
|
||||||
|
data=non_default_values,
|
||||||
|
table_name="user",
|
||||||
|
)
|
||||||
|
return response
|
||||||
# update based on remaining passed in values
|
# update based on remaining passed in values
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
|
@ -542,6 +542,34 @@ class PrismaClient:
|
||||||
|
|
||||||
print("MonthlyGlobalSpend Created!") # noqa
|
print("MonthlyGlobalSpend Created!") # noqa
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.db.query_raw("""SELECT 1 FROM "Last30dKeysBySpend" LIMIT 1""")
|
||||||
|
print("Last30dKeysBySpend Exists!") # noqa
|
||||||
|
except Exception as e:
|
||||||
|
sql_query = """
|
||||||
|
CREATE OR REPLACE VIEW "Last30dKeysBySpend" AS
|
||||||
|
SELECT
|
||||||
|
L."api_key",
|
||||||
|
V."key_alias",
|
||||||
|
V."key_name",
|
||||||
|
SUM(L."spend") AS total_spend
|
||||||
|
FROM
|
||||||
|
"LiteLLM_SpendLogs" L
|
||||||
|
LEFT JOIN
|
||||||
|
"LiteLLM_VerificationToken" V
|
||||||
|
ON
|
||||||
|
L."api_key" = V."token"
|
||||||
|
WHERE
|
||||||
|
L."startTime" >= (CURRENT_DATE - INTERVAL '30 days')
|
||||||
|
GROUP BY
|
||||||
|
L."api_key", V."key_alias", V."key_name"
|
||||||
|
ORDER BY
|
||||||
|
total_spend DESC;
|
||||||
|
"""
|
||||||
|
await self.db.execute_raw(query=sql_query)
|
||||||
|
|
||||||
|
print("Last30dKeysBySpend Created!") # noqa
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@backoff.on_exception(
|
@backoff.on_exception(
|
||||||
|
@ -1034,7 +1062,7 @@ class PrismaClient:
|
||||||
+ f"DB User Table - update succeeded {update_user_row}"
|
+ f"DB User Table - update succeeded {update_user_row}"
|
||||||
+ "\033[0m"
|
+ "\033[0m"
|
||||||
)
|
)
|
||||||
return {"user_id": user_id, "data": db_data}
|
return {"user_id": user_id, "data": update_user_row}
|
||||||
elif (
|
elif (
|
||||||
team_id is not None
|
team_id is not None
|
||||||
or (table_name is not None and table_name == "team")
|
or (table_name is not None and table_name == "team")
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
||||||
<!DOCTYPE html><html id="__next_error__"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/ui/_next/static/chunks/webpack-12184ee6a95c1363.js" crossorigin=""/><script src="/ui/_next/static/chunks/fd9d1056-a85b2c176012d8e5.js" async="" crossorigin=""></script><script src="/ui/_next/static/chunks/69-e1b183dda365ec86.js" async="" crossorigin=""></script><script src="/ui/_next/static/chunks/main-app-096338c8e1915716.js" async="" crossorigin=""></script><title>🚅 LiteLLM</title><meta name="description" content="LiteLLM Proxy Admin UI"/><link rel="icon" href="/ui/favicon.ico" type="image/x-icon" sizes="16x16"/><meta name="next-size-adjust"/><script src="/ui/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js" crossorigin="" noModule=""></script></head><body><script src="/ui/_next/static/chunks/webpack-12184ee6a95c1363.js" crossorigin="" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/ui/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/ui/_next/static/css/a40ad0909dd7838e.css\",\"style\",{\"crossOrigin\":\"\"}]\n0:\"$L3\"\n"])</script><script>self.__next_f.push([1,"4:I[47690,[],\"\"]\n6:I[77831,[],\"\"]\n7:I[30280,[\"303\",\"static/chunks/303-d80f23087a9e6aec.js\",\"931\",\"static/chunks/app/page-8f65fc157f538dff.js\"],\"\"]\n8:I[5613,[],\"\"]\n9:I[31778,[],\"\"]\nb:I[48955,[],\"\"]\nc:[]\n"])</script><script>self.__next_f.push([1,"3:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/a40ad0909dd7838e.css\",\"precedence\":\"next\",\"crossOrigin\":\"\"}]],[\"$\",\"$L4\",null,{\"buildId\":\"kyOCJPBB9pyUfbMKCAXr-\",\"assetPrefix\":\"/ui\",\"initialCanonicalUrl\":\"/\",\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"__PAGE__\",{},[\"$L5\",[\"$\",\"$L6\",null,{\"propsForComponent\":{\"params\":{}},\"Component\":\"$7\",\"isStaticGeneration\":true}],null]]},[null,[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_c23dc8\",\"children\":[\"$\",\"$L8\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"loadingScripts\":\"$undefined\",\"hasLoading\":false,\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L9\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[],\"styles\":null}]}]}],null]],\"initialHead\":[false,\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"🚅 LiteLLM\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"LiteLLM Proxy Admin UI\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/ui/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"meta\",\"5\",{\"name\":\"next-size-adjust\"}]]\n5:null\n"])</script><script>self.__next_f.push([1,""])</script></body></html>
|
<!DOCTYPE html><html id="__next_error__"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/ui/_next/static/chunks/webpack-12184ee6a95c1363.js" crossorigin=""/><script src="/ui/_next/static/chunks/fd9d1056-a85b2c176012d8e5.js" async="" crossorigin=""></script><script src="/ui/_next/static/chunks/69-e1b183dda365ec86.js" async="" crossorigin=""></script><script src="/ui/_next/static/chunks/main-app-096338c8e1915716.js" async="" crossorigin=""></script><title>🚅 LiteLLM</title><meta name="description" content="LiteLLM Proxy Admin UI"/><link rel="icon" href="/ui/favicon.ico" type="image/x-icon" sizes="16x16"/><meta name="next-size-adjust"/><script src="/ui/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js" crossorigin="" noModule=""></script></head><body><script src="/ui/_next/static/chunks/webpack-12184ee6a95c1363.js" crossorigin="" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/ui/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/ui/_next/static/css/a40ad0909dd7838e.css\",\"style\",{\"crossOrigin\":\"\"}]\n0:\"$L3\"\n"])</script><script>self.__next_f.push([1,"4:I[47690,[],\"\"]\n6:I[77831,[],\"\"]\n7:I[30280,[\"303\",\"static/chunks/303-d80f23087a9e6aec.js\",\"931\",\"static/chunks/app/page-f9c7d1158cda8c5f.js\"],\"\"]\n8:I[5613,[],\"\"]\n9:I[31778,[],\"\"]\nb:I[48955,[],\"\"]\nc:[]\n"])</script><script>self.__next_f.push([1,"3:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/ui/_next/static/css/a40ad0909dd7838e.css\",\"precedence\":\"next\",\"crossOrigin\":\"\"}]],[\"$\",\"$L4\",null,{\"buildId\":\"Au9NvM222ys66B_S9303T\",\"assetPrefix\":\"/ui\",\"initialCanonicalUrl\":\"/\",\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"__PAGE__\",{},[\"$L5\",[\"$\",\"$L6\",null,{\"propsForComponent\":{\"params\":{}},\"Component\":\"$7\",\"isStaticGeneration\":true}],null]]},[null,[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_c23dc8\",\"children\":[\"$\",\"$L8\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"loadingScripts\":\"$undefined\",\"hasLoading\":false,\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L9\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[],\"styles\":null}]}]}],null]],\"initialHead\":[false,\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"🚅 LiteLLM\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"LiteLLM Proxy Admin UI\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/ui/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"meta\",\"5\",{\"name\":\"next-size-adjust\"}]]\n5:null\n"])</script><script>self.__next_f.push([1,""])</script></body></html>
|
|
@ -1,7 +1,7 @@
|
||||||
2:I[77831,[],""]
|
2:I[77831,[],""]
|
||||||
3:I[30280,["303","static/chunks/303-d80f23087a9e6aec.js","931","static/chunks/app/page-8f65fc157f538dff.js"],""]
|
3:I[30280,["303","static/chunks/303-d80f23087a9e6aec.js","931","static/chunks/app/page-f9c7d1158cda8c5f.js"],""]
|
||||||
4:I[5613,[],""]
|
4:I[5613,[],""]
|
||||||
5:I[31778,[],""]
|
5:I[31778,[],""]
|
||||||
0:["kyOCJPBB9pyUfbMKCAXr-",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","$L2",null,{"propsForComponent":{"params":{}},"Component":"$3","isStaticGeneration":true}],null]]},[null,["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_c23dc8","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/ui/_next/static/css/a40ad0909dd7838e.css","precedence":"next","crossOrigin":""}]],"$L6"]]]]
|
0:["Au9NvM222ys66B_S9303T",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},["$L1",["$","$L2",null,{"propsForComponent":{"params":{}},"Component":"$3","isStaticGeneration":true}],null]]},[null,["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_c23dc8","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"loading":"$undefined","loadingStyles":"$undefined","loadingScripts":"$undefined","hasLoading":false,"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[],"styles":null}]}]}],null]],[[["$","link","0",{"rel":"stylesheet","href":"/ui/_next/static/css/a40ad0909dd7838e.css","precedence":"next","crossOrigin":""}]],"$L6"]]]]
|
||||||
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"🚅 LiteLLM"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/ui/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"🚅 LiteLLM"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/ui/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||||
1:null
|
1:null
|
||||||
|
|
|
@ -11,8 +11,10 @@ import ChatUI from "@/components/chat_ui";
|
||||||
import Sidebar from "../components/leftnav";
|
import Sidebar from "../components/leftnav";
|
||||||
import Usage from "../components/usage";
|
import Usage from "../components/usage";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { Typography } from "antd";
|
||||||
|
|
||||||
const CreateKeyPage = () => {
|
const CreateKeyPage = () => {
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
const [userRole, setUserRole] = useState("");
|
const [userRole, setUserRole] = useState("");
|
||||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||||
const [teams, setTeams] = useState<null | any[]>(null);
|
const [teams, setTeams] = useState<null | any[]>(null);
|
||||||
|
@ -41,6 +43,9 @@ const CreateKeyPage = () => {
|
||||||
const formattedUserRole = formatUserRole(decoded.user_role);
|
const formattedUserRole = formatUserRole(decoded.user_role);
|
||||||
console.log("Decoded user_role:", formattedUserRole);
|
console.log("Decoded user_role:", formattedUserRole);
|
||||||
setUserRole(formattedUserRole);
|
setUserRole(formattedUserRole);
|
||||||
|
if (formattedUserRole == "Admin Viewer") {
|
||||||
|
setPage("usage");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("User role not defined");
|
console.log("User role not defined");
|
||||||
}
|
}
|
||||||
|
@ -66,7 +71,8 @@ const CreateKeyPage = () => {
|
||||||
if (!userRole) {
|
if (!userRole) {
|
||||||
return "Undefined Role";
|
return "Undefined Role";
|
||||||
}
|
}
|
||||||
console.log(`Received user role: ${userRole}`);
|
console.log(`Received user role: ${userRole.toLowerCase()}`);
|
||||||
|
console.log(`Received user role length: ${userRole.toLowerCase().length}`);
|
||||||
switch (userRole.toLowerCase()) {
|
switch (userRole.toLowerCase()) {
|
||||||
case "app_owner":
|
case "app_owner":
|
||||||
return "App Owner";
|
return "App Owner";
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
import { modelAvailableCall } from "./networking";
|
import { modelAvailableCall } from "./networking";
|
||||||
import openai from "openai";
|
import openai from "openai";
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { Typography } from "antd";
|
||||||
|
|
||||||
interface ChatUIProps {
|
interface ChatUIProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
|
@ -145,6 +146,16 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||||
setInputMessage("");
|
setInputMessage("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (userRole && userRole == "Admin Viewer") {
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title level={1}>Access Denied</Title>
|
||||||
|
<Paragraph>Ask your proxy admin for access to test models</Paragraph>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", position: "relative" }}>
|
<div style={{ width: "100%", position: "relative" }}>
|
||||||
<Grid className="gap-2 p-10 h-[75vh] w-full">
|
<Grid className="gap-2 p-10 h-[75vh] w-full">
|
||||||
|
|
|
@ -16,6 +16,34 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
userRole,
|
userRole,
|
||||||
defaultSelectedKey,
|
defaultSelectedKey,
|
||||||
}) => {
|
}) => {
|
||||||
|
if (userRole == "Admin Viewer") {
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
||||||
|
<Sider width={120}>
|
||||||
|
<Menu
|
||||||
|
mode="inline"
|
||||||
|
defaultSelectedKeys={
|
||||||
|
defaultSelectedKey ? defaultSelectedKey : ["4"]
|
||||||
|
}
|
||||||
|
style={{ height: "100%", borderRight: 0 }}
|
||||||
|
>
|
||||||
|
<Menu.Item key="4" onClick={() => setPage("api-keys")}>
|
||||||
|
API Keys
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="2" onClick={() => setPage("models")}>
|
||||||
|
Models
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="3" onClick={() => setPage("llm-playground")}>
|
||||||
|
Chat UI
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="1" onClick={() => setPage("usage")}>
|
||||||
|
Usage
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
</Sider>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
||||||
<Sider width={120}>
|
<Sider width={120}>
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, Title, Subtitle, Table, TableHead, TableRow, TableCell, TableBody, Metric, Grid } from "@tremor/react";
|
import {
|
||||||
|
Card,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableBody,
|
||||||
|
Metric,
|
||||||
|
Grid,
|
||||||
|
} from "@tremor/react";
|
||||||
import { modelInfoCall, userGetRequesedtModelsCall } from "./networking";
|
import { modelInfoCall, userGetRequesedtModelsCall } 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 { Typography } from "antd";
|
||||||
|
|
||||||
interface ModelDashboardProps {
|
interface ModelDashboardProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
|
@ -20,7 +32,6 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
const [modelData, setModelData] = useState<any>({ data: [] });
|
const [modelData, setModelData] = useState<any>({ data: [] });
|
||||||
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!accessToken || !token || !userRole || !userID) {
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
return;
|
return;
|
||||||
|
@ -28,17 +39,20 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
// Replace with your actual API call for model data
|
// Replace with your actual API call for model data
|
||||||
const modelDataResponse = await modelInfoCall(accessToken, userID, userRole);
|
const modelDataResponse = await modelInfoCall(
|
||||||
|
accessToken,
|
||||||
|
userID,
|
||||||
|
userRole
|
||||||
|
);
|
||||||
console.log("Model data response:", modelDataResponse.data);
|
console.log("Model data response:", modelDataResponse.data);
|
||||||
setModelData(modelDataResponse);
|
setModelData(modelDataResponse);
|
||||||
|
|
||||||
// if userRole is Admin, show the pending requests
|
// if userRole is Admin, show the pending requests
|
||||||
if (userRole === "Admin" && accessToken) {
|
if (userRole === "Admin" && accessToken) {
|
||||||
const user_requests = await userGetRequesedtModelsCall(accessToken);
|
const user_requests = await userGetRequesedtModelsCall(accessToken);
|
||||||
console.log("Pending Requests:", pendingRequests);
|
console.log("Pending Requests:", pendingRequests);
|
||||||
setPendingRequests(user_requests.requests || []);
|
setPendingRequests(user_requests.requests || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +72,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
}
|
}
|
||||||
let all_models_on_proxy: any[] = [];
|
let all_models_on_proxy: any[] = [];
|
||||||
|
|
||||||
// loop through model data and edit each row
|
// loop through model data and edit each row
|
||||||
for (let i = 0; i < modelData.data.length; i++) {
|
for (let i = 0; i < modelData.data.length; i++) {
|
||||||
let curr_model = modelData.data[i];
|
let curr_model = modelData.data[i];
|
||||||
let litellm_model_name = curr_model?.litellm_params?.model;
|
let litellm_model_name = curr_model?.litellm_params?.model;
|
||||||
|
@ -67,110 +81,155 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
||||||
|
|
||||||
let defaultProvider = "openai";
|
let defaultProvider = "openai";
|
||||||
let provider = "";
|
let provider = "";
|
||||||
let input_cost = "Undefined"
|
let input_cost = "Undefined";
|
||||||
let output_cost = "Undefined"
|
let output_cost = "Undefined";
|
||||||
let max_tokens = "Undefined"
|
let max_tokens = "Undefined";
|
||||||
|
|
||||||
// Check if litellm_model_name is null or undefined
|
// Check if litellm_model_name is null or undefined
|
||||||
if (litellm_model_name) {
|
if (litellm_model_name) {
|
||||||
// Split litellm_model_name based on "/"
|
// Split litellm_model_name based on "/"
|
||||||
let splitModel = litellm_model_name.split("/");
|
let splitModel = litellm_model_name.split("/");
|
||||||
|
|
||||||
// Get the first element in the split
|
// Get the first element in the split
|
||||||
let firstElement = splitModel[0];
|
let firstElement = splitModel[0];
|
||||||
|
|
||||||
// If there is only one element, default provider to openai
|
// If there is only one element, default provider to openai
|
||||||
provider = splitModel.length === 1 ? defaultProvider : firstElement;
|
provider = splitModel.length === 1 ? defaultProvider : firstElement;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// litellm_model_name is null or undefined, default provider to openai
|
// litellm_model_name is null or undefined, default provider to openai
|
||||||
provider = defaultProvider;
|
provider = defaultProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model_info) {
|
if (model_info) {
|
||||||
input_cost = model_info?.input_cost_per_token;
|
input_cost = model_info?.input_cost_per_token;
|
||||||
output_cost = model_info?.output_cost_per_token;
|
output_cost = model_info?.output_cost_per_token;
|
||||||
max_tokens = model_info?.max_tokens;
|
max_tokens = model_info?.max_tokens;
|
||||||
|
|
||||||
}
|
}
|
||||||
modelData.data[i].provider = provider
|
modelData.data[i].provider = provider;
|
||||||
modelData.data[i].input_cost = input_cost
|
modelData.data[i].input_cost = input_cost;
|
||||||
modelData.data[i].output_cost = output_cost
|
modelData.data[i].output_cost = output_cost;
|
||||||
modelData.data[i].max_tokens = max_tokens
|
modelData.data[i].max_tokens = max_tokens;
|
||||||
|
|
||||||
all_models_on_proxy.push(curr_model.model_name);
|
all_models_on_proxy.push(curr_model.model_name);
|
||||||
|
|
||||||
console.log(modelData.data[i]);
|
console.log(modelData.data[i]);
|
||||||
|
|
||||||
}
|
}
|
||||||
// when users click request access show pop up to allow them to request access
|
// when users click request access show pop up to allow them to request access
|
||||||
|
|
||||||
|
if (userRole && userRole == "Admin Viewer") {
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title level={1}>Access Denied</Title>
|
||||||
|
<Paragraph>
|
||||||
|
Ask your proxy admin for access to view all models
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<Grid className="gap-2 p-10 h-[75vh] w-full">
|
<Grid className="gap-2 p-10 h-[75vh] w-full">
|
||||||
<Card>
|
<Card>
|
||||||
<Table className="mt-5">
|
<Table className="mt-5">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell><Title>Model Name </Title></TableCell>
|
|
||||||
<TableCell><Title>Provider</Title></TableCell>
|
|
||||||
<TableCell><Title>Access</Title></TableCell>
|
|
||||||
<TableCell><Title>Input Price per token ($)</Title></TableCell>
|
|
||||||
<TableCell><Title>Output Price per token ($)</Title></TableCell>
|
|
||||||
<TableCell><Title>Max Tokens</Title></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{modelData.data.map((model: any) => (
|
|
||||||
<TableRow key={model.model_name}>
|
|
||||||
|
|
||||||
<TableCell><Title>{model.model_name}</Title></TableCell>
|
|
||||||
<TableCell>{model.provider}</TableCell>
|
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{model.user_access ? <Badge color={"green"}>Yes</Badge> : <RequestAccess userModels={all_models_on_proxy} accessToken={accessToken} userID={userID}></RequestAccess>}
|
<Title>Model Name </Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>Provider</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>Access</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>Input Price per token ($)</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>Output Price per token ($)</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>Max Tokens</Title>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>{model.input_cost}</TableCell>
|
|
||||||
<TableCell>{model.output_cost}</TableCell>
|
|
||||||
<TableCell>{model.max_tokens}</TableCell>
|
|
||||||
|
|
||||||
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHead>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{modelData.data.map((model: any) => (
|
||||||
</Card>
|
<TableRow key={model.model_name}>
|
||||||
{
|
<TableCell>
|
||||||
userRole === "Admin" && pendingRequests && pendingRequests.length > 0 ? (
|
<Title>{model.model_name}</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{model.provider}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
{model.user_access ? (
|
||||||
|
<Badge color={"green"}>Yes</Badge>
|
||||||
|
) : (
|
||||||
|
<RequestAccess
|
||||||
|
userModels={all_models_on_proxy}
|
||||||
|
accessToken={accessToken}
|
||||||
|
userID={userID}
|
||||||
|
></RequestAccess>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>{model.input_cost}</TableCell>
|
||||||
|
<TableCell>{model.output_cost}</TableCell>
|
||||||
|
<TableCell>{model.max_tokens}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
{userRole === "Admin" &&
|
||||||
|
pendingRequests &&
|
||||||
|
pendingRequests.length > 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<Title>Pending Requests</Title>
|
<Title>Pending Requests</Title>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell><Title>User ID</Title></TableCell>
|
<TableCell>
|
||||||
<TableCell><Title>Requested Models</Title></TableCell>
|
<Title>User ID</Title>
|
||||||
<TableCell><Title>Justification</Title></TableCell>
|
</TableCell>
|
||||||
<TableCell><Title>Justification</Title></TableCell>
|
<TableCell>
|
||||||
|
<Title>Requested Models</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>Justification</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>Justification</Title>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{pendingRequests.map((request: any) => (
|
{pendingRequests.map((request: any) => (
|
||||||
<TableRow key={request.request_id}>
|
<TableRow key={request.request_id}>
|
||||||
<TableCell><p>{request.user_id}</p></TableCell>
|
<TableCell>
|
||||||
<TableCell><p>{request.models[0]}</p></TableCell>
|
<p>{request.user_id}</p>
|
||||||
<TableCell><p>{request.justification}</p></TableCell>
|
</TableCell>
|
||||||
<TableCell><p>{request.user_id}</p></TableCell>
|
<TableCell>
|
||||||
|
<p>{request.models[0]}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<p>{request.justification}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<p>{request.user_id}</p>
|
||||||
|
</TableCell>
|
||||||
<Button>Approve</Button>
|
<Button>Approve</Button>
|
||||||
<Button variant="secondary" className="ml-2">Deny</Button>
|
<Button variant="secondary" className="ml-2">
|
||||||
|
Deny
|
||||||
|
</Button>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Card>
|
</Card>
|
||||||
) : null
|
) : null}
|
||||||
}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -175,7 +175,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
|
||||||
* If user is App Owner - use the normal spend logs call
|
* If user is App Owner - use the normal spend logs call
|
||||||
*/
|
*/
|
||||||
console.log(`user role: ${userRole}`);
|
console.log(`user role: ${userRole}`);
|
||||||
if (userRole == "Admin") {
|
if (userRole == "Admin" || userRole == "Admin Viewer") {
|
||||||
const overall_spend = await adminSpendLogsCall(accessToken);
|
const overall_spend = await adminSpendLogsCall(accessToken);
|
||||||
setKeySpendData(overall_spend);
|
setKeySpendData(overall_spend);
|
||||||
const top_keys = await adminTopKeysCall(accessToken);
|
const top_keys = await adminTopKeysCall(accessToken);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ViewUserSpend from "./view_user_spend";
|
||||||
import DashboardTeam from "./dashboard_default_team";
|
import DashboardTeam from "./dashboard_default_team";
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { Typography } from "antd";
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
console.log("isLocal:", isLocal);
|
console.log("isLocal:", isLocal);
|
||||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||||
|
@ -73,6 +73,10 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
return "App Owner";
|
return "App Owner";
|
||||||
case "app_admin":
|
case "app_admin":
|
||||||
return "Admin";
|
return "Admin";
|
||||||
|
case "proxy_admin":
|
||||||
|
return "Admin";
|
||||||
|
case "proxy_admin_viewer":
|
||||||
|
return "Admin Viewer";
|
||||||
case "app_user":
|
case "app_user":
|
||||||
return "App User";
|
return "App User";
|
||||||
default:
|
default:
|
||||||
|
@ -180,6 +184,16 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
setUserRole("App Owner");
|
setUserRole("App Owner");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userRole && userRole == "Admin Viewer") {
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title level={1}>Access Denied</Title>
|
||||||
|
<Paragraph>Ask your proxy admin for access to create keys</Paragraph>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
|
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, Title, Subtitle, Table, TableHead, TableRow, TableCell, TableBody, Metric, Grid } from "@tremor/react";
|
import {
|
||||||
|
Card,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableBody,
|
||||||
|
Metric,
|
||||||
|
Grid,
|
||||||
|
} from "@tremor/react";
|
||||||
import { userInfoCall } from "./networking";
|
import { userInfoCall } 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";
|
||||||
|
|
||||||
|
@ -21,7 +32,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
const [userData, setuserData] = useState<null | any[]>(null);
|
const [userData, setuserData] = useState<null | any[]>(null);
|
||||||
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!accessToken || !token || !userRole || !userID) {
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
return;
|
return;
|
||||||
|
@ -29,10 +39,14 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
// Replace with your actual API call for model data
|
// Replace with your actual API call for model data
|
||||||
const userDataResponse = await userInfoCall(accessToken, null, userRole, true);
|
const userDataResponse = await userInfoCall(
|
||||||
|
accessToken,
|
||||||
|
null,
|
||||||
|
userRole,
|
||||||
|
true
|
||||||
|
);
|
||||||
console.log("user data response:", userDataResponse);
|
console.log("user data response:", userDataResponse);
|
||||||
setuserData(userDataResponse);
|
setuserData(userDataResponse);
|
||||||
|
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
|
@ -51,40 +65,62 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// when users click request access show pop up to allow them to request access
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<Grid className="gap-2 p-10 h-[75vh] w-full">
|
<Grid className="gap-2 p-10 h-[75vh] w-full">
|
||||||
<CreateUser
|
<CreateUser userID={userID} accessToken={accessToken} />
|
||||||
userID={userID}
|
<Card>
|
||||||
accessToken={accessToken}
|
<Table className="mt-5">
|
||||||
/>
|
<TableHead>
|
||||||
<Card>
|
<TableRow>
|
||||||
<Table className="mt-5">
|
<TableCell>
|
||||||
<TableHead>
|
<Title>User ID </Title>
|
||||||
<TableRow>
|
</TableCell>
|
||||||
<TableCell><Title>User ID </Title></TableCell>
|
<TableCell>
|
||||||
<TableCell><Title>User Role</Title></TableCell>
|
<Title>User Role</Title>
|
||||||
<TableCell><Title>User Models</Title></TableCell>
|
</TableCell>
|
||||||
<TableCell><Title>User Spend ($ USD)</Title></TableCell>
|
<TableCell>
|
||||||
<TableCell><Title>User Max Budget ($ USD)</Title></TableCell>
|
<Title>User Models</Title>
|
||||||
</TableRow>
|
</TableCell>
|
||||||
</TableHead>
|
<TableCell>
|
||||||
<TableBody>
|
<Title>User Spend ($ USD)</Title>
|
||||||
{userData.map((user: any) => (
|
</TableCell>
|
||||||
<TableRow key={user.user_id}>
|
<TableCell>
|
||||||
<TableCell><Title>{user.user_id}</Title></TableCell>
|
<Title>User Max Budget ($ USD)</Title>
|
||||||
<TableCell><Title>{user.user_role ? user.user_role : "app_user"}</Title></TableCell>
|
</TableCell>
|
||||||
<TableCell><Title>{user.models && user.models.length > 0 ? user.models : "All Models"}</Title></TableCell>
|
|
||||||
<TableCell><Title>{user.spend ? user.spend : 0}</Title></TableCell>
|
|
||||||
<TableCell><Title>{user.max_budget ? user.max_budget : "Unlimited"}</Title></TableCell>
|
|
||||||
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHead>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{userData.map((user: any) => (
|
||||||
</Card>
|
<TableRow key={user.user_id}>
|
||||||
|
<TableCell>
|
||||||
|
<Title>{user.user_id}</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>
|
||||||
|
{user.user_role ? user.user_role : "app_user"}
|
||||||
|
</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>
|
||||||
|
{user.models && user.models.length > 0
|
||||||
|
? user.models
|
||||||
|
: "All Models"}
|
||||||
|
</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>{user.spend ? user.spend : 0}</Title>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Title>
|
||||||
|
{user.max_budget ? user.max_budget : "Unlimited"}
|
||||||
|
</Title>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue