mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Display Error from Backend on the UI - Keys Page (#13435)
* fix sso logout - add a new login page with sso button * lint fix * lint fix * lint fix * fix tests * fix test * Revert "fix test" This reverts commit74eb734571. * Reapply "fix test" This reverts commit72d0b2d4c6. * add host to add modal * close modal after save is clicked. and auto-refresh * show old values in edit modal * send the whole payload on edit * Update settings.tsx * resolve conflict * fix conflict * merge main * first draft of notifications added to settings * add error compatibility by taking errors from the backend - db errors - auth errors * add support for different types of errors * minor * name change * email alerts page notifications modified * remove unused code * move create_key to organisms/ folder * move view_key_table to templates * keys page notifications * move regenerate_key to organisms * notifications for regenerate key modal * move key info view and key edit view to templates/ folder * replace "Network response was not ok" with error data * resolve merge conflict
This commit is contained in:
parent
280ad6f049
commit
911b0cfd73
12 changed files with 1670 additions and 1959 deletions
|
|
@ -1,234 +1,220 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import Navbar from "@/components/navbar";
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import UserDashboard from "@/components/user_dashboard";
|
||||
import ModelDashboard from "@/components/model_dashboard";
|
||||
import ViewUserDashboard from "@/components/view_users";
|
||||
import Teams from "@/components/teams";
|
||||
import Organizations from "@/components/organizations";
|
||||
import { fetchOrganizations } from "@/components/organizations";
|
||||
import AdminPanel from "@/components/admins";
|
||||
import Settings from "@/components/settings";
|
||||
import GeneralSettings from "@/components/general_settings";
|
||||
import PassThroughSettings from "@/components/pass_through_settings";
|
||||
import BudgetPanel from "@/components/budgets/budget_panel";
|
||||
import SpendLogsTable from "@/components/view_logs";
|
||||
import ModelHubTable from "@/components/model_hub_table";
|
||||
import NewUsagePage from "@/components/new_usage";
|
||||
import APIRef from "@/components/api_ref";
|
||||
import ChatUI from "@/components/chat_ui";
|
||||
import Sidebar from "@/components/leftnav";
|
||||
import Usage from "@/components/usage";
|
||||
import CacheDashboard from "@/components/cache_dashboard";
|
||||
import {
|
||||
getUiConfig,
|
||||
proxyBaseUrl,
|
||||
setGlobalLitellmHeaderName,
|
||||
} from "@/components/networking";
|
||||
import { Organization } from "@/components/networking";
|
||||
import GuardrailsPanel from "@/components/guardrails";
|
||||
import PromptsPanel from "@/components/prompts";
|
||||
import TransformRequestPanel from "@/components/transform_request";
|
||||
import { fetchUserModels } from "@/components/create_key_button";
|
||||
import { fetchTeams } from "@/components/common_components/fetch_teams";
|
||||
import { MCPServers } from "@/components/mcp_tools";
|
||||
import TagManagement from "@/components/tag_management";
|
||||
import VectorStoreManagement from "@/components/vector_store_management";
|
||||
import UIThemeSettings from "@/components/ui_theme_settings";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { cx } from "@/lib/cva.config";
|
||||
import React, { Suspense, useEffect, useState } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { jwtDecode } from "jwt-decode"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { Team } from "@/components/key_team_helpers/key_list"
|
||||
import Navbar from "@/components/navbar"
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext"
|
||||
import UserDashboard from "@/components/user_dashboard"
|
||||
import ModelDashboard from "@/components/model_dashboard"
|
||||
import ViewUserDashboard from "@/components/view_users"
|
||||
import Teams from "@/components/teams"
|
||||
import Organizations from "@/components/organizations"
|
||||
import { fetchOrganizations } from "@/components/organizations"
|
||||
import AdminPanel from "@/components/admins"
|
||||
import Settings from "@/components/settings"
|
||||
import GeneralSettings from "@/components/general_settings"
|
||||
import PassThroughSettings from "@/components/pass_through_settings"
|
||||
import BudgetPanel from "@/components/budgets/budget_panel"
|
||||
import SpendLogsTable from "@/components/view_logs"
|
||||
import ModelHubTable from "@/components/model_hub_table"
|
||||
import NewUsagePage from "@/components/new_usage"
|
||||
import APIRef from "@/components/api_ref"
|
||||
import ChatUI from "@/components/chat_ui"
|
||||
import Sidebar from "@/components/leftnav"
|
||||
import Usage from "@/components/usage"
|
||||
import CacheDashboard from "@/components/cache_dashboard"
|
||||
import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"
|
||||
import { Organization } from "@/components/networking"
|
||||
import GuardrailsPanel from "@/components/guardrails"
|
||||
import PromptsPanel from "@/components/prompts"
|
||||
import TransformRequestPanel from "@/components/transform_request"
|
||||
import { fetchUserModels } from "@/components/organisms/create_key_button"
|
||||
import { fetchTeams } from "@/components/common_components/fetch_teams"
|
||||
import { MCPServers } from "@/components/mcp_tools"
|
||||
import TagManagement from "@/components/tag_management"
|
||||
import VectorStoreManagement from "@/components/vector_store_management"
|
||||
import UIThemeSettings from "@/components/ui_theme_settings"
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"
|
||||
import { cx } from "@/lib/cva.config"
|
||||
|
||||
function getCookie(name: string) {
|
||||
const cookieValue = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith(name + "="));
|
||||
return cookieValue ? cookieValue.split("=")[1] : null;
|
||||
const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "="))
|
||||
return cookieValue ? cookieValue.split("=")[1] : null
|
||||
}
|
||||
|
||||
function formatUserRole(userRole: string) {
|
||||
if (!userRole) {
|
||||
return "Undefined Role";
|
||||
return "Undefined Role"
|
||||
}
|
||||
switch (userRole.toLowerCase()) {
|
||||
case "app_owner":
|
||||
return "App Owner";
|
||||
return "App Owner"
|
||||
case "demo_app_owner":
|
||||
return "App Owner";
|
||||
return "App Owner"
|
||||
case "app_admin":
|
||||
return "Admin";
|
||||
return "Admin"
|
||||
case "proxy_admin":
|
||||
return "Admin";
|
||||
return "Admin"
|
||||
case "proxy_admin_viewer":
|
||||
return "Admin Viewer";
|
||||
return "Admin Viewer"
|
||||
case "org_admin":
|
||||
return "Org Admin";
|
||||
return "Org Admin"
|
||||
case "internal_user":
|
||||
return "Internal User";
|
||||
return "Internal User"
|
||||
case "internal_user_viewer":
|
||||
case "internal_viewer": // TODO:remove if deprecated
|
||||
return "Internal Viewer";
|
||||
return "Internal Viewer"
|
||||
case "app_user":
|
||||
return "App User";
|
||||
return "App User"
|
||||
default:
|
||||
return "Unknown Role";
|
||||
return "Unknown Role"
|
||||
}
|
||||
}
|
||||
|
||||
interface ProxySettings {
|
||||
PROXY_BASE_URL: string;
|
||||
PROXY_LOGOUT_URL: string;
|
||||
PROXY_BASE_URL: string
|
||||
PROXY_LOGOUT_URL: string
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
function LoadingScreen() {
|
||||
return (
|
||||
<div className={cx("h-screen", "flex items-center justify-center gap-4")}>
|
||||
<div className="text-lg font-medium py-2 pr-4 border-r border-r-gray-200">
|
||||
🚅 LiteLLM
|
||||
</div>
|
||||
<div className="text-lg font-medium py-2 pr-4 border-r border-r-gray-200">🚅 LiteLLM</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<UiLoadingSpinner className="size-4" />
|
||||
<span className="text-gray-600 text-sm">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default function CreateKeyPage() {
|
||||
const [userRole, setUserRole] = useState("");
|
||||
const [premiumUser, setPremiumUser] = useState(false);
|
||||
const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] =
|
||||
useState(false);
|
||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||
const [teams, setTeams] = useState<Team[] | null>(null);
|
||||
const [keys, setKeys] = useState<null | any[]>([]);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const [userRole, setUserRole] = useState("")
|
||||
const [premiumUser, setPremiumUser] = useState(false)
|
||||
const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false)
|
||||
const [userEmail, setUserEmail] = useState<null | string>(null)
|
||||
const [teams, setTeams] = useState<Team[] | null>(null)
|
||||
const [keys, setKeys] = useState<null | any[]>([])
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([])
|
||||
const [userModels, setUserModels] = useState<string[]>([])
|
||||
const [proxySettings, setProxySettings] = useState<ProxySettings>({
|
||||
PROXY_BASE_URL: "",
|
||||
PROXY_LOGOUT_URL: "",
|
||||
});
|
||||
})
|
||||
|
||||
const [showSSOBanner, setShowSSOBanner] = useState<boolean>(true);
|
||||
const searchParams = useSearchParams()!;
|
||||
const [modelData, setModelData] = useState<any>({ data: [] });
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [createClicked, setCreateClicked] = useState<boolean>(false);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const [userID, setUserID] = useState<string | null>(null);
|
||||
const [showSSOBanner, setShowSSOBanner] = useState<boolean>(true)
|
||||
const searchParams = useSearchParams()!
|
||||
const [modelData, setModelData] = useState<any>({ data: [] })
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [createClicked, setCreateClicked] = useState<boolean>(false)
|
||||
const [authLoading, setAuthLoading] = useState(true)
|
||||
const [userID, setUserID] = useState<string | null>(null)
|
||||
|
||||
const invitation_id = searchParams.get("invitation_id");
|
||||
const invitation_id = searchParams.get("invitation_id")
|
||||
|
||||
// Get page from URL, default to 'api-keys' if not present
|
||||
const [page, setPage] = useState(() => {
|
||||
return searchParams.get("page") || "api-keys";
|
||||
});
|
||||
return searchParams.get("page") || "api-keys"
|
||||
})
|
||||
|
||||
// Custom setPage function that updates URL
|
||||
const updatePage = (newPage: string) => {
|
||||
// Update URL without full page reload
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.set("page", newPage);
|
||||
const newSearchParams = new URLSearchParams(searchParams)
|
||||
newSearchParams.set("page", newPage)
|
||||
|
||||
// Use Next.js router to update URL
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`)
|
||||
|
||||
setPage(newPage);
|
||||
};
|
||||
setPage(newPage)
|
||||
}
|
||||
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null)
|
||||
|
||||
const addKey = (data: any) => {
|
||||
setKeys((prevData) => (prevData ? [...prevData, data] : [data]));
|
||||
setCreateClicked(() => !createClicked);
|
||||
};
|
||||
const redirectToLogin =
|
||||
authLoading === false && token === null && invitation_id === null;
|
||||
setKeys((prevData) => (prevData ? [...prevData, data] : [data]))
|
||||
setCreateClicked(() => !createClicked)
|
||||
}
|
||||
const redirectToLogin = authLoading === false && token === null && invitation_id === null
|
||||
|
||||
useEffect(() => {
|
||||
const token = getCookie("token");
|
||||
const token = getCookie("token")
|
||||
getUiConfig().then((data) => {
|
||||
// get the information for constructing the proxy base url, and then set the token and auth loading
|
||||
setToken(token);
|
||||
setAuthLoading(false);
|
||||
});
|
||||
}, []);
|
||||
setToken(token)
|
||||
setAuthLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectToLogin) {
|
||||
window.location.href = (proxyBaseUrl || "") + "/sso/key/generate";
|
||||
window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"
|
||||
}
|
||||
}, [redirectToLogin]);
|
||||
}, [redirectToLogin])
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||
const decoded = jwtDecode(token) as { [key: string]: any }
|
||||
if (decoded) {
|
||||
// set accessToken
|
||||
setAccessToken(decoded.key);
|
||||
setAccessToken(decoded.key)
|
||||
|
||||
setDisabledPersonalKeyCreation(
|
||||
decoded.disabled_non_admin_personal_key_creation
|
||||
);
|
||||
setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation)
|
||||
|
||||
// check if userRole is defined
|
||||
if (decoded.user_role) {
|
||||
const formattedUserRole = formatUserRole(decoded.user_role);
|
||||
setUserRole(formattedUserRole);
|
||||
const formattedUserRole = formatUserRole(decoded.user_role)
|
||||
setUserRole(formattedUserRole)
|
||||
if (formattedUserRole == "Admin Viewer") {
|
||||
setPage("usage");
|
||||
setPage("usage")
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded.user_email) {
|
||||
setUserEmail(decoded.user_email);
|
||||
setUserEmail(decoded.user_email)
|
||||
}
|
||||
|
||||
if (decoded.login_method) {
|
||||
setShowSSOBanner(
|
||||
decoded.login_method == "username_password" ? true : false
|
||||
);
|
||||
setShowSSOBanner(decoded.login_method == "username_password" ? true : false)
|
||||
}
|
||||
|
||||
if (decoded.premium_user) {
|
||||
setPremiumUser(decoded.premium_user);
|
||||
setPremiumUser(decoded.premium_user)
|
||||
}
|
||||
|
||||
if (decoded.auth_header_name) {
|
||||
setGlobalLitellmHeaderName(decoded.auth_header_name);
|
||||
setGlobalLitellmHeaderName(decoded.auth_header_name)
|
||||
}
|
||||
|
||||
if (decoded.user_id) {
|
||||
setUserID(decoded.user_id);
|
||||
setUserID(decoded.user_id)
|
||||
}
|
||||
}
|
||||
}, [token]);
|
||||
}, [token])
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken && userID && userRole) {
|
||||
fetchUserModels(userID, userRole, accessToken, setUserModels);
|
||||
fetchUserModels(userID, userRole, accessToken, setUserModels)
|
||||
}
|
||||
if (accessToken && userID && userRole) {
|
||||
fetchTeams(accessToken, userID, userRole, null, setTeams);
|
||||
fetchTeams(accessToken, userID, userRole, null, setTeams)
|
||||
}
|
||||
if (accessToken) {
|
||||
fetchOrganizations(accessToken, setOrganizations);
|
||||
fetchOrganizations(accessToken, setOrganizations)
|
||||
}
|
||||
}, [accessToken, userID, userRole]);
|
||||
}, [accessToken, userID, userRole])
|
||||
|
||||
if (authLoading || redirectToLogin) {
|
||||
return <LoadingScreen />;
|
||||
return <LoadingScreen />
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -237,226 +223,199 @@ export default function CreateKeyPage() {
|
|||
<ThemeProvider accessToken={accessToken}>
|
||||
{invitation_id ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setProxySettings={setProxySettings}
|
||||
proxySettings={proxySettings}
|
||||
accessToken={accessToken}
|
||||
isPublicPage={false}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
/>
|
||||
<div className="flex flex-1 overflow-auto">
|
||||
<div className="mt-8">
|
||||
<Sidebar
|
||||
accessToken={accessToken}
|
||||
setPage={updatePage}
|
||||
userRole={userRole}
|
||||
defaultSelectedKey={page}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
userEmail={userEmail}
|
||||
setProxySettings={setProxySettings}
|
||||
proxySettings={proxySettings}
|
||||
accessToken={accessToken}
|
||||
isPublicPage={false}
|
||||
/>
|
||||
<div className="flex flex-1 overflow-auto">
|
||||
<div className="mt-8">
|
||||
<Sidebar
|
||||
accessToken={accessToken}
|
||||
setPage={updatePage}
|
||||
userRole={userRole}
|
||||
defaultSelectedKey={page}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{page == "api-keys" ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
/>
|
||||
) : page == "models" ? (
|
||||
<ModelDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
keys={keys}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
setModelData={setModelData}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
/>
|
||||
) : page == "llm-playground" ? (
|
||||
<ChatUI
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
/>
|
||||
) : page == "users" ? (
|
||||
<ViewUserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
keys={keys}
|
||||
teams={teams}
|
||||
accessToken={accessToken}
|
||||
setKeys={setKeys}
|
||||
/>
|
||||
) : page == "teams" ? (
|
||||
<Teams
|
||||
teams={teams}
|
||||
setTeams={setTeams}
|
||||
searchParams={searchParams}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
organizations={organizations}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "organizations" ? (
|
||||
<Organizations
|
||||
organizations={organizations}
|
||||
setOrganizations={setOrganizations}
|
||||
userModels={userModels}
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "admin-panel" ? (
|
||||
<AdminPanel
|
||||
setTeams={setTeams}
|
||||
searchParams={searchParams}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
showSSOBanner={showSSOBanner}
|
||||
premiumUser={premiumUser}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : page == "api_ref" ? (
|
||||
<APIRef proxySettings={proxySettings} />
|
||||
) : page == "settings" ? (
|
||||
<Settings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "budgets" ? (
|
||||
<BudgetPanel accessToken={accessToken} />
|
||||
) : page == "guardrails" ? (
|
||||
<GuardrailsPanel
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : page == "prompts" ? (
|
||||
<PromptsPanel
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : page == "transform-request" ? (
|
||||
<TransformRequestPanel accessToken={accessToken} />
|
||||
) : page == "general-settings" ? (
|
||||
<GeneralSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
/>
|
||||
) : page == "ui-theme" ? (
|
||||
<UIThemeSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
) : page == "model-hub-table" ? (
|
||||
<ModelHubTable
|
||||
accessToken={accessToken}
|
||||
publicPage={false}
|
||||
premiumUser={premiumUser}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : page == "caching" ? (
|
||||
<CacheDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "pass-through-settings" ? (
|
||||
<PassThroughSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
/>
|
||||
) : page == "logs" ? (
|
||||
<SpendLogsTable
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
allTeams={(teams as Team[]) ?? []}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "mcp-servers" ? (
|
||||
<MCPServers
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
/>
|
||||
) : page == "tag-management" ? (
|
||||
<TagManagement
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
/>
|
||||
) : page == "vector-stores" ? (
|
||||
<VectorStoreManagement
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
/>
|
||||
) : page == "new_usage" ? (
|
||||
<NewUsagePage
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
teams={(teams as Team[]) ?? []}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : (
|
||||
<Usage
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
keys={keys}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
)}
|
||||
{page == "api-keys" ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
/>
|
||||
) : page == "models" ? (
|
||||
<ModelDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
keys={keys}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
setModelData={setModelData}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
/>
|
||||
) : page == "llm-playground" ? (
|
||||
<ChatUI
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
/>
|
||||
) : page == "users" ? (
|
||||
<ViewUserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
keys={keys}
|
||||
teams={teams}
|
||||
accessToken={accessToken}
|
||||
setKeys={setKeys}
|
||||
/>
|
||||
) : page == "teams" ? (
|
||||
<Teams
|
||||
teams={teams}
|
||||
setTeams={setTeams}
|
||||
searchParams={searchParams}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
organizations={organizations}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "organizations" ? (
|
||||
<Organizations
|
||||
organizations={organizations}
|
||||
setOrganizations={setOrganizations}
|
||||
userModels={userModels}
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "admin-panel" ? (
|
||||
<AdminPanel
|
||||
setTeams={setTeams}
|
||||
searchParams={searchParams}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
showSSOBanner={showSSOBanner}
|
||||
premiumUser={premiumUser}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : page == "api_ref" ? (
|
||||
<APIRef proxySettings={proxySettings} />
|
||||
) : page == "settings" ? (
|
||||
<Settings userID={userID} userRole={userRole} accessToken={accessToken} premiumUser={premiumUser} />
|
||||
) : page == "budgets" ? (
|
||||
<BudgetPanel accessToken={accessToken} />
|
||||
) : page == "guardrails" ? (
|
||||
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "prompts" ? (
|
||||
<PromptsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "transform-request" ? (
|
||||
<TransformRequestPanel accessToken={accessToken} />
|
||||
) : page == "general-settings" ? (
|
||||
<GeneralSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
/>
|
||||
) : page == "ui-theme" ? (
|
||||
<UIThemeSettings userID={userID} userRole={userRole} accessToken={accessToken} />
|
||||
) : page == "model-hub-table" ? (
|
||||
<ModelHubTable
|
||||
accessToken={accessToken}
|
||||
publicPage={false}
|
||||
premiumUser={premiumUser}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : page == "caching" ? (
|
||||
<CacheDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "pass-through-settings" ? (
|
||||
<PassThroughSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
/>
|
||||
) : page == "logs" ? (
|
||||
<SpendLogsTable
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
allTeams={(teams as Team[]) ?? []}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "mcp-servers" ? (
|
||||
<MCPServers accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "tag-management" ? (
|
||||
<TagManagement accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "vector-stores" ? (
|
||||
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "new_usage" ? (
|
||||
<NewUsagePage
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
teams={(teams as Team[]) ?? []}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : (
|
||||
<Usage
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
keys={keys}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</Suspense>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +1,54 @@
|
|||
"use client";
|
||||
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
import { DataTable } from "./view_logs/table";
|
||||
"use client"
|
||||
import React, { useEffect, useState, useCallback, useRef } from "react"
|
||||
import { ColumnDef, Row } from "@tanstack/react-table"
|
||||
import { DataTable } from "./view_logs/table"
|
||||
import { Select, SelectItem } from "@tremor/react"
|
||||
import { Button } from "@tremor/react"
|
||||
import KeyInfoView from "./key_info_view";
|
||||
import { Tooltip } from "antd";
|
||||
import { Team, KeyResponse } from "./key_team_helpers/key_list";
|
||||
import FilterComponent from "./common_components/filter";
|
||||
import { FilterOption } from "./common_components/filter";
|
||||
import { keyListCall, Organization, userListCall } from "./networking";
|
||||
import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn";
|
||||
import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn";
|
||||
import { useFilterLogic } from "./key_team_helpers/filter_logic";
|
||||
import { Setter } from "@/types";
|
||||
import { updateExistingKeys } from "@/utils/dataUtils";
|
||||
import { debounce } from "lodash";
|
||||
import { defaultPageSize } from "./constants";
|
||||
import { fetchAllTeams } from "./key_team_helpers/filter_helpers";
|
||||
import { fetchAllOrganizations } from "./key_team_helpers/filter_helpers";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Icon,
|
||||
} from "@tremor/react";
|
||||
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { Badge, Text } from "@tremor/react";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import KeyInfoView from "./templates/key_info_view"
|
||||
import { Tooltip } from "antd"
|
||||
import { Team, KeyResponse } from "./key_team_helpers/key_list"
|
||||
import FilterComponent from "./common_components/filter"
|
||||
import { FilterOption } from "./common_components/filter"
|
||||
import { keyListCall, Organization, userListCall } from "./networking"
|
||||
import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn"
|
||||
import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn"
|
||||
import { useFilterLogic } from "./key_team_helpers/filter_logic"
|
||||
import { Setter } from "@/types"
|
||||
import { updateExistingKeys } from "@/utils/dataUtils"
|
||||
import { debounce } from "lodash"
|
||||
import { defaultPageSize } from "./constants"
|
||||
import { fetchAllTeams } from "./key_team_helpers/filter_helpers"
|
||||
import { fetchAllOrganizations } from "./key_team_helpers/filter_helpers"
|
||||
import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react"
|
||||
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"
|
||||
import { Badge, Text } from "@tremor/react"
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils"
|
||||
|
||||
interface AllKeysTableProps {
|
||||
keys: KeyResponse[]
|
||||
setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void
|
||||
isLoading?: boolean
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
};
|
||||
onPageChange: (page: number) => void;
|
||||
pageSize?: number;
|
||||
teams: Team[] | null;
|
||||
selectedTeam: Team | null;
|
||||
setSelectedTeam: (team: Team | null) => void;
|
||||
selectedKeyAlias: string | null;
|
||||
setSelectedKeyAlias: Setter<string | null>;
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
organizations: Organization[] | null;
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
|
||||
refresh?: () => void;
|
||||
onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void;
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
totalCount: number
|
||||
}
|
||||
onPageChange: (page: number) => void
|
||||
pageSize?: number
|
||||
teams: Team[] | null
|
||||
selectedTeam: Team | null
|
||||
setSelectedTeam: (team: Team | null) => void
|
||||
selectedKeyAlias: string | null
|
||||
setSelectedKeyAlias: Setter<string | null>
|
||||
accessToken: string | null
|
||||
userID: string | null
|
||||
userRole: string | null
|
||||
organizations: Organization[] | null
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>
|
||||
refresh?: () => void
|
||||
onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void
|
||||
currentSort?: {
|
||||
sortBy: string
|
||||
sortOrder: "asc" | "desc"
|
||||
|
|
@ -74,57 +60,55 @@ interface AllKeysTableProps {
|
|||
// Define columns similar to our logs table
|
||||
|
||||
interface UserResponse {
|
||||
user_id: string;
|
||||
user_email: string;
|
||||
user_role: string;
|
||||
user_id: string
|
||||
user_email: string
|
||||
user_role: string
|
||||
}
|
||||
|
||||
const TeamFilter = ({
|
||||
teams,
|
||||
selectedTeam,
|
||||
setSelectedTeam
|
||||
}: {
|
||||
teams: Team[] | null;
|
||||
selectedTeam: Team | null;
|
||||
setSelectedTeam: (team: Team | null) => void;
|
||||
const TeamFilter = ({
|
||||
teams,
|
||||
selectedTeam,
|
||||
setSelectedTeam,
|
||||
}: {
|
||||
teams: Team[] | null
|
||||
selectedTeam: Team | null
|
||||
setSelectedTeam: (team: Team | null) => void
|
||||
}) => {
|
||||
const handleTeamChange = (value: string) => {
|
||||
const team = teams?.find(t => t.team_id === value);
|
||||
setSelectedTeam(team || null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Where Team is</span>
|
||||
<Select
|
||||
value={selectedTeam?.team_id || ""}
|
||||
onValueChange={handleTeamChange}
|
||||
placeholder="Team ID"
|
||||
className="w-[400px]"
|
||||
>
|
||||
<SelectItem value="team_id">Team ID</SelectItem>
|
||||
{teams?.map((team) => (
|
||||
<SelectItem key={team.team_id} value={team.team_id}>
|
||||
<span className="font-medium">{team.team_alias}</span>{" "}
|
||||
<span className="text-gray-500">({team.team_id})</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
const handleTeamChange = (value: string) => {
|
||||
const team = teams?.find((t) => t.team_id === value)
|
||||
setSelectedTeam(team || null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Where Team is</span>
|
||||
<Select
|
||||
value={selectedTeam?.team_id || ""}
|
||||
onValueChange={handleTeamChange}
|
||||
placeholder="Team ID"
|
||||
className="w-[400px]"
|
||||
>
|
||||
<SelectItem value="team_id">Team ID</SelectItem>
|
||||
{teams?.map((team) => (
|
||||
<SelectItem key={team.team_id} value={team.team_id}>
|
||||
<span className="font-medium">{team.team_alias}</span>{" "}
|
||||
<span className="text-gray-500">({team.team_id})</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* AllKeysTable – a new table for keys that mimics the table styling used in view_logs.
|
||||
* The team selector and filtering have been removed so that all keys are shown.
|
||||
*/
|
||||
|
||||
|
||||
export function AllKeysTable({
|
||||
keys,
|
||||
export function AllKeysTable({
|
||||
keys,
|
||||
setKeys,
|
||||
isLoading = false,
|
||||
pagination,
|
||||
|
|
@ -146,67 +130,62 @@ export function AllKeysTable({
|
|||
premiumUser,
|
||||
setAccessToken,
|
||||
}: AllKeysTableProps) {
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
|
||||
const [userList, setUserList] = useState<UserResponse[]>([]);
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null)
|
||||
const [userList, setUserList] = useState<UserResponse[]>([])
|
||||
const [sorting, setSorting] = React.useState<SortingState>(() => {
|
||||
if (currentSort) {
|
||||
return [{
|
||||
id: currentSort.sortBy,
|
||||
desc: currentSort.sortOrder === 'desc'
|
||||
}];
|
||||
return [
|
||||
{
|
||||
id: currentSort.sortBy,
|
||||
desc: currentSort.sortOrder === "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
return [{
|
||||
id: "created_at",
|
||||
desc: true
|
||||
}];
|
||||
});
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
|
||||
return [
|
||||
{
|
||||
id: "created_at",
|
||||
desc: true,
|
||||
},
|
||||
]
|
||||
})
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Use the filter logic hook
|
||||
|
||||
const {
|
||||
filters,
|
||||
filteredKeys,
|
||||
allKeyAliases,
|
||||
allTeams,
|
||||
allOrganizations,
|
||||
handleFilterChange,
|
||||
handleFilterReset
|
||||
} = useFilterLogic({
|
||||
keys,
|
||||
teams,
|
||||
organizations,
|
||||
accessToken,
|
||||
});
|
||||
|
||||
|
||||
const { filters, filteredKeys, allKeyAliases, allTeams, allOrganizations, handleFilterChange, handleFilterReset } =
|
||||
useFilterLogic({
|
||||
keys,
|
||||
teams,
|
||||
organizations,
|
||||
accessToken,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
const user_IDs = keys.map(key => key.user_id).filter(id => id !== null);
|
||||
const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null)
|
||||
const fetchUserList = async () => {
|
||||
const userListData = await userListCall(accessToken, user_IDs, 1, 100);
|
||||
setUserList(userListData.users);
|
||||
};
|
||||
fetchUserList();
|
||||
const userListData = await userListCall(accessToken, user_IDs, 1, 100)
|
||||
setUserList(userListData.users)
|
||||
}
|
||||
fetchUserList()
|
||||
}
|
||||
}, [accessToken, keys]);
|
||||
}, [accessToken, keys])
|
||||
|
||||
// Add a useEffect to call refresh when a key is created
|
||||
useEffect(() => {
|
||||
if (refresh) {
|
||||
const handleStorageChange = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
// Listen for storage events that might indicate a key was created
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
window.addEventListener("storage", handleStorageChange)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
window.removeEventListener("storage", handleStorageChange)
|
||||
}
|
||||
}
|
||||
}, [refresh]);
|
||||
}, [refresh])
|
||||
|
||||
const columns: ColumnDef<KeyResponse>[] = [
|
||||
{
|
||||
|
|
@ -214,10 +193,7 @@ export function AllKeysTable({
|
|||
header: () => null,
|
||||
cell: ({ row }) =>
|
||||
row.getCanExpand() ? (
|
||||
<button
|
||||
onClick={row.getToggleExpandedHandler()}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<button onClick={row.getToggleExpandedHandler()} style={{ cursor: "pointer" }}>
|
||||
{row.getIsExpanded() ? "▼" : "▶"}
|
||||
</button>
|
||||
) : null,
|
||||
|
|
@ -229,7 +205,7 @@ export function AllKeysTable({
|
|||
cell: (info) => (
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip title={info.getValue() as string}>
|
||||
<Button
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
|
|
@ -246,9 +222,9 @@ export function AllKeysTable({
|
|||
accessorKey: "key_alias",
|
||||
header: "Key Alias",
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
const value = info.getValue() as string
|
||||
return <Tooltip title={value}>{value ? (value.length > 20 ? `${value.slice(0, 20)}...` : value) : "-"}</Tooltip>
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "key_name",
|
||||
|
|
@ -261,35 +237,41 @@ export function AllKeysTable({
|
|||
accessorKey: "team_id",
|
||||
header: "Team Alias",
|
||||
cell: ({ row, getValue }) => {
|
||||
const teamId = getValue() as string;
|
||||
const team = teams?.find(t => t.team_id === teamId);
|
||||
return team?.team_alias || "Unknown";
|
||||
const teamId = getValue() as string
|
||||
const team = teams?.find((t) => t.team_id === teamId)
|
||||
return team?.team_alias || "Unknown"
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "team_id",
|
||||
accessorKey: "team_id",
|
||||
header: "Team ID",
|
||||
cell: (info) => <Tooltip title={info.getValue() as string}>{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"}</Tooltip>
|
||||
cell: (info) => (
|
||||
<Tooltip title={info.getValue() as string}>
|
||||
{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "organization_id",
|
||||
accessorKey: "organization_id",
|
||||
header: "Organization ID",
|
||||
cell: (info) => info.getValue() ? info.renderValue() : "-",
|
||||
cell: (info) => (info.getValue() ? info.renderValue() : "-"),
|
||||
},
|
||||
{
|
||||
id: "user_email",
|
||||
accessorKey: "user_id",
|
||||
header: "User Email",
|
||||
cell: (info) => {
|
||||
const userId = info.getValue() as string;
|
||||
const user = userList.find(u => u.user_id === userId);
|
||||
const userId = info.getValue() as string
|
||||
const user = userList.find((u) => u.user_id === userId)
|
||||
return user?.user_email ? (
|
||||
<Tooltip title={user?.user_email}>
|
||||
<span>{user?.user_email.slice(0, 20)}...</span>
|
||||
</Tooltip>
|
||||
) : "-";
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -297,15 +279,15 @@ export function AllKeysTable({
|
|||
accessorKey: "user_id",
|
||||
header: "User ID",
|
||||
cell: (info) => {
|
||||
const userId = info.getValue() as string | null;
|
||||
const userId = info.getValue() as string | null
|
||||
if (userId && userId.length > 15) {
|
||||
return (
|
||||
<Tooltip title={userId}>
|
||||
<span>{userId.slice(0, 7)}...</span>
|
||||
</Tooltip>
|
||||
);
|
||||
)
|
||||
}
|
||||
return userId ? userId : "-";
|
||||
return userId ? userId : "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -313,8 +295,8 @@ export function AllKeysTable({
|
|||
accessorKey: "created_at",
|
||||
header: "Created At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "-";
|
||||
const value = info.getValue()
|
||||
return value ? new Date(value as string).toLocaleDateString() : "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -322,15 +304,15 @@ export function AllKeysTable({
|
|||
accessorKey: "created_by",
|
||||
header: "Created By",
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string | null;
|
||||
const value = info.getValue() as string | null
|
||||
if (value && value.length > 15) {
|
||||
return (
|
||||
<Tooltip title={value}>
|
||||
<span>{value.slice(0, 7)}...</span>
|
||||
</Tooltip>
|
||||
);
|
||||
)
|
||||
}
|
||||
return value;
|
||||
return value
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -338,8 +320,8 @@ export function AllKeysTable({
|
|||
accessorKey: "updated_at",
|
||||
header: "Updated At",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never";
|
||||
const value = info.getValue()
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never"
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -347,8 +329,8 @@ export function AllKeysTable({
|
|||
accessorKey: "expires",
|
||||
header: "Expires",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never";
|
||||
const value = info.getValue()
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never"
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -362,11 +344,11 @@ export function AllKeysTable({
|
|||
accessorKey: "max_budget",
|
||||
header: "Budget (USD)",
|
||||
cell: (info) => {
|
||||
const maxBudget = info.getValue() as number | null;
|
||||
const maxBudget = info.getValue() as number | null
|
||||
if (maxBudget === null) {
|
||||
return "Unlimited";
|
||||
return "Unlimited"
|
||||
}
|
||||
return `$${formatNumberWithCommas(maxBudget)}`;
|
||||
return `$${formatNumberWithCommas(maxBudget)}`
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -374,8 +356,8 @@ export function AllKeysTable({
|
|||
accessorKey: "budget_reset_at",
|
||||
header: "Budget Reset",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleString() : "Never";
|
||||
const value = info.getValue()
|
||||
return value ? new Date(value as string).toLocaleString() : "Never"
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -383,7 +365,7 @@ export function AllKeysTable({
|
|||
accessorKey: "models",
|
||||
header: "Models",
|
||||
cell: (info) => {
|
||||
const models = info.getValue() as string[];
|
||||
const models = info.getValue() as string[]
|
||||
return (
|
||||
<div className="flex flex-col py-2">
|
||||
{Array.isArray(models) ? (
|
||||
|
|
@ -402,68 +384,54 @@ export function AllKeysTable({
|
|||
className="cursor-pointer"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setExpandedAccordions(prev => ({
|
||||
setExpandedAccordions((prev) => ({
|
||||
...prev,
|
||||
[info.row.id]: !prev[info.row.id]
|
||||
}));
|
||||
[info.row.id]: !prev[info.row.id],
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models.slice(0, 3).map((model, index) => (
|
||||
{models.slice(0, 3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge
|
||||
key={index}
|
||||
size={"xs"}
|
||||
color="red"
|
||||
>
|
||||
<Badge key={index} size={"xs"} color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
key={index}
|
||||
size={"xs"}
|
||||
color="blue"
|
||||
>
|
||||
<Badge key={index} size={"xs"} color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
</Badge>
|
||||
)
|
||||
))}
|
||||
),
|
||||
)}
|
||||
{models.length > 3 && !expandedAccordions[info.row.id] && (
|
||||
<Badge size={"xs"} color="gray" className="cursor-pointer">
|
||||
<Text>+{models.length - 3} {models.length - 3 === 1 ? 'more model' : 'more models'}</Text>
|
||||
<Text>
|
||||
+{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"}
|
||||
</Text>
|
||||
</Badge>
|
||||
)}
|
||||
{expandedAccordions[info.row.id] && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models.slice(3).map((model, index) => (
|
||||
{models.slice(3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge
|
||||
key={index + 3}
|
||||
size={"xs"}
|
||||
color="red"
|
||||
>
|
||||
<Badge key={index + 3} size={"xs"} color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
key={index + 3}
|
||||
size={"xs"}
|
||||
color="blue"
|
||||
>
|
||||
<Badge key={index + 3} size={"xs"} color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
</Badge>
|
||||
)
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -473,154 +441,156 @@ export function AllKeysTable({
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rate_limits",
|
||||
header: "Rate Limits",
|
||||
cell: ({ row }) => {
|
||||
const key = row.original;
|
||||
const key = row.original
|
||||
return (
|
||||
<div>
|
||||
<div>TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}</div>
|
||||
<div>RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const filterOptions: FilterOption[] = [
|
||||
{
|
||||
name: 'Team ID',
|
||||
label: 'Team ID',
|
||||
isSearchable: true,
|
||||
{
|
||||
name: "Team ID",
|
||||
label: "Team ID",
|
||||
isSearchable: true,
|
||||
searchFn: async (searchText: string) => {
|
||||
if (!allTeams || allTeams.length === 0) return [];
|
||||
|
||||
const filteredTeams = allTeams.filter(team =>
|
||||
team.team_id.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase()))
|
||||
);
|
||||
|
||||
return filteredTeams.map(team => ({
|
||||
if (!allTeams || allTeams.length === 0) return []
|
||||
|
||||
const filteredTeams = allTeams.filter(
|
||||
(team) =>
|
||||
team.team_id.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())),
|
||||
)
|
||||
|
||||
return filteredTeams.map((team) => ({
|
||||
label: `${team.team_alias || team.team_id} (${team.team_id})`,
|
||||
value: team.team_id
|
||||
}));
|
||||
}
|
||||
value: team.team_id,
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Organization ID',
|
||||
label: 'Organization ID',
|
||||
isSearchable: true,
|
||||
{
|
||||
name: "Organization ID",
|
||||
label: "Organization ID",
|
||||
isSearchable: true,
|
||||
searchFn: async (searchText: string) => {
|
||||
if (!allOrganizations || allOrganizations.length === 0) return [];
|
||||
|
||||
const filteredOrgs = allOrganizations.filter(org =>
|
||||
org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false
|
||||
);
|
||||
|
||||
if (!allOrganizations || allOrganizations.length === 0) return []
|
||||
|
||||
const filteredOrgs = allOrganizations.filter(
|
||||
(org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false,
|
||||
)
|
||||
|
||||
return filteredOrgs
|
||||
.filter(org => org.organization_id !== null && org.organization_id !== undefined)
|
||||
.map(org => ({
|
||||
label: `${org.organization_id || 'Unknown'} (${org.organization_id})`,
|
||||
value: org.organization_id as string
|
||||
}));
|
||||
}
|
||||
.filter((org) => org.organization_id !== null && org.organization_id !== undefined)
|
||||
.map((org) => ({
|
||||
label: `${org.organization_id || "Unknown"} (${org.organization_id})`,
|
||||
value: org.organization_id as string,
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Key Alias",
|
||||
label: "Key Alias",
|
||||
isSearchable: true,
|
||||
searchFn: async (searchText) => {
|
||||
const filteredKeyAliases = allKeyAliases.filter(key => {
|
||||
const filteredKeyAliases = allKeyAliases.filter((key) => {
|
||||
return key.toLowerCase().includes(searchText.toLowerCase())
|
||||
});
|
||||
})
|
||||
|
||||
return filteredKeyAliases.map((key) => {
|
||||
return {
|
||||
label: key,
|
||||
value: key
|
||||
value: key,
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'User ID',
|
||||
label: 'User ID',
|
||||
name: "User ID",
|
||||
label: "User ID",
|
||||
isSearchable: false,
|
||||
},
|
||||
{
|
||||
name: 'Key Hash',
|
||||
label: 'Key Hash',
|
||||
name: "Key Hash",
|
||||
label: "Key Hash",
|
||||
isSearchable: false,
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
console.log(`keys: ${JSON.stringify(keys)}`)
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredKeys,
|
||||
columns: columns.filter(col => col.id !== 'expander'),
|
||||
columns: columns.filter((col) => col.id !== "expander"),
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: (updaterOrValue) => {
|
||||
const newSorting = typeof updaterOrValue === 'function'
|
||||
? updaterOrValue(sorting)
|
||||
: updaterOrValue;
|
||||
const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue
|
||||
console.log(`newSorting: ${JSON.stringify(newSorting)}`)
|
||||
setSorting(newSorting);
|
||||
setSorting(newSorting)
|
||||
if (newSorting && newSorting.length > 0) {
|
||||
const sortState = newSorting[0];
|
||||
const sortBy = sortState.id;
|
||||
const sortOrder = sortState.desc ? 'desc' : 'asc';
|
||||
const sortState = newSorting[0]
|
||||
const sortBy = sortState.id
|
||||
const sortOrder = sortState.desc ? "desc" : "asc"
|
||||
console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`)
|
||||
handleFilterChange({
|
||||
...filters,
|
||||
'Sort By': sortBy,
|
||||
'Sort Order': sortOrder
|
||||
});
|
||||
onSortChange?.(sortBy, sortOrder);
|
||||
"Sort By": sortBy,
|
||||
"Sort Order": sortOrder,
|
||||
})
|
||||
onSortChange?.(sortBy, sortOrder)
|
||||
}
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableSorting: true,
|
||||
manualSorting: false,
|
||||
});
|
||||
})
|
||||
|
||||
// Update local sorting state when currentSort prop changes
|
||||
React.useEffect(() => {
|
||||
if (currentSort) {
|
||||
setSorting([{
|
||||
id: currentSort.sortBy,
|
||||
desc: currentSort.sortOrder === 'desc'
|
||||
}]);
|
||||
setSorting([
|
||||
{
|
||||
id: currentSort.sortBy,
|
||||
desc: currentSort.sortOrder === "desc",
|
||||
},
|
||||
])
|
||||
}
|
||||
}, [currentSort]);
|
||||
}, [currentSort])
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
{selectedKeyId ? (
|
||||
<KeyInfoView
|
||||
keyId={selectedKeyId}
|
||||
<KeyInfoView
|
||||
keyId={selectedKeyId}
|
||||
onClose={() => setSelectedKeyId(null)}
|
||||
keyData={filteredKeys.find(k => k.token === selectedKeyId)}
|
||||
keyData={filteredKeys.find((k) => k.token === selectedKeyId)}
|
||||
onKeyDataUpdate={(updatedKeyData) => {
|
||||
setKeys(keys => keys.map(key => {
|
||||
if (key.token === updatedKeyData.token) {
|
||||
return updateExistingKeys(key, updatedKeyData)
|
||||
}
|
||||
return key
|
||||
}))
|
||||
if (refresh) refresh(); // Minimal fix: refresh the full key list after an update
|
||||
setKeys((keys) =>
|
||||
keys.map((key) => {
|
||||
if (key.token === updatedKeyData.token) {
|
||||
return updateExistingKeys(key, updatedKeyData)
|
||||
}
|
||||
return key
|
||||
}),
|
||||
)
|
||||
if (refresh) refresh() // Minimal fix: refresh the full key list after an update
|
||||
}}
|
||||
onDelete={() => {
|
||||
setKeys(keys => keys.filter(key => key.token !== selectedKeyId))
|
||||
if (refresh) refresh(); // Minimal fix: refresh the full key list after a delete
|
||||
setKeys((keys) => keys.filter((key) => key.token !== selectedKeyId))
|
||||
if (refresh) refresh() // Minimal fix: refresh the full key list after a delete
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
|
|
@ -632,19 +602,28 @@ export function AllKeysTable({
|
|||
) : (
|
||||
<div className="border-b py-4 flex-1 overflow-hidden">
|
||||
<div className="w-full mb-6">
|
||||
<FilterComponent options={filterOptions} onApplyFilters={handleFilterChange} initialValues={filters} onResetFilters={handleFilterReset}/>
|
||||
<FilterComponent
|
||||
options={filterOptions}
|
||||
onApplyFilters={handleFilterChange}
|
||||
initialValues={filters}
|
||||
onResetFilters={handleFilterReset}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between w-full mb-4">
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
Showing {isLoading ? "..." : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`} of {isLoading ? "..." : pagination.totalCount} results
|
||||
Showing{" "}
|
||||
{isLoading
|
||||
? "..."
|
||||
: `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`}{" "}
|
||||
of {isLoading ? "..." : pagination.totalCount} results
|
||||
</span>
|
||||
|
||||
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages}
|
||||
</span>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(pagination.currentPage - 1)}
|
||||
disabled={isLoading || pagination.currentPage === 1}
|
||||
|
|
@ -652,9 +631,9 @@ export function AllKeysTable({
|
|||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(pagination.currentPage + 1)}
|
||||
onClick={() => onPageChange(pagination.currentPage + 1)}
|
||||
disabled={isLoading || pagination.currentPage === pagination.totalPages}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
|
|
@ -670,30 +649,27 @@ export function AllKeysTable({
|
|||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
className={`py-1 h-8 ${
|
||||
header.id === 'actions'
|
||||
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]'
|
||||
: ''
|
||||
header.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder ? null : (
|
||||
flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)
|
||||
)}
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
{header.id !== 'actions' && (
|
||||
{header.id !== "actions" && (
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
|
|
@ -726,7 +702,7 @@ export function AllKeysTable({
|
|||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${cell.column.id === 'models' && (cell.getValue() as string[]).length > 3 ? "px-0" : ""}`}
|
||||
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${cell.column.id === "models" && (cell.getValue() as string[]).length > 3 ? "px-0" : ""}`}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
|
|
@ -750,5 +726,5 @@ export function AllKeysTable({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3672,7 +3672,7 @@ export const keyUpdateCall = async (
|
|||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
console.error("Error response from the server:", errorData);
|
||||
throw new Error("Network response was not ok");
|
||||
throw new Error(errorData);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log("Update key Response:", data);
|
||||
|
|
|
|||
|
|
@ -1,29 +1,12 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { Button, TextInput, Grid, Col, Select as TremorSelect, SelectItem } from "@tremor/react";
|
||||
import {
|
||||
Card,
|
||||
Metric,
|
||||
Text,
|
||||
Title,
|
||||
Subtitle,
|
||||
Accordion,
|
||||
AccordionHeader,
|
||||
AccordionBody,
|
||||
} from "@tremor/react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Radio,
|
||||
} from "antd";
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
import { unfurlWildcardModelsInList, getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import SchemaFormFields from './common_components/check_openapi_schema';
|
||||
"use client"
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react"
|
||||
import { Button, TextInput, Grid, Col, Select as TremorSelect, SelectItem } from "@tremor/react"
|
||||
import { Card, Metric, Text, Title, Subtitle, Accordion, AccordionHeader, AccordionBody } from "@tremor/react"
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard"
|
||||
import { Button as Button2, Modal, Form, Input, Select, message, Radio } from "antd"
|
||||
import NumericalInput from "../shared/numerical_input"
|
||||
import { unfurlWildcardModelsInList, getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"
|
||||
import SchemaFormFields from "../common_components/check_openapi_schema"
|
||||
import {
|
||||
keyCreateCall,
|
||||
slackBudgetAlertsHealthCheck,
|
||||
|
|
@ -35,22 +18,21 @@ import {
|
|||
keyCreateServiceAccountCall,
|
||||
fetchMCPAccessGroups,
|
||||
getPromptsList,
|
||||
} from "./networking";
|
||||
import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
|
||||
import { Team } from "./key_team_helpers/key_list";
|
||||
import TeamDropdown from "./common_components/team_dropdown";
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import PremiumLoggingSettings from "./common_components/PremiumLoggingSettings";
|
||||
import Createuser from "./create_user_button";
|
||||
import debounce from 'lodash/debounce';
|
||||
import { rolesWithWriteAccess } from '../utils/roles';
|
||||
import BudgetDurationDropdown from "./common_components/budget_duration_dropdown";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { callback_map, mapDisplayToInternalNames } from "./callback_info_helpers";
|
||||
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
|
||||
import ModelAliasManager from "./common_components/ModelAliasManager";
|
||||
|
||||
} from "../networking"
|
||||
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"
|
||||
import { Team } from "../key_team_helpers/key_list"
|
||||
import TeamDropdown from "../common_components/team_dropdown"
|
||||
import { InfoCircleOutlined } from "@ant-design/icons"
|
||||
import { Tooltip } from "antd"
|
||||
import PremiumLoggingSettings from "../common_components/PremiumLoggingSettings"
|
||||
import Createuser from "../create_user_button"
|
||||
import debounce from "lodash/debounce"
|
||||
import { rolesWithWriteAccess } from "../../utils/roles"
|
||||
import BudgetDurationDropdown from "../common_components/budget_duration_dropdown"
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils"
|
||||
import { callback_map, mapDisplayToInternalNames } from "../callback_info_helpers"
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector"
|
||||
import ModelAliasManager from "../common_components/ModelAliasManager"
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
|
|
@ -1201,4 +1183,4 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default CreateKey;
|
||||
export default CreateKey;
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Text, TextInput, Title, Grid, Col } from "@tremor/react";
|
||||
import { Modal, Form, InputNumber, message } from "antd";
|
||||
import { add } from "date-fns";
|
||||
import { regenerateKeyCall } from "./networking";
|
||||
import { KeyResponse } from "./key_team_helpers/key_list";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { Button, Text, TextInput, Title, Grid, Col } from "@tremor/react"
|
||||
import { Modal, Form, InputNumber, message } from "antd"
|
||||
import { add } from "date-fns"
|
||||
import { regenerateKeyCall } from "../networking"
|
||||
import { KeyResponse } from "../key_team_helpers/key_list"
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard"
|
||||
import NotificationManager from "../molecules/notifications_manager"
|
||||
|
||||
interface RegenerateKeyModalProps {
|
||||
selectedToken: KeyResponse | null
|
||||
|
|
@ -70,45 +71,49 @@ export function RegenerateKeyModal({
|
|||
const calculateNewExpiryTime = (duration: string | undefined): string | null => {
|
||||
if (!duration) return null
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
let newExpiry: Date;
|
||||
try {
|
||||
const now = new Date()
|
||||
let newExpiry: Date
|
||||
|
||||
if (duration.endsWith("s")) {
|
||||
newExpiry = add(now, { seconds: parseInt(duration) });
|
||||
} else if (duration.endsWith("h")) {
|
||||
newExpiry = add(now, { hours: parseInt(duration) });
|
||||
} else if (duration.endsWith("d")) {
|
||||
newExpiry = add(now, { days: parseInt(duration) });
|
||||
} else {
|
||||
throw new Error("Invalid duration format");
|
||||
}
|
||||
|
||||
return newExpiry.toLocaleString();
|
||||
} catch (error) {
|
||||
return null;
|
||||
if (duration.endsWith("s")) {
|
||||
newExpiry = add(now, { seconds: parseInt(duration) })
|
||||
} else if (duration.endsWith("h")) {
|
||||
newExpiry = add(now, { hours: parseInt(duration) })
|
||||
} else if (duration.endsWith("d")) {
|
||||
newExpiry = add(now, { days: parseInt(duration) })
|
||||
} else {
|
||||
throw new Error("Invalid duration format")
|
||||
}
|
||||
};
|
||||
|
||||
return newExpiry.toLocaleString()
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (regenerateFormData?.duration) {
|
||||
setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration));
|
||||
setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration))
|
||||
} else {
|
||||
setNewExpiryTime(null);
|
||||
setNewExpiryTime(null)
|
||||
}
|
||||
}, [regenerateFormData?.duration]);
|
||||
}, [regenerateFormData?.duration])
|
||||
|
||||
const handleRegenerateKey = async () => {
|
||||
if (!selectedToken || !currentAccessToken) return
|
||||
|
||||
setIsRegenerating(true);
|
||||
setIsRegenerating(true)
|
||||
try {
|
||||
const formValues = await form.validateFields()
|
||||
|
||||
// Use the current access token for the API call
|
||||
const response = await regenerateKeyCall(currentAccessToken, selectedToken.token || selectedToken.token_id, formValues)
|
||||
const response = await regenerateKeyCall(
|
||||
currentAccessToken,
|
||||
selectedToken.token || selectedToken.token_id,
|
||||
formValues,
|
||||
)
|
||||
setRegeneratedKey(response.key)
|
||||
message.success("API Key regenerated successfully")
|
||||
NotificationManager.success("API Key regenerated successfully")
|
||||
|
||||
console.log("Full regenerate response:", response) // Debug log to see what's returned
|
||||
|
||||
|
|
@ -142,11 +147,11 @@ export function RegenerateKeyModal({
|
|||
|
||||
setIsRegenerating(false)
|
||||
} catch (error) {
|
||||
console.error("Error regenerating key:", error);
|
||||
message.error("Failed to regenerate API Key");
|
||||
setIsRegenerating(false); // Reset regenerating state on error
|
||||
console.error("Error regenerating key:", error)
|
||||
NotificationManager.fromBackend(error)
|
||||
setIsRegenerating(false) // Reset regenerating state on error
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setRegeneratedKey(null)
|
||||
|
|
@ -162,49 +167,43 @@ export function RegenerateKeyModal({
|
|||
title="Regenerate API Key"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={regeneratedKey ? [
|
||||
<Button key="close" onClick={handleClose}>
|
||||
Close
|
||||
</Button>,
|
||||
] : [
|
||||
<Button key="cancel" onClick={handleClose} className="mr-2">
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="regenerate"
|
||||
onClick={handleRegenerateKey}
|
||||
disabled={isRegenerating}
|
||||
>
|
||||
{isRegenerating ? "Regenerating..." : "Regenerate"}
|
||||
</Button>,
|
||||
]}
|
||||
footer={
|
||||
regeneratedKey
|
||||
? [
|
||||
<Button key="close" onClick={handleClose}>
|
||||
Close
|
||||
</Button>,
|
||||
]
|
||||
: [
|
||||
<Button key="cancel" onClick={handleClose} className="mr-2">
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button key="regenerate" onClick={handleRegenerateKey} disabled={isRegenerating}>
|
||||
{isRegenerating ? "Regenerating..." : "Regenerate"}
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
>
|
||||
{regeneratedKey ? (
|
||||
<Grid numItems={1} className="gap-2 w-full">
|
||||
<Title>Regenerated Key</Title>
|
||||
<Col numColSpan={1}>
|
||||
<p>
|
||||
Please replace your old key with the new key generated. For
|
||||
security reasons, <b>you will not be able to view it again</b>{" "}
|
||||
through your LiteLLM account. If you lose this secret key, you
|
||||
will need to generate a new one.
|
||||
Please replace your old key with the new key generated. For security reasons,{" "}
|
||||
<b>you will not be able to view it again</b> through your LiteLLM account. If you lose this secret key,
|
||||
you will need to generate a new one.
|
||||
</p>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Text className="mt-3">Key Alias:</Text>
|
||||
<div className="bg-gray-100 p-2 rounded mb-2">
|
||||
<pre className="break-words whitespace-normal">
|
||||
{selectedToken?.key_alias || "No alias set"}
|
||||
</pre>
|
||||
<pre className="break-words whitespace-normal">{selectedToken?.key_alias || "No alias set"}</pre>
|
||||
</div>
|
||||
<Text className="mt-3">New API Key:</Text>
|
||||
<div className="bg-gray-100 p-2 rounded mb-2">
|
||||
<pre className="break-words whitespace-normal">{regeneratedKey}</pre>
|
||||
</div>
|
||||
<CopyToClipboard
|
||||
text={regeneratedKey}
|
||||
onCopy={() => message.success("API Key copied to clipboard")}
|
||||
>
|
||||
<CopyToClipboard text={regeneratedKey} onCopy={() => NotificationManager.success("API Key copied to clipboard")}>
|
||||
<Button className="mt-3">Copy API Key</Button>
|
||||
</CopyToClipboard>
|
||||
</Col>
|
||||
|
|
@ -215,7 +214,7 @@ export function RegenerateKeyModal({
|
|||
layout="vertical"
|
||||
onValuesChange={(changedValues) => {
|
||||
if ("duration" in changedValues) {
|
||||
setRegenerateFormData((prev: { duration?: string }) => ({ ...prev, duration: changedValues.duration }));
|
||||
setRegenerateFormData((prev: { duration?: string }) => ({ ...prev, duration: changedValues.duration }))
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -237,13 +236,9 @@ export function RegenerateKeyModal({
|
|||
<div className="mt-2 text-sm text-gray-500">
|
||||
Current expiry: {selectedToken?.expires ? new Date(selectedToken.expires).toLocaleString() : "Never"}
|
||||
</div>
|
||||
{newExpiryTime && (
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
New expiry: {newExpiryTime}
|
||||
</div>
|
||||
)}
|
||||
{newExpiryTime && <div className="mt-2 text-sm text-green-600">New expiry: {newExpiryTime}</div>}
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,149 +1,116 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
Title,
|
||||
Button,
|
||||
Badge,
|
||||
} from "@tremor/react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Select as Select2,
|
||||
message,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { fetchUserModels } from "../create_key_button";
|
||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import { tagInfoCall, tagUpdateCall } from "../networking";
|
||||
import { Tag, TagInfoResponse } from "./types";
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { Card, Text, Title, Button, Badge } from "@tremor/react"
|
||||
import { Form, Input, Select as Select2, message, Tooltip } from "antd"
|
||||
import { InfoCircleOutlined } from "@ant-design/icons"
|
||||
import { fetchUserModels } from "../organisms/create_key_button"
|
||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"
|
||||
import { tagInfoCall, tagUpdateCall } from "../networking"
|
||||
import { Tag, TagInfoResponse } from "./types"
|
||||
|
||||
interface TagInfoViewProps {
|
||||
tagId: string;
|
||||
onClose: () => void;
|
||||
accessToken: string | null;
|
||||
is_admin: boolean;
|
||||
editTag: boolean;
|
||||
tagId: string
|
||||
onClose: () => void
|
||||
accessToken: string | null
|
||||
is_admin: boolean
|
||||
editTag: boolean
|
||||
}
|
||||
|
||||
const TagInfoView: React.FC<TagInfoViewProps> = ({
|
||||
tagId,
|
||||
onClose,
|
||||
accessToken,
|
||||
is_admin,
|
||||
editTag,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [tagDetails, setTagDetails] = useState<Tag | null>(null);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(editTag);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const TagInfoView: React.FC<TagInfoViewProps> = ({ tagId, onClose, accessToken, is_admin, editTag }) => {
|
||||
const [form] = Form.useForm()
|
||||
const [tagDetails, setTagDetails] = useState<Tag | null>(null)
|
||||
const [isEditing, setIsEditing] = useState<boolean>(editTag)
|
||||
const [userModels, setUserModels] = useState<string[]>([])
|
||||
|
||||
const fetchTagDetails = async () => {
|
||||
if (!accessToken) return;
|
||||
if (!accessToken) return
|
||||
try {
|
||||
const response = await tagInfoCall(accessToken, [tagId]);
|
||||
const tagData = response[tagId];
|
||||
const response = await tagInfoCall(accessToken, [tagId])
|
||||
const tagData = response[tagId]
|
||||
if (tagData) {
|
||||
setTagDetails(tagData);
|
||||
setTagDetails(tagData)
|
||||
if (editTag) {
|
||||
form.setFieldsValue({
|
||||
name: tagData.name,
|
||||
description: tagData.description,
|
||||
models: tagData.models,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching tag details:", error);
|
||||
message.error("Error fetching tag details: " + error);
|
||||
console.error("Error fetching tag details:", error)
|
||||
message.error("Error fetching tag details: " + error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchTagDetails();
|
||||
}, [tagId, accessToken]);
|
||||
fetchTagDetails()
|
||||
}, [tagId, accessToken])
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
// Using dummy values for userID and userRole since they're required by the function
|
||||
// TODO: Pass these as props if needed for the actual API implementation
|
||||
fetchUserModels("dummy-user", "Admin", accessToken, setUserModels);
|
||||
fetchUserModels("dummy-user", "Admin", accessToken, setUserModels)
|
||||
}
|
||||
}, [accessToken]);
|
||||
}, [accessToken])
|
||||
|
||||
const handleSave = async (values: any) => {
|
||||
if (!accessToken) return;
|
||||
if (!accessToken) return
|
||||
try {
|
||||
await tagUpdateCall(accessToken, {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
models: values.models,
|
||||
});
|
||||
message.success("Tag updated successfully");
|
||||
setIsEditing(false);
|
||||
fetchTagDetails();
|
||||
})
|
||||
message.success("Tag updated successfully")
|
||||
setIsEditing(false)
|
||||
fetchTagDetails()
|
||||
} catch (error) {
|
||||
console.error("Error updating tag:", error);
|
||||
message.error("Error updating tag: " + error);
|
||||
console.error("Error updating tag:", error)
|
||||
message.error("Error updating tag: " + error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!tagDetails) {
|
||||
return <div>Loading...</div>;
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button onClick={onClose} className="mb-4">← Back to Tags</Button>
|
||||
<Button onClick={onClose} className="mb-4">
|
||||
← Back to Tags
|
||||
</Button>
|
||||
<Title>Tag Name: {tagDetails.name}</Title>
|
||||
<Text className="text-gray-500">{tagDetails.description || "No description"}</Text>
|
||||
</div>
|
||||
{is_admin && !isEditing && (
|
||||
<Button onClick={() => setIsEditing(true)}>Edit Tag</Button>
|
||||
)}
|
||||
{is_admin && !isEditing && <Button onClick={() => setIsEditing(true)}>Edit Tag</Button>}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<Card>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleSave}
|
||||
layout="vertical"
|
||||
initialValues={tagDetails}
|
||||
>
|
||||
<Form.Item
|
||||
label="Tag Name"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "Please input a tag name" }]}
|
||||
>
|
||||
<Form form={form} onFinish={handleSave} layout="vertical" initialValues={tagDetails}>
|
||||
<Form.Item label="Tag Name" name="name" rules={[{ required: true, message: "Please input a tag name" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Description"
|
||||
name="description"
|
||||
>
|
||||
<Form.Item label="Description" name="description">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed LLMs{' '}
|
||||
Allowed LLMs{" "}
|
||||
<Tooltip title="Select which LLMs are allowed to process this type of data">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="models"
|
||||
>
|
||||
<Select2
|
||||
mode="multiple"
|
||||
placeholder="Select LLMs"
|
||||
>
|
||||
<Select2 mode="multiple" placeholder="Select LLMs">
|
||||
{userModels.map((modelId) => (
|
||||
<Select2.Option key={modelId} value={modelId}>
|
||||
{getModelDisplayName(modelId)}
|
||||
|
|
@ -179,9 +146,7 @@ const TagInfoView: React.FC<TagInfoViewProps> = ({
|
|||
) : (
|
||||
tagDetails.models.map((modelId) => (
|
||||
<Badge key={modelId} color="blue">
|
||||
<Tooltip title={`ID: ${modelId}`}>
|
||||
{tagDetails.model_info?.[modelId] || modelId}
|
||||
</Tooltip>
|
||||
<Tooltip title={`ID: ${modelId}`}>{tagDetails.model_info?.[modelId] || modelId}</Tooltip>
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
|
|
@ -200,7 +165,7 @@ const TagInfoView: React.FC<TagInfoViewProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default TagInfoView;
|
||||
export default TagInfoView
|
||||
|
|
|
|||
|
|
@ -1,137 +1,131 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Input, Select, Button as AntdButton, Tooltip } from "antd";
|
||||
import { Button as TremorButton, TextInput } from "@tremor/react";
|
||||
import { KeyResponse } from "./key_team_helpers/key_list";
|
||||
import { fetchTeamModels } from "../components/create_key_button";
|
||||
import { modelAvailableCall, getPromptsList } from "./networking";
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
|
||||
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
|
||||
import EditLoggingSettings from "./team/EditLoggingSettings";
|
||||
import { extractLoggingSettings, formatMetadataForDisplay } from "./key_info_utils";
|
||||
import { fetchMCPAccessGroups } from "./networking";
|
||||
import { mapInternalToDisplayNames, mapDisplayToInternalNames } from "./callback_info_helpers";
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { Form, Input, Select, Button as AntdButton, Tooltip } from "antd"
|
||||
import { Button as TremorButton, TextInput } from "@tremor/react"
|
||||
import { KeyResponse } from "../key_team_helpers/key_list"
|
||||
import { fetchTeamModels } from "../organisms/create_key_button"
|
||||
import { modelAvailableCall, getPromptsList } from "../networking"
|
||||
import NumericalInput from "../shared/numerical_input"
|
||||
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector"
|
||||
import EditLoggingSettings from "../team/EditLoggingSettings"
|
||||
import { extractLoggingSettings, formatMetadataForDisplay } from "../key_info_utils"
|
||||
import { fetchMCPAccessGroups } from "../networking"
|
||||
import { mapInternalToDisplayNames, mapDisplayToInternalNames } from "../callback_info_helpers"
|
||||
|
||||
interface KeyEditViewProps {
|
||||
keyData: KeyResponse;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: any) => Promise<void>;
|
||||
teams?: any[] | null;
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
premiumUser?: boolean;
|
||||
keyData: KeyResponse
|
||||
onCancel: () => void
|
||||
onSubmit: (values: any) => Promise<void>
|
||||
teams?: any[] | null
|
||||
accessToken: string | null
|
||||
userID: string | null
|
||||
userRole: string | null
|
||||
premiumUser?: boolean
|
||||
}
|
||||
|
||||
// Add this helper function
|
||||
const getAvailableModelsForKey = (keyData: KeyResponse, teams: any[] | null): string[] => {
|
||||
// If no teams data is available, return empty array
|
||||
console.log("getAvailableModelsForKey:", teams);
|
||||
console.log("getAvailableModelsForKey:", teams)
|
||||
if (!teams || !keyData.team_id) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
// Find the team that matches the key's team_id
|
||||
const keyTeam = teams.find(team => team.team_id === keyData.team_id);
|
||||
|
||||
const keyTeam = teams.find((team) => team.team_id === keyData.team_id)
|
||||
|
||||
// If team found and has models, return those models
|
||||
if (keyTeam?.models) {
|
||||
return keyTeam.models;
|
||||
return keyTeam.models
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
return []
|
||||
}
|
||||
|
||||
export function KeyEditView({
|
||||
keyData,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
teams,
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
premiumUser = false
|
||||
export function KeyEditView({
|
||||
keyData,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
teams,
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
premiumUser = false,
|
||||
}: KeyEditViewProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const [promptsList, setPromptsList] = useState<string[]>([]);
|
||||
const team = teams?.find(team => team.team_id === keyData.team_id);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
|
||||
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
|
||||
const [form] = Form.useForm()
|
||||
const [userModels, setUserModels] = useState<string[]>([])
|
||||
const [promptsList, setPromptsList] = useState<string[]>([])
|
||||
const team = teams?.find((team) => team.team_id === keyData.team_id)
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([])
|
||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([])
|
||||
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false)
|
||||
const [disabledCallbacks, setDisabledCallbacks] = useState<string[]>(
|
||||
Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
|
||||
Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
|
||||
: []
|
||||
);
|
||||
: [],
|
||||
)
|
||||
|
||||
const fetchMcpAccessGroups = async () => {
|
||||
if (!accessToken) return;
|
||||
if (mcpAccessGroupsLoaded) return;
|
||||
if (!accessToken) return
|
||||
if (mcpAccessGroupsLoaded) return
|
||||
try {
|
||||
const groups = await fetchMCPAccessGroups(accessToken);
|
||||
setMcpAccessGroups(groups);
|
||||
setMcpAccessGroupsLoaded(true);
|
||||
const groups = await fetchMCPAccessGroups(accessToken)
|
||||
setMcpAccessGroups(groups)
|
||||
setMcpAccessGroupsLoaded(true)
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch MCP access groups:", error);
|
||||
console.error("Failed to fetch MCP access groups:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchModels = async () => {
|
||||
if (!userID || !userRole || !accessToken) return;
|
||||
if (!userID || !userRole || !accessToken) return
|
||||
|
||||
try {
|
||||
if (keyData.team_id === null) {
|
||||
// Fetch user models if no team
|
||||
const model_available = await modelAvailableCall(
|
||||
accessToken,
|
||||
userID,
|
||||
userRole
|
||||
);
|
||||
const available_model_names = model_available["data"].map(
|
||||
(element: { id: string }) => element.id
|
||||
);
|
||||
setAvailableModels(available_model_names);
|
||||
const model_available = await modelAvailableCall(accessToken, userID, userRole)
|
||||
const available_model_names = model_available["data"].map((element: { id: string }) => element.id)
|
||||
setAvailableModels(available_model_names)
|
||||
} else if (team?.team_id) {
|
||||
// Fetch team models if team exists
|
||||
const models = await fetchTeamModels(userID, userRole, accessToken, team.team_id);
|
||||
setAvailableModels(Array.from(new Set([...team.models, ...models])));
|
||||
const models = await fetchTeamModels(userID, userRole, accessToken, team.team_id)
|
||||
setAvailableModels(Array.from(new Set([...team.models, ...models])))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching models:", error);
|
||||
console.error("Error fetching models:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const fetchPrompts = async () => {
|
||||
if (!accessToken) return;
|
||||
if (!accessToken) return
|
||||
try {
|
||||
const response = await getPromptsList(accessToken);
|
||||
setPromptsList(response.prompts.map(prompt => prompt.prompt_id));
|
||||
const response = await getPromptsList(accessToken)
|
||||
setPromptsList(response.prompts.map((prompt) => prompt.prompt_id))
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch prompts:", error);
|
||||
console.error("Failed to fetch prompts:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fetchPrompts();
|
||||
fetchModels();
|
||||
}, [userID, userRole, accessToken, team, keyData.team_id]);
|
||||
fetchPrompts()
|
||||
fetchModels()
|
||||
}, [userID, userRole, accessToken, team, keyData.team_id])
|
||||
|
||||
// Sync disabled callbacks with form when component mounts
|
||||
useEffect(() => {
|
||||
form.setFieldValue('disabled_callbacks', disabledCallbacks);
|
||||
}, [form, disabledCallbacks]);
|
||||
form.setFieldValue("disabled_callbacks", disabledCallbacks)
|
||||
}, [form, disabledCallbacks])
|
||||
|
||||
// Convert API budget duration to form format
|
||||
const getBudgetDuration = (duration: string | null) => {
|
||||
if (!duration) return null;
|
||||
if (!duration) return null
|
||||
const durationMap: Record<string, string> = {
|
||||
"24h": "daily",
|
||||
"7d": "weekly",
|
||||
"30d": "monthly"
|
||||
};
|
||||
return durationMap[duration] || null;
|
||||
};
|
||||
"30d": "monthly",
|
||||
}
|
||||
return durationMap[duration] || null
|
||||
}
|
||||
|
||||
// Set initial form values
|
||||
const initialValues = {
|
||||
|
|
@ -143,39 +137,28 @@ export function KeyEditView({
|
|||
vector_stores: keyData.object_permission?.vector_stores || [],
|
||||
mcp_servers_and_groups: {
|
||||
servers: keyData.object_permission?.mcp_servers || [],
|
||||
accessGroups: keyData.object_permission?.mcp_access_groups || []
|
||||
accessGroups: keyData.object_permission?.mcp_access_groups || [],
|
||||
},
|
||||
logging_settings: extractLoggingSettings(keyData.metadata),
|
||||
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
|
||||
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
|
||||
: []
|
||||
};
|
||||
: [],
|
||||
}
|
||||
|
||||
console.log("premiumUser:", premiumUser);
|
||||
console.log("premiumUser:", premiumUser)
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onSubmit}
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form form={form} onFinish={onSubmit} initialValues={initialValues} layout="vertical">
|
||||
<Form.Item label="Key Alias" name="key_alias">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Models" name="models">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select models"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Select mode="multiple" placeholder="Select models" style={{ width: "100%" }}>
|
||||
{/* Only show All Team Models if team has models */}
|
||||
{availableModels.length > 0 && (
|
||||
<Select.Option value="all-team-models">All Team Models</Select.Option>
|
||||
)}
|
||||
{availableModels.length > 0 && <Select.Option value="all-team-models">All Team Models</Select.Option>}
|
||||
{/* Show available team models */}
|
||||
{availableModels.map(model => (
|
||||
{availableModels.map((model) => (
|
||||
<Select.Option key={model} value={model}>
|
||||
{model}
|
||||
</Select.Option>
|
||||
|
|
@ -184,7 +167,7 @@ export function KeyEditView({
|
|||
</Form.Item>
|
||||
|
||||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||
<NumericalInput step={0.01} style={{ width: "100%" }} placeholder="Enter a numerical value"/>
|
||||
<NumericalInput step={0.01} style={{ width: "100%" }} placeholder="Enter a numerical value" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Reset Budget" name="budget_duration">
|
||||
|
|
@ -196,31 +179,27 @@ export function KeyEditView({
|
|||
</Form.Item>
|
||||
|
||||
<Form.Item label="TPM Limit" name="tpm_limit">
|
||||
<NumericalInput min={0}/>
|
||||
<NumericalInput min={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="RPM Limit" name="rpm_limit">
|
||||
<NumericalInput min={0}/>
|
||||
<NumericalInput min={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Max Parallel Requests" name="max_parallel_requests">
|
||||
<NumericalInput min={0}/>
|
||||
<Form.Item label="Max Parallel Requests" name="max_parallel_requests">
|
||||
<NumericalInput min={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Model TPM Limit" name="model_tpm_limit">
|
||||
<Input.TextArea rows={4} placeholder='{"gpt-4": 100, "claude-v1": 200}'/>
|
||||
<Input.TextArea rows={4} placeholder='{"gpt-4": 100, "claude-v1": 200}' />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Model RPM Limit" name="model_rpm_limit">
|
||||
<Input.TextArea rows={4} placeholder='{"gpt-4": 100, "claude-v1": 200}'/>
|
||||
<Input.TextArea rows={4} placeholder='{"gpt-4": 100, "claude-v1": 200}' />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item label="Guardrails" name="guardrails">
|
||||
<Tooltip
|
||||
title={!premiumUser ? "Setting guardrails by key is a premium feature" : ""}
|
||||
placement="top"
|
||||
>
|
||||
<Tooltip title={!premiumUser ? "Setting guardrails by key is a premium feature" : ""} placement="top">
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
|
|
@ -229,7 +208,7 @@ export function KeyEditView({
|
|||
!premiumUser
|
||||
? "Premium feature - Upgrade to set guardrails by key"
|
||||
: Array.isArray(keyData.metadata?.guardrails) && keyData.metadata.guardrails.length > 0
|
||||
? `Current: ${keyData.metadata.guardrails.join(', ')}`
|
||||
? `Current: ${keyData.metadata.guardrails.join(", ")}`
|
||||
: "Select or enter guardrails"
|
||||
}
|
||||
/>
|
||||
|
|
@ -237,10 +216,7 @@ export function KeyEditView({
|
|||
</Form.Item>
|
||||
|
||||
<Form.Item label="Prompts" name="prompts">
|
||||
<Tooltip
|
||||
title={!premiumUser ? "Setting prompts by key is a premium feature" : ""}
|
||||
placement="top"
|
||||
>
|
||||
<Tooltip title={!premiumUser ? "Setting prompts by key is a premium feature" : ""} placement="top">
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
|
|
@ -249,18 +225,18 @@ export function KeyEditView({
|
|||
!premiumUser
|
||||
? "Premium feature - Upgrade to set prompts by key"
|
||||
: Array.isArray(keyData.metadata?.prompts) && keyData.metadata.prompts.length > 0
|
||||
? `Current: ${keyData.metadata.prompts.join(', ')}`
|
||||
? `Current: ${keyData.metadata.prompts.join(", ")}`
|
||||
: "Select or enter prompts"
|
||||
}
|
||||
options={promptsList.map(name => ({ value: name, label: name }))}
|
||||
options={promptsList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Vector Stores" name="vector_stores">
|
||||
<VectorStoreSelector
|
||||
onChange={(values: string[]) => form.setFieldValue('vector_stores', values)}
|
||||
value={form.getFieldValue('vector_stores')}
|
||||
onChange={(values: string[]) => form.setFieldValue("vector_stores", values)}
|
||||
value={form.getFieldValue("vector_stores")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select vector stores"
|
||||
/>
|
||||
|
|
@ -268,20 +244,17 @@ export function KeyEditView({
|
|||
|
||||
<Form.Item label="MCP Servers / Access Groups" name="mcp_servers_and_groups">
|
||||
<MCPServerSelector
|
||||
onChange={val => form.setFieldValue('mcp_servers_and_groups', val)}
|
||||
value={form.getFieldValue('mcp_servers_and_groups')}
|
||||
accessToken={accessToken || ''}
|
||||
onChange={(val) => form.setFieldValue("mcp_servers_and_groups", val)}
|
||||
value={form.getFieldValue("mcp_servers_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select MCP servers or access groups (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Team ID" name="team_id">
|
||||
<Select
|
||||
placeholder="Select team"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Select placeholder="Select team" style={{ width: "100%" }}>
|
||||
{/* Only show All Team Models if team has models */}
|
||||
{teams?.map(team => (
|
||||
{teams?.map((team) => (
|
||||
<Select.Option key={team.team_id} value={team.team_id}>
|
||||
{`${team.team_alias} (${team.team_id})`}
|
||||
</Select.Option>
|
||||
|
|
@ -290,25 +263,23 @@ export function KeyEditView({
|
|||
</Form.Item>
|
||||
<Form.Item label="Logging Settings" name="logging_settings">
|
||||
<EditLoggingSettings
|
||||
value={form.getFieldValue('logging_settings')}
|
||||
onChange={(values) => form.setFieldValue('logging_settings', values)}
|
||||
value={form.getFieldValue("logging_settings")}
|
||||
onChange={(values) => form.setFieldValue("logging_settings", values)}
|
||||
disabledCallbacks={disabledCallbacks}
|
||||
onDisabledCallbacksChange={(internalValues) => {
|
||||
// Convert internal values back to display names for UI state
|
||||
const displayNames = mapInternalToDisplayNames(internalValues);
|
||||
setDisabledCallbacks(displayNames);
|
||||
const displayNames = mapInternalToDisplayNames(internalValues)
|
||||
setDisabledCallbacks(displayNames)
|
||||
// Store internal values in form for submission
|
||||
form.setFieldValue('disabled_callbacks', internalValues);
|
||||
form.setFieldValue("disabled_callbacks", internalValues)
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item label="Metadata" name="metadata">
|
||||
<Input.TextArea rows={10} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
{/* Hidden form field for token */}
|
||||
<Form.Item name="token" hidden>
|
||||
<Input />
|
||||
|
|
@ -321,14 +292,10 @@ export function KeyEditView({
|
|||
|
||||
<div className="sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]">
|
||||
<div className="flex justify-end items-center gap-2">
|
||||
<AntdButton onClick={onCancel}>
|
||||
Cancel
|
||||
</AntdButton>
|
||||
<TremorButton type="submit">
|
||||
Save Changes
|
||||
</TremorButton>
|
||||
<AntdButton onClick={onCancel}>Cancel</AntdButton>
|
||||
<TremorButton type="submit">Save Changes</TremorButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react"
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
|
|
@ -13,21 +13,23 @@ import {
|
|||
Title,
|
||||
Badge,
|
||||
TextInput,
|
||||
Select as TremorSelect
|
||||
} from "@tremor/react";
|
||||
import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||
import { keyDeleteCall, keyUpdateCall } from "./networking";
|
||||
import { KeyResponse } from "./key_team_helpers/key_list";
|
||||
import { Form, Input, InputNumber, message, Select, Tooltip, Button as AntdButton } from "antd";
|
||||
import { KeyEditView } from "./key_edit_view";
|
||||
import { RegenerateKeyModal } from "./regenerate_key_modal";
|
||||
import { rolesWithWriteAccess } from '../utils/roles';
|
||||
import ObjectPermissionsView from "./object_permissions_view";
|
||||
import LoggingSettingsView from "./logging_settings_view";
|
||||
import { copyToClipboard as utilCopyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { extractLoggingSettings, formatMetadataForDisplay } from "./key_info_utils";
|
||||
import { CopyIcon, CheckIcon } from "lucide-react";
|
||||
import { callback_map, mapInternalToDisplayNames, mapDisplayToInternalNames } from "./callback_info_helpers";
|
||||
Select as TremorSelect,
|
||||
} from "@tremor/react"
|
||||
import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline"
|
||||
import { keyDeleteCall, keyUpdateCall } from "../networking"
|
||||
import { KeyResponse } from "../key_team_helpers/key_list"
|
||||
import { Form, Input, InputNumber, Select, Tooltip, Button as AntdButton } from "antd"
|
||||
import NotificationManager from "../molecules/notifications_manager"
|
||||
import { KeyEditView } from "./key_edit_view"
|
||||
import { RegenerateKeyModal } from "../organisms/regenerate_key_modal"
|
||||
import { rolesWithWriteAccess } from "../../utils/roles"
|
||||
import ObjectPermissionsView from "../object_permissions_view"
|
||||
import LoggingSettingsView from "../logging_settings_view"
|
||||
import { copyToClipboard as utilCopyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils"
|
||||
import { extractLoggingSettings, formatMetadataForDisplay } from "../key_info_utils"
|
||||
import { CopyIcon, CheckIcon } from "lucide-react"
|
||||
import { callback_map, mapInternalToDisplayNames, mapDisplayToInternalNames } from "../callback_info_helpers"
|
||||
import { parseErrorMessage } from "../shared/errorUtils"
|
||||
|
||||
interface KeyInfoViewProps {
|
||||
keyId: string
|
||||
|
|
@ -88,97 +90,84 @@ export default function KeyInfoView({
|
|||
if (!currentKeyData) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Button
|
||||
icon={ArrowLeftIcon}
|
||||
variant="light"
|
||||
onClick={onClose}
|
||||
className="mb-4"
|
||||
>
|
||||
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
Back to Keys
|
||||
</Button>
|
||||
<Text>Key not found</Text>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const handleKeyUpdate = async (formValues: Record<string, any>) => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
if (!accessToken) return
|
||||
|
||||
const currentKey = formValues.token;
|
||||
formValues.key = currentKey;
|
||||
const currentKey = formValues.token
|
||||
formValues.key = currentKey
|
||||
|
||||
// Handle object_permission updates
|
||||
if (formValues.vector_stores !== undefined) {
|
||||
formValues.object_permission = {
|
||||
...currentKeyData.object_permission,
|
||||
vector_stores: formValues.vector_stores || []
|
||||
};
|
||||
vector_stores: formValues.vector_stores || [],
|
||||
}
|
||||
// Remove vector_stores from the top level as it should be in object_permission
|
||||
delete formValues.vector_stores;
|
||||
delete formValues.vector_stores
|
||||
}
|
||||
|
||||
if (formValues.mcp_servers_and_groups !== undefined) {
|
||||
const { servers, accessGroups } = formValues.mcp_servers_and_groups || { servers: [], accessGroups: [] };
|
||||
const { servers, accessGroups } = formValues.mcp_servers_and_groups || { servers: [], accessGroups: [] }
|
||||
formValues.object_permission = {
|
||||
...currentKeyData.object_permission,
|
||||
mcp_servers: servers || [],
|
||||
mcp_access_groups: accessGroups || []
|
||||
};
|
||||
mcp_access_groups: accessGroups || [],
|
||||
}
|
||||
// Remove mcp_servers_and_groups from the top level as it should be in object_permission
|
||||
delete formValues.mcp_servers_and_groups;
|
||||
delete formValues.mcp_servers_and_groups
|
||||
}
|
||||
|
||||
// Convert metadata back to an object if it exists and is a string
|
||||
if (formValues.metadata && typeof formValues.metadata === "string") {
|
||||
try {
|
||||
const parsedMetadata = JSON.parse(formValues.metadata);
|
||||
const parsedMetadata = JSON.parse(formValues.metadata)
|
||||
formValues.metadata = {
|
||||
...parsedMetadata,
|
||||
...(formValues.guardrails?.length > 0
|
||||
? { guardrails: formValues.guardrails }
|
||||
: {}),
|
||||
...(formValues.logging_settings
|
||||
? { logging: formValues.logging_settings }
|
||||
: {}),
|
||||
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}),
|
||||
...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}),
|
||||
...(formValues.disabled_callbacks?.length > 0
|
||||
? {
|
||||
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks)
|
||||
? {
|
||||
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing metadata JSON:", error);
|
||||
message.error("Invalid metadata JSON");
|
||||
return;
|
||||
console.error("Error parsing metadata JSON:", error)
|
||||
NotificationManager.error("Invalid metadata JSON")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
formValues.metadata = {
|
||||
...(formValues.metadata || {}),
|
||||
...(formValues.guardrails?.length > 0
|
||||
? { guardrails: formValues.guardrails }
|
||||
: {}),
|
||||
...(formValues.logging_settings
|
||||
? { logging: formValues.logging_settings }
|
||||
: {}),
|
||||
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}),
|
||||
...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}),
|
||||
...(formValues.disabled_callbacks?.length > 0
|
||||
? {
|
||||
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks)
|
||||
? {
|
||||
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
delete formValues.logging_settings;
|
||||
delete formValues.logging_settings
|
||||
|
||||
// Convert budget_duration to API format
|
||||
if (formValues.budget_duration) {
|
||||
const durationMap: Record<string, string> = {
|
||||
daily: "24h",
|
||||
weekly: "7d",
|
||||
monthly: "30d"
|
||||
};
|
||||
formValues.budget_duration = durationMap[formValues.budget_duration];
|
||||
monthly: "30d",
|
||||
}
|
||||
formValues.budget_duration = durationMap[formValues.budget_duration]
|
||||
}
|
||||
|
||||
const newKeyValues = await keyUpdateCall(accessToken, formValues)
|
||||
|
|
@ -189,34 +178,34 @@ export default function KeyInfoView({
|
|||
if (onKeyDataUpdate) {
|
||||
onKeyDataUpdate(newKeyValues)
|
||||
}
|
||||
message.success("Key updated successfully");
|
||||
setIsEditing(false);
|
||||
NotificationManager.success("Key updated successfully")
|
||||
setIsEditing(false)
|
||||
// Refresh key data here if needed
|
||||
} catch (error) {
|
||||
message.error("Failed to update key");
|
||||
console.error("Error updating key:", error);
|
||||
NotificationManager.fromBackend(parseErrorMessage(error))
|
||||
console.error("Error updating key:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (!accessToken) return
|
||||
await keyDeleteCall(accessToken as string, currentKeyData.token || currentKeyData.token_id)
|
||||
message.success("Key deleted successfully")
|
||||
NotificationManager.success("Key deleted successfully")
|
||||
if (onDelete) {
|
||||
onDelete()
|
||||
}
|
||||
onClose();
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error("Error deleting the key:", error);
|
||||
message.error("Failed to delete key");
|
||||
console.error("Error deleting the key:", error)
|
||||
NotificationManager.fromBackend(error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string, key: string) => {
|
||||
const success = await utilCopyToClipboard(text);
|
||||
const success = await utilCopyToClipboard(text)
|
||||
if (success) {
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: true }));
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: true }))
|
||||
setTimeout(() => {
|
||||
setCopiedStates((prev) => ({ ...prev, [key]: false }))
|
||||
}, 2000)
|
||||
|
|
@ -268,12 +257,7 @@ export default function KeyInfoView({
|
|||
<div className="w-full h-screen p-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button
|
||||
icon={ArrowLeftIcon}
|
||||
variant="light"
|
||||
onClick={onClose}
|
||||
className="mb-4"
|
||||
>
|
||||
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
Back to Keys
|
||||
</Button>
|
||||
<Title>{currentKeyData.key_alias || "API Key"}</Title>
|
||||
|
|
@ -319,7 +303,9 @@ export default function KeyInfoView({
|
|||
</div>
|
||||
{userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<div className="flex gap-2">
|
||||
<Tooltip title={!premiumUser ? "This is a LiteLLM Enterprise feature, and requires a valid key to use." : ""}>
|
||||
<Tooltip
|
||||
title={!premiumUser ? "This is a LiteLLM Enterprise feature, and requires a valid key to use." : ""}
|
||||
>
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
icon={RefreshIcon}
|
||||
|
|
@ -363,34 +349,26 @@ export default function KeyInfoView({
|
|||
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
Delete Key
|
||||
</h3>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete Key</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to delete this key?
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Are you sure you want to delete this key?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
color="red"
|
||||
className="ml-2"
|
||||
>
|
||||
<Button onClick={handleDelete} color="red" className="ml-2">
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={() => setIsDeleteModalOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => setIsDeleteModalOpen(false)}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -492,7 +470,7 @@ export default function KeyInfoView({
|
|||
<Text className="font-medium">Key ID</Text>
|
||||
<Text className="font-mono">{currentKeyData.token_id || currentKeyData.token}</Text>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Key Alias</Text>
|
||||
<Text>{currentKeyData.key_alias || "Not Set"}</Text>
|
||||
|
|
@ -548,16 +526,16 @@ export default function KeyInfoView({
|
|||
: "Unlimited"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Prompts</Text>
|
||||
<Text>
|
||||
{Array.isArray(currentKeyData.metadata?.prompts) && currentKeyData.metadata.prompts.length > 0
|
||||
? currentKeyData.metadata.prompts.map((prompt, index) => (
|
||||
<span key={index} className="px-2 mr-2 py-1 bg-blue-100 rounded text-xs">
|
||||
{prompt}
|
||||
</span>
|
||||
))
|
||||
<span key={index} className="px-2 mr-2 py-1 bg-blue-100 rounded text-xs">
|
||||
{prompt}
|
||||
</span>
|
||||
))
|
||||
: "No prompts specified"}
|
||||
</Text>
|
||||
</div>
|
||||
|
|
@ -632,5 +610,5 @@ export default function KeyInfoView({
|
|||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,26 +1,21 @@
|
|||
"use client";
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import {
|
||||
keyDeleteCall,
|
||||
modelAvailableCall,
|
||||
getGuardrailsList,
|
||||
Organization,
|
||||
} from "./networking";
|
||||
import { add } from "date-fns";
|
||||
"use client"
|
||||
import React, { useEffect, useState, useMemo } from "react"
|
||||
import { keyDeleteCall, modelAvailableCall, getGuardrailsList, Organization } from "../networking"
|
||||
import { add } from "date-fns"
|
||||
import {
|
||||
InformationCircleIcon,
|
||||
StatusOnlineIcon,
|
||||
TrashIcon,
|
||||
PencilAltIcon,
|
||||
RefreshIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
} from "@heroicons/react/outline"
|
||||
import {
|
||||
keySpendLogsCall,
|
||||
PredictedSpendLogsCall,
|
||||
keyUpdateCall,
|
||||
modelInfoCall,
|
||||
regenerateKeyCall,
|
||||
} from "./networking";
|
||||
} from "../networking"
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
|
|
@ -44,16 +39,13 @@ import {
|
|||
Textarea,
|
||||
Select,
|
||||
SelectItem,
|
||||
} from "@tremor/react";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
} from "@tremor/react"
|
||||
import { InfoCircleOutlined } from "@ant-design/icons"
|
||||
import {
|
||||
fetchAvailableModelsForTeamOrKey,
|
||||
getModelDisplayName,
|
||||
} from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import {
|
||||
MultiSelect,
|
||||
MultiSelectItem,
|
||||
} from "@tremor/react";
|
||||
} from "../key_team_helpers/fetch_available_models_team_key"
|
||||
import { MultiSelect, MultiSelectItem } from "@tremor/react"
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
|
|
@ -64,28 +56,30 @@ import {
|
|||
message,
|
||||
Tooltip,
|
||||
DatePicker,
|
||||
} from "antd";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import useKeyList from "./key_team_helpers/key_list";
|
||||
import { KeyResponse } from "./key_team_helpers/key_list";
|
||||
import { AllKeysTable } from "./all_keys_table";
|
||||
import { Team } from "./key_team_helpers/key_list";
|
||||
import { Setter } from "@/types";
|
||||
} from "antd"
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard"
|
||||
import TextArea from "antd/es/input/TextArea"
|
||||
import useKeyList from "../key_team_helpers/key_list"
|
||||
import { KeyResponse } from "../key_team_helpers/key_list"
|
||||
import { AllKeysTable } from "../all_keys_table"
|
||||
import { Team } from "../key_team_helpers/key_list"
|
||||
import { Setter } from "@/types"
|
||||
|
||||
import NotificationManager from "../molecules/notifications_manager"
|
||||
|
||||
interface EditKeyModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
token: any; // Assuming TeamType is a type representing your team object
|
||||
onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted
|
||||
visible: boolean
|
||||
onCancel: () => void
|
||||
token: any // Assuming TeamType is a type representing your team object
|
||||
onSubmit: (data: FormData) => void // Assuming FormData is the type of data to be submitted
|
||||
}
|
||||
|
||||
interface ModelLimitModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
token: KeyResponse;
|
||||
onSubmit: (updatedMetadata: any) => void;
|
||||
accessToken: string;
|
||||
visible: boolean
|
||||
onCancel: () => void
|
||||
token: KeyResponse
|
||||
onSubmit: (updatedMetadata: any) => void
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
// Define the props type
|
||||
|
|
@ -94,14 +88,14 @@ interface ViewKeyTableProps {
|
|||
userRole: string | null
|
||||
accessToken: string | null
|
||||
selectedTeam: Team | null
|
||||
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
|
||||
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>
|
||||
data: KeyResponse[] | null
|
||||
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
|
||||
teams: Team[] | null;
|
||||
premiumUser: boolean;
|
||||
currentOrg: Organization | null;
|
||||
organizations: Organization[] | null;
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
|
||||
setData: React.Dispatch<React.SetStateAction<any[] | null>>
|
||||
teams: Team[] | null
|
||||
premiumUser: boolean
|
||||
currentOrg: Organization | null
|
||||
organizations: Organization[] | null
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>
|
||||
selectedKeyAlias: string | null
|
||||
setSelectedKeyAlias: Setter<string | null>
|
||||
createClicked: boolean
|
||||
|
|
@ -109,36 +103,36 @@ interface ViewKeyTableProps {
|
|||
}
|
||||
|
||||
interface ItemData {
|
||||
key_alias: string | null;
|
||||
key_name: string;
|
||||
spend: string;
|
||||
max_budget: string | null;
|
||||
models: string[];
|
||||
tpm_limit: string | null;
|
||||
rpm_limit: string | null;
|
||||
token: string;
|
||||
token_id: string | null;
|
||||
id: number;
|
||||
team_id: string;
|
||||
metadata: any;
|
||||
user_id: string | null;
|
||||
expires: any;
|
||||
budget_duration: string | null;
|
||||
budget_reset_at: string | null;
|
||||
key_alias: string | null
|
||||
key_name: string
|
||||
spend: string
|
||||
max_budget: string | null
|
||||
models: string[]
|
||||
tpm_limit: string | null
|
||||
rpm_limit: string | null
|
||||
token: string
|
||||
token_id: string | null
|
||||
id: number
|
||||
team_id: string
|
||||
metadata: any
|
||||
user_id: string | null
|
||||
expires: any
|
||||
budget_duration: string | null
|
||||
budget_reset_at: string | null
|
||||
// Add any other properties that exist in the item data
|
||||
}
|
||||
|
||||
interface ModelLimits {
|
||||
[key: string]: number; // Index signature allowing string keys
|
||||
[key: string]: number // Index signature allowing string keys
|
||||
}
|
||||
|
||||
interface CombinedLimit {
|
||||
tpm: number;
|
||||
rpm: number;
|
||||
tpm: number
|
||||
rpm: number
|
||||
}
|
||||
|
||||
interface CombinedLimits {
|
||||
[key: string]: CombinedLimit; // Index signature allowing string keys
|
||||
[key: string]: CombinedLimit // Index signature allowing string keys
|
||||
}
|
||||
|
||||
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
|
|
@ -159,22 +153,19 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
createClicked,
|
||||
setAccessToken,
|
||||
}) => {
|
||||
const [isButtonClicked, setIsButtonClicked] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<KeyResponse | null>(null);
|
||||
const [spendData, setSpendData] = useState<
|
||||
{ day: string; spend: number }[] | null
|
||||
>(null);
|
||||
|
||||
// NEW: Declare filter states for team and key alias.
|
||||
const [teamFilter, setTeamFilter] = useState<string>(selectedTeam?.team_id || "");
|
||||
const [isButtonClicked, setIsButtonClicked] = useState(false)
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||
const [keyToDelete, setKeyToDelete] = useState<string | null>(null)
|
||||
const [selectedItem, setSelectedItem] = useState<KeyResponse | null>(null)
|
||||
const [spendData, setSpendData] = useState<{ day: string; spend: number }[] | null>(null)
|
||||
|
||||
// NEW: Declare filter states for team and key alias.
|
||||
const [teamFilter, setTeamFilter] = useState<string>(selectedTeam?.team_id || "")
|
||||
|
||||
// Keep the team filter in sync with the incoming prop.
|
||||
useEffect(() => {
|
||||
setTeamFilter(selectedTeam?.team_id || "");
|
||||
}, [selectedTeam]);
|
||||
setTeamFilter(selectedTeam?.team_id || "")
|
||||
}, [selectedTeam])
|
||||
|
||||
// Build a memoized filters object for the backend call.
|
||||
|
||||
|
|
@ -185,46 +176,45 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
selectedKeyAlias,
|
||||
accessToken: accessToken || "",
|
||||
createClicked,
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
refresh({ page: newPage });
|
||||
};
|
||||
refresh({ page: newPage })
|
||||
}
|
||||
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
|
||||
const [selectedToken, setSelectedToken] = useState<KeyResponse | null>(null);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const initialKnownTeamIDs: Set<string> = new Set();
|
||||
const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false);
|
||||
const [regenerateDialogVisible, setRegenerateDialogVisible] = useState(false);
|
||||
const [regeneratedKey, setRegeneratedKey] = useState<string | null>(null);
|
||||
const [regenerateFormData, setRegenerateFormData] = useState<any>(null);
|
||||
const [regenerateForm] = Form.useForm();
|
||||
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false)
|
||||
const [infoDialogVisible, setInfoDialogVisible] = useState(false)
|
||||
const [selectedToken, setSelectedToken] = useState<KeyResponse | null>(null)
|
||||
const [userModels, setUserModels] = useState<string[]>([])
|
||||
const initialKnownTeamIDs: Set<string> = new Set()
|
||||
const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false)
|
||||
const [regenerateDialogVisible, setRegenerateDialogVisible] = useState(false)
|
||||
const [regeneratedKey, setRegeneratedKey] = useState<string | null>(null)
|
||||
const [regenerateFormData, setRegenerateFormData] = useState<any>(null)
|
||||
const [regenerateForm] = Form.useForm()
|
||||
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null)
|
||||
|
||||
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs)
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const calculateNewExpiryTime = (duration: string | undefined) => {
|
||||
if (!duration) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
let newExpiry: Date;
|
||||
const now = new Date()
|
||||
let newExpiry: Date
|
||||
|
||||
if (duration.endsWith("s")) {
|
||||
newExpiry = add(now, { seconds: parseInt(duration) });
|
||||
newExpiry = add(now, { seconds: parseInt(duration) })
|
||||
} else if (duration.endsWith("h")) {
|
||||
newExpiry = add(now, { hours: parseInt(duration) });
|
||||
newExpiry = add(now, { hours: parseInt(duration) })
|
||||
} else if (duration.endsWith("d")) {
|
||||
newExpiry = add(now, { days: parseInt(duration) });
|
||||
newExpiry = add(now, { days: parseInt(duration) })
|
||||
} else {
|
||||
throw new Error("Invalid duration format");
|
||||
throw new Error("Invalid duration format")
|
||||
}
|
||||
|
||||
return newExpiry.toLocaleString("en-US", {
|
||||
|
|
@ -235,143 +225,134 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
console.log("in calculateNewExpiryTime for selectedToken", selectedToken);
|
||||
console.log("in calculateNewExpiryTime for selectedToken", selectedToken)
|
||||
|
||||
// When a new duration is entered
|
||||
if (regenerateFormData?.duration) {
|
||||
setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration));
|
||||
setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration))
|
||||
} else {
|
||||
setNewExpiryTime(null);
|
||||
setNewExpiryTime(null)
|
||||
}
|
||||
|
||||
console.log("calculateNewExpiryTime:", newExpiryTime);
|
||||
}, [selectedToken, regenerateFormData?.duration]);
|
||||
console.log("calculateNewExpiryTime:", newExpiryTime)
|
||||
}, [selectedToken, regenerateFormData?.duration])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserModels = async () => {
|
||||
try {
|
||||
if (userID === null || userRole === null || accessToken === null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const models = await fetchAvailableModelsForTeamOrKey(
|
||||
userID,
|
||||
userRole,
|
||||
accessToken
|
||||
);
|
||||
const models = await fetchAvailableModelsForTeamOrKey(userID, userRole, accessToken)
|
||||
if (models) {
|
||||
setUserModels(models);
|
||||
setUserModels(models)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user models:", error);
|
||||
NotificationManager.error({ description: "Error fetching user models" })
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserModels();
|
||||
}, [accessToken, userID, userRole]);
|
||||
}
|
||||
|
||||
fetchUserModels()
|
||||
}, [accessToken, userID, userRole])
|
||||
|
||||
useEffect(() => {
|
||||
if (teams) {
|
||||
const teamIDSet: Set<string> = new Set();
|
||||
const teamIDSet: Set<string> = new Set()
|
||||
teams.forEach((team: any, index: number) => {
|
||||
const team_obj: string = team.team_id;
|
||||
teamIDSet.add(team_obj);
|
||||
});
|
||||
setKnownTeamIDs(teamIDSet);
|
||||
const team_obj: string = team.team_id
|
||||
teamIDSet.add(team_obj)
|
||||
})
|
||||
setKnownTeamIDs(teamIDSet)
|
||||
}
|
||||
}, [teams]);
|
||||
}, [teams])
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (keyToDelete == null || data == null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!accessToken) return
|
||||
await keyDeleteCall(accessToken, keyToDelete)
|
||||
// Successfully completed the deletion. Update the state to trigger a rerender.
|
||||
const filteredData = data.filter((item) => item.token !== keyToDelete);
|
||||
setData(filteredData);
|
||||
const filteredData = data.filter((item) => item.token !== keyToDelete)
|
||||
setData(filteredData)
|
||||
} catch (error) {
|
||||
console.error("Error deleting the key:", error);
|
||||
// Handle any error situations, such as displaying an error message to the user.
|
||||
NotificationManager.error({ description: "Error deleting the key" })
|
||||
}
|
||||
|
||||
// Close the confirmation modal and reset the keyToDelete
|
||||
setIsDeleteModalOpen(false);
|
||||
setKeyToDelete(null);
|
||||
};
|
||||
setIsDeleteModalOpen(false)
|
||||
setKeyToDelete(null)
|
||||
}
|
||||
|
||||
const cancelDelete = () => {
|
||||
// Close the confirmation modal and reset the keyToDelete
|
||||
setIsDeleteModalOpen(false);
|
||||
setKeyToDelete(null);
|
||||
};
|
||||
setIsDeleteModalOpen(false)
|
||||
setKeyToDelete(null)
|
||||
}
|
||||
|
||||
const handleRegenerateClick = (token: any) => {
|
||||
setSelectedToken(token);
|
||||
setNewExpiryTime(null);
|
||||
setSelectedToken(token)
|
||||
setNewExpiryTime(null)
|
||||
regenerateForm.setFieldsValue({
|
||||
key_alias: token.key_alias,
|
||||
max_budget: token.max_budget,
|
||||
tpm_limit: token.tpm_limit,
|
||||
rpm_limit: token.rpm_limit,
|
||||
duration: token.duration || "",
|
||||
});
|
||||
setRegenerateDialogVisible(true);
|
||||
};
|
||||
})
|
||||
setRegenerateDialogVisible(true)
|
||||
}
|
||||
|
||||
const handleRegenerateFormChange = (field: string, value: any) => {
|
||||
setRegenerateFormData((prev: any) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
}))
|
||||
}
|
||||
|
||||
const handleRegenerateKey = async () => {
|
||||
if (!premiumUser) {
|
||||
message.error(
|
||||
"Regenerate API Key is an Enterprise feature. Please upgrade to use this feature."
|
||||
);
|
||||
return;
|
||||
NotificationManager.warning({
|
||||
description: "Regenerate API Key is an Enterprise feature. Please upgrade to use this feature.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedToken == null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const formValues = await regenerateForm.validateFields()
|
||||
if (!accessToken) return;
|
||||
if (!accessToken) return
|
||||
const response = await regenerateKeyCall(accessToken, selectedToken.token || selectedToken.token_id, formValues)
|
||||
setRegeneratedKey(response.key)
|
||||
|
||||
// Update the data state with the new key_name
|
||||
if (data) {
|
||||
const updatedData = data.map((item) =>
|
||||
item.token === selectedToken?.token ?
|
||||
{ ...item, key_name: response.key_name, ...formValues }
|
||||
: item
|
||||
);
|
||||
setData(updatedData);
|
||||
item.token === selectedToken?.token ? { ...item, key_name: response.key_name, ...formValues } : item,
|
||||
)
|
||||
setData(updatedData)
|
||||
}
|
||||
|
||||
setRegenerateDialogVisible(false);
|
||||
regenerateForm.resetFields();
|
||||
message.success("API Key regenerated successfully");
|
||||
setRegenerateDialogVisible(false)
|
||||
regenerateForm.resetFields()
|
||||
NotificationManager.success({ description: "API Key regenerated successfully" })
|
||||
} catch (error) {
|
||||
console.error("Error regenerating key:", error);
|
||||
message.error("Failed to regenerate API Key");
|
||||
console.error("Error regenerating key:", error)
|
||||
NotificationManager.error({ description: "Failed to regenerate API Key" })
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -400,18 +381,12 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
{isDeleteModalOpen && (
|
||||
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div
|
||||
className="fixed inset-0 transition-opacity"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
{/* Modal Panel */}
|
||||
<span
|
||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
|
||||
|
|
@ -420,13 +395,9 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
Delete Key
|
||||
</h3>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete Key</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to delete this key ?
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">Are you sure you want to delete this key ?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -447,36 +418,32 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
title="Regenerate API Key"
|
||||
visible={regenerateDialogVisible}
|
||||
onCancel={() => {
|
||||
setRegenerateDialogVisible(false);
|
||||
regenerateForm.resetFields();
|
||||
setRegenerateDialogVisible(false)
|
||||
regenerateForm.resetFields()
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={() => {
|
||||
setRegenerateDialogVisible(false);
|
||||
regenerateForm.resetFields();
|
||||
setRegenerateDialogVisible(false)
|
||||
regenerateForm.resetFields()
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="regenerate"
|
||||
onClick={handleRegenerateKey}
|
||||
disabled={!premiumUser}
|
||||
>
|
||||
<Button key="regenerate" onClick={handleRegenerateKey} disabled={!premiumUser}>
|
||||
{premiumUser ? "Regenerate" : "Upgrade to Regenerate"}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{premiumUser ?
|
||||
{premiumUser ? (
|
||||
<Form
|
||||
form={regenerateForm}
|
||||
layout="vertical"
|
||||
onValuesChange={(changedValues, allValues) => {
|
||||
if ("duration" in changedValues) {
|
||||
handleRegenerateFormChange("duration", changedValues.duration);
|
||||
handleRegenerateFormChange("duration", changedValues.duration)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -484,11 +451,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
<TextInput disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item name="max_budget" label="Max Budget (USD)">
|
||||
<InputNumber
|
||||
step={0.01}
|
||||
precision={2}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
<InputNumber step={0.01} precision={2} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="tpm_limit" label="TPM Limit">
|
||||
<InputNumber style={{ width: "100%" }} />
|
||||
|
|
@ -496,39 +459,25 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
<Form.Item name="rpm_limit" label="RPM Limit">
|
||||
<InputNumber style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="duration"
|
||||
label="Expire Key (eg: 30s, 30h, 30d)"
|
||||
className="mt-8"
|
||||
>
|
||||
<Form.Item name="duration" label="Expire Key (eg: 30s, 30h, 30d)" className="mt-8">
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
Current expiry:{" "}
|
||||
{selectedToken?.expires != null ?
|
||||
new Date(selectedToken.expires).toLocaleString()
|
||||
: "Never"}
|
||||
{selectedToken?.expires != null ? new Date(selectedToken.expires).toLocaleString() : "Never"}
|
||||
</div>
|
||||
{newExpiryTime && (
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
New expiry: {newExpiryTime}
|
||||
</div>
|
||||
)}
|
||||
{newExpiryTime && <div className="mt-2 text-sm text-green-600">New expiry: {newExpiryTime}</div>}
|
||||
</Form>
|
||||
: <div>
|
||||
<p className="mb-2 text-gray-500 italic text-[12px]">
|
||||
Upgrade to use this feature
|
||||
</p>
|
||||
) : (
|
||||
<div>
|
||||
<p className="mb-2 text-gray-500 italic text-[12px]">Upgrade to use this feature</p>
|
||||
<Button variant="primary" className="mb-2">
|
||||
<a
|
||||
href="https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat"
|
||||
target="_blank"
|
||||
>
|
||||
<a href="https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat" target="_blank">
|
||||
Get Free Trial
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Regenerated Key Display Modal */}
|
||||
|
|
@ -546,10 +495,9 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
<Title>Regenerated Key</Title>
|
||||
<Col numColSpan={1}>
|
||||
<p>
|
||||
Please replace your old key with the new key generated. For
|
||||
security reasons, <b>you will not be able to view it again</b>{" "}
|
||||
through your LiteLLM account. If you lose this secret key, you
|
||||
will need to generate a new one.
|
||||
Please replace your old key with the new key generated. For security reasons,{" "}
|
||||
<b>you will not be able to view it again</b> through your LiteLLM account. If you lose this secret key,
|
||||
you will need to generate a new one.
|
||||
</p>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
|
|
@ -575,13 +523,11 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<pre style={{ wordWrap: "break-word", whiteSpace: "normal" }}>
|
||||
{regeneratedKey}
|
||||
</pre>
|
||||
<pre style={{ wordWrap: "break-word", whiteSpace: "normal" }}>{regeneratedKey}</pre>
|
||||
</div>
|
||||
<CopyToClipboard
|
||||
text={regeneratedKey}
|
||||
onCopy={() => message.success("API Key copied to clipboard")}
|
||||
onCopy={() => NotificationManager.success({ description: "API Key copied to clipboard" })}
|
||||
>
|
||||
<Button className="mt-3">Copy API Key</Button>
|
||||
</CopyToClipboard>
|
||||
|
|
@ -590,15 +536,15 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
// Update the type declaration to include the new function
|
||||
declare global {
|
||||
interface Window {
|
||||
refreshKeysList?: () => void;
|
||||
addNewKeyToList?: (newKey: any) => void;
|
||||
refreshKeysList?: () => void
|
||||
addNewKeyToList?: (newKey: any) => void
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewKeyTable;
|
||||
export default ViewKeyTable
|
||||
|
|
@ -1,74 +1,67 @@
|
|||
import React, { useState } from "react";
|
||||
import { BarChart } from "@tremor/react";
|
||||
import KeyInfoView from "./key_info_view";
|
||||
import { keyInfoV1Call } from "./networking";
|
||||
import { transformKeyInfo } from "../components/key_team_helpers/transform_key_info";
|
||||
import { DataTable } from "./view_logs/table";
|
||||
import { Tooltip } from "antd";
|
||||
import { Button } from "@tremor/react";
|
||||
import { formatNumberWithCommas } from "../utils/dataUtils";
|
||||
import React, { useState } from "react"
|
||||
import { BarChart } from "@tremor/react"
|
||||
import KeyInfoView from "./templates/key_info_view"
|
||||
import { keyInfoV1Call } from "./networking"
|
||||
import { transformKeyInfo } from "../components/key_team_helpers/transform_key_info"
|
||||
import { DataTable } from "./view_logs/table"
|
||||
import { Tooltip } from "antd"
|
||||
import { Button } from "@tremor/react"
|
||||
import { formatNumberWithCommas } from "../utils/dataUtils"
|
||||
|
||||
interface TopKeyViewProps {
|
||||
topKeys: any[];
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
teams: any[] | null;
|
||||
premiumUser: boolean;
|
||||
topKeys: any[]
|
||||
accessToken: string | null
|
||||
userID: string | null
|
||||
userRole: string | null
|
||||
teams: any[] | null
|
||||
premiumUser: boolean
|
||||
}
|
||||
|
||||
const TopKeyView: React.FC<TopKeyViewProps> = ({
|
||||
topKeys,
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
teams,
|
||||
premiumUser
|
||||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
||||
const [keyData, setKeyData] = useState<any | undefined>(undefined);
|
||||
const [viewMode, setViewMode] = useState<'chart' | 'table'>('table');
|
||||
const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, accessToken, userID, userRole, teams, premiumUser }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false)
|
||||
const [selectedKey, setSelectedKey] = useState<string | null>(null)
|
||||
const [keyData, setKeyData] = useState<any | undefined>(undefined)
|
||||
const [viewMode, setViewMode] = useState<"chart" | "table">("table")
|
||||
|
||||
const handleKeyClick = async (item: any) => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
const keyInfo = await keyInfoV1Call(accessToken, item.api_key);
|
||||
const transformedKeyData = transformKeyInfo(keyInfo);
|
||||
if (!accessToken) return
|
||||
|
||||
setKeyData(transformedKeyData);
|
||||
setSelectedKey(item.api_key);
|
||||
setIsModalOpen(true); // Open modal when key is clicked
|
||||
try {
|
||||
const keyInfo = await keyInfoV1Call(accessToken, item.api_key)
|
||||
const transformedKeyData = transformKeyInfo(keyInfo)
|
||||
|
||||
setKeyData(transformedKeyData)
|
||||
setSelectedKey(item.api_key)
|
||||
setIsModalOpen(true) // Open modal when key is clicked
|
||||
} catch (error) {
|
||||
console.error("Error fetching key info:", error);
|
||||
console.error("Error fetching key info:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedKey(null);
|
||||
setKeyData(undefined);
|
||||
};
|
||||
setIsModalOpen(false)
|
||||
setSelectedKey(null)
|
||||
setKeyData(undefined)
|
||||
}
|
||||
|
||||
// Handle clicking outside the modal
|
||||
const handleOutsideClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
handleClose();
|
||||
handleClose()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle escape key
|
||||
React.useEffect(() => {
|
||||
const handleEscapeKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isModalOpen) {
|
||||
handleClose();
|
||||
if (e.key === "Escape" && isModalOpen) {
|
||||
handleClose()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleEscapeKey);
|
||||
return () => document.removeEventListener('keydown', handleEscapeKey);
|
||||
}, [isModalOpen]);
|
||||
document.addEventListener("keydown", handleEscapeKey)
|
||||
return () => document.removeEventListener("keydown", handleEscapeKey)
|
||||
}, [isModalOpen])
|
||||
|
||||
// Define columns for the table view
|
||||
const columns = [
|
||||
|
|
@ -78,7 +71,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({
|
|||
cell: (info: any) => (
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip title={info.getValue() as string}>
|
||||
<Button
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
|
|
@ -91,42 +84,42 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Key Alias",
|
||||
accessorKey: "key_alias",
|
||||
cell: (info: any) => info.getValue() || "-",
|
||||
},
|
||||
header: "Key Alias",
|
||||
accessorKey: "key_alias",
|
||||
cell: (info: any) => info.getValue() || "-",
|
||||
},
|
||||
{
|
||||
header: "Spend (USD)",
|
||||
accessorKey: "spend",
|
||||
cell: (info: any) => `$${formatNumberWithCommas(info.getValue(), 2)}`,
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const processedTopKeys = topKeys.map(k => ({
|
||||
const processedTopKeys = topKeys.map((k) => ({
|
||||
...k,
|
||||
display_key_alias: k.key_alias && k.key_alias.length > 10 ? `${k.key_alias.slice(0, 10)}...` : (k.key_alias || '-'),
|
||||
}));
|
||||
display_key_alias: k.key_alias && k.key_alias.length > 10 ? `${k.key_alias.slice(0, 10)}...` : k.key_alias || "-",
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex justify-end items-center">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setViewMode('table')}
|
||||
className={`px-3 py-1 text-sm rounded-md ${viewMode === 'table' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'}`}
|
||||
<button
|
||||
onClick={() => setViewMode("table")}
|
||||
className={`px-3 py-1 text-sm rounded-md ${viewMode === "table" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
|
||||
>
|
||||
Table View
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('chart')}
|
||||
className={`px-3 py-1 text-sm rounded-md ${viewMode === 'chart' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'}`}
|
||||
onClick={() => setViewMode("chart")}
|
||||
className={`px-3 py-1 text-sm rounded-md ${viewMode === "chart" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
|
||||
>
|
||||
Chart View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewMode === 'chart' ? (
|
||||
{viewMode === "chart" ? (
|
||||
<div className="relative">
|
||||
<BarChart
|
||||
className="mt-4 h-40 cursor-pointer hover:opacity-90"
|
||||
|
|
@ -139,11 +132,11 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({
|
|||
layout="vertical"
|
||||
showXAxis={false}
|
||||
showLegend={false}
|
||||
valueFormatter={(value) => value ? `$${formatNumberWithCommas(value, 2)}` : "No Key Alias"}
|
||||
valueFormatter={(value) => (value ? `$${formatNumberWithCommas(value, 2)}` : "No Key Alias")}
|
||||
onValueChange={(item) => handleKeyClick(item)}
|
||||
showTooltip={true}
|
||||
customTooltip={(props) => {
|
||||
const item = props.payload?.[0]?.payload;
|
||||
const item = props.payload?.[0]?.payload
|
||||
return (
|
||||
<div className="relative z-50 p-3 bg-black/90 shadow-lg rounded-lg text-white max-w-xs">
|
||||
<div className="space-y-1.5">
|
||||
|
|
@ -161,7 +154,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -177,42 +170,45 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{isModalOpen && selectedKey && keyData && (
|
||||
console.log('Rendering modal with:', { isModalOpen, selectedKey, keyData }),
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
onClick={handleOutsideClick}
|
||||
>
|
||||
<div className="bg-white rounded-lg shadow-xl relative w-11/12 max-w-6xl max-h-[90vh] overflow-y-auto min-h-[750px]">
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{isModalOpen &&
|
||||
selectedKey &&
|
||||
keyData &&
|
||||
(console.log("Rendering modal with:", { isModalOpen, selectedKey, keyData }),
|
||||
(
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
onClick={handleOutsideClick}
|
||||
>
|
||||
<div className="bg-white rounded-lg shadow-xl relative w-11/12 max-w-6xl max-h-[90vh] overflow-y-auto min-h-[750px]">
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 h-full">
|
||||
<KeyInfoView
|
||||
keyId={selectedKey}
|
||||
onClose={handleClose}
|
||||
keyData={keyData}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
teams={teams}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
{/* Content */}
|
||||
<div className="p-6 h-full">
|
||||
<KeyInfoView
|
||||
keyId={selectedKey}
|
||||
onClose={handleClose}
|
||||
keyData={keyData}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
teams={teams}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default TopKeyView;
|
||||
export default TopKeyView
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import {
|
|||
} from "./networking"
|
||||
import { fetchTeams } from "./common_components/fetch_teams"
|
||||
import { Grid, Col, Card, Text, Title } from "@tremor/react"
|
||||
import CreateKey from "./create_key_button"
|
||||
import ViewKeyTable from "./view_key_table"
|
||||
import CreateKey from "./organisms/create_key_button"
|
||||
import ViewKeyTable from "./templates/view_key_table"
|
||||
import ViewUserSpend from "./view_user_spend"
|
||||
import ViewUserTeam from "./view_user_team"
|
||||
import DashboardTeam from "./dashboard_default_team"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue