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 commit 74eb734571.

* Reapply "fix test"

This reverts commit 72d0b2d4c6.

* 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:
tanjiro 2025-08-13 09:21:04 +09:00 • committed by GitHub
parent 280ad6f049
commit 911b0cfd73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1670 additions and 1959 deletions

View file

@ -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>
);
)
}

View file

@ -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>
);
}
)
}

View file

@ -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);

View file

@ -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;

View file

@ -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>
);
}
)
}

View file

@ -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

View file

@ -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>
);
}
)
}

View file

@ -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">&#8203;</span>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</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>
);
}
)
}

View file

@ -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">
&#8203;
</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

View file

@ -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

View file

@ -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