mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 18:54:30 +00:00
Reflect key and team update in UI (#9825)
* Reflect updates to keys in UI instantly * Reflect updates to teams in UI instantly
This commit is contained in:
parent
cc7d59a11e
commit
d4e5da87be
9 changed files with 76 additions and 5 deletions
|
@ -13,9 +13,12 @@ import { Organization, userListCall } from "./networking";
|
||||||
import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn";
|
import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn";
|
||||||
import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn";
|
import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn";
|
||||||
import { useFilterLogic } from "./key_team_helpers/filter_logic";
|
import { useFilterLogic } from "./key_team_helpers/filter_logic";
|
||||||
|
import { Setter } from "@/types";
|
||||||
|
import { updateExistingKeys } from "@/utils/dataUtils";
|
||||||
|
|
||||||
interface AllKeysTableProps {
|
interface AllKeysTableProps {
|
||||||
keys: KeyResponse[];
|
keys: KeyResponse[];
|
||||||
|
setKeys: Setter<KeyResponse[]>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
pagination: {
|
pagination: {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
|
@ -87,6 +90,7 @@ const TeamFilter = ({
|
||||||
*/
|
*/
|
||||||
export function AllKeysTable({
|
export function AllKeysTable({
|
||||||
keys,
|
keys,
|
||||||
|
setKeys,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
pagination,
|
pagination,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
|
@ -364,6 +368,23 @@ export function AllKeysTable({
|
||||||
keyId={selectedKeyId}
|
keyId={selectedKeyId}
|
||||||
onClose={() => setSelectedKeyId(null)}
|
onClose={() => setSelectedKeyId(null)}
|
||||||
keyData={keys.find(k => k.token === selectedKeyId)}
|
keyData={keys.find(k => k.token === selectedKeyId)}
|
||||||
|
onKeyDataUpdate={(updatedKeyData) => {
|
||||||
|
setKeys(keys => keys.map(key => {
|
||||||
|
if (key.token === updatedKeyData.token) {
|
||||||
|
// The shape of key is different from that of
|
||||||
|
// updatedKeyData(received from keyUpdateCall in networking.tsx).
|
||||||
|
// Hence, we can't replace key with updatedKeys since it might lead
|
||||||
|
// to unintended bugs/behaviors.
|
||||||
|
// So instead, we only update fields that are present in both.
|
||||||
|
return updateExistingKeys(key, updatedKeyData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
setKeys(keys => keys.filter(key => key.token !== selectedKeyId))
|
||||||
|
}}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
userID={userID}
|
userID={userID}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
|
|
|
@ -27,13 +27,15 @@ interface KeyInfoViewProps {
|
||||||
keyId: string;
|
keyId: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
keyData: KeyResponse | undefined;
|
keyData: KeyResponse | undefined;
|
||||||
|
onKeyDataUpdate?: (data: Partial<KeyResponse>) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
userID: string | null;
|
userID: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
teams: any[] | null;
|
teams: any[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams }: KeyInfoViewProps) {
|
export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams, onKeyDataUpdate, onDelete }: KeyInfoViewProps) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
@ -93,6 +95,9 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||||
}
|
}
|
||||||
|
|
||||||
const newKeyValues = await keyUpdateCall(accessToken, formValues);
|
const newKeyValues = await keyUpdateCall(accessToken, formValues);
|
||||||
|
if (onKeyDataUpdate) {
|
||||||
|
onKeyDataUpdate(newKeyValues)
|
||||||
|
}
|
||||||
message.success("Key updated successfully");
|
message.success("Key updated successfully");
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
// Refresh key data here if needed
|
// Refresh key data here if needed
|
||||||
|
@ -107,6 +112,9 @@ export default function KeyInfoView({ keyId, onClose, keyData, accessToken, user
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
await keyDeleteCall(accessToken as string, keyData.token);
|
await keyDeleteCall(accessToken as string, keyData.token);
|
||||||
message.success("Key deleted successfully");
|
message.success("Key deleted successfully");
|
||||||
|
if (onDelete) {
|
||||||
|
onDelete()
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting the key:", error);
|
console.error("Error deleting the key:", error);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { keyListCall, Organization } from '../networking';
|
import { keyListCall, Organization } from '../networking';
|
||||||
|
import { Setter } from '@/types';
|
||||||
|
|
||||||
export interface Team {
|
export interface Team {
|
||||||
team_id: string;
|
team_id: string;
|
||||||
|
@ -94,13 +95,14 @@ totalPages: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface UseKeyListReturn {
|
interface UseKeyListReturn {
|
||||||
keys: KeyResponse[];
|
keys: KeyResponse[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
pagination: PaginationData;
|
pagination: PaginationData;
|
||||||
refresh: (params?: Record<string, unknown>) => Promise<void>;
|
refresh: (params?: Record<string, unknown>) => Promise<void>;
|
||||||
setKeys: (newKeysOrUpdater: KeyResponse[] | ((prevKeys: KeyResponse[]) => KeyResponse[])) => void;
|
setKeys: Setter<KeyResponse[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useKeyList = ({
|
const useKeyList = ({
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { all_admin_roles } from "@/utils/roles";
|
import { all_admin_roles } from "@/utils/roles";
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
import { TagNewRequest, TagUpdateRequest, TagDeleteRequest, TagInfoRequest, TagListResponse, TagInfoResponse } from "./tag_management/types";
|
import { TagNewRequest, TagUpdateRequest, TagDeleteRequest, TagInfoRequest, TagListResponse, TagInfoResponse } from "./tag_management/types";
|
||||||
|
import { Team } from "./key_team_helpers/key_list";
|
||||||
|
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
export const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
export const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||||
|
@ -2983,7 +2984,7 @@ export const teamUpdateCall = async (
|
||||||
console.error("Error response from the server:", errorData);
|
console.error("Error response from the server:", errorData);
|
||||||
throw new Error("Network response was not ok");
|
throw new Error("Network response was not ok");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json() as { data: Team, team_id: string };
|
||||||
console.log("Update Team Response:", data);
|
console.log("Update Team Response:", data);
|
||||||
return data;
|
return data;
|
||||||
// Handle success - you might want to update some state or UI based on the created key
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { PencilAltIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
|
||||||
import MemberModal from "./edit_membership";
|
import MemberModal from "./edit_membership";
|
||||||
import UserSearchModal from "@/components/common_components/user_search_modal";
|
import UserSearchModal from "@/components/common_components/user_search_modal";
|
||||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||||
|
import { Team } from "../key_team_helpers/key_list";
|
||||||
|
|
||||||
|
|
||||||
interface TeamData {
|
interface TeamData {
|
||||||
|
@ -69,6 +70,7 @@ interface TeamInfoProps {
|
||||||
is_proxy_admin: boolean;
|
is_proxy_admin: boolean;
|
||||||
userModels: string[];
|
userModels: string[];
|
||||||
editTeam: boolean;
|
editTeam: boolean;
|
||||||
|
onUpdate?: (team: Team) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TeamInfoView: React.FC<TeamInfoProps> = ({
|
const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||||
|
@ -78,7 +80,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||||
is_team_admin,
|
is_team_admin,
|
||||||
is_proxy_admin,
|
is_proxy_admin,
|
||||||
userModels,
|
userModels,
|
||||||
editTeam
|
editTeam,
|
||||||
|
onUpdate
|
||||||
}) => {
|
}) => {
|
||||||
const [teamData, setTeamData] = useState<TeamData | null>(null);
|
const [teamData, setTeamData] = useState<TeamData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
@ -199,7 +202,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await teamUpdateCall(accessToken, updateData);
|
const response = await teamUpdateCall(accessToken, updateData);
|
||||||
|
if (onUpdate) {
|
||||||
|
onUpdate(response.data)
|
||||||
|
}
|
||||||
|
|
||||||
message.success("Team settings updated successfully");
|
message.success("Team settings updated successfully");
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
fetchTeamInfo();
|
fetchTeamInfo();
|
||||||
|
|
|
@ -84,6 +84,7 @@ import {
|
||||||
modelAvailableCall,
|
modelAvailableCall,
|
||||||
teamListCall
|
teamListCall
|
||||||
} from "./networking";
|
} from "./networking";
|
||||||
|
import { updateExistingKeys } from "@/utils/dataUtils";
|
||||||
|
|
||||||
const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
|
const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
|
||||||
let tempModelsToPick = [];
|
let tempModelsToPick = [];
|
||||||
|
@ -321,6 +322,22 @@ const Teams: React.FC<TeamProps> = ({
|
||||||
{selectedTeamId ? (
|
{selectedTeamId ? (
|
||||||
<TeamInfoView
|
<TeamInfoView
|
||||||
teamId={selectedTeamId}
|
teamId={selectedTeamId}
|
||||||
|
onUpdate={data => {
|
||||||
|
setTeams(teams => {
|
||||||
|
if (teams == null) {
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
|
||||||
|
return teams.map(team => {
|
||||||
|
if (data.team_id === team.team_id) {
|
||||||
|
return updateExistingKeys(team, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return team
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setSelectedTeamId(null);
|
setSelectedTeamId(null);
|
||||||
setEditTeam(false);
|
setEditTeam(false);
|
||||||
|
|
|
@ -418,6 +418,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
<div>
|
<div>
|
||||||
<AllKeysTable
|
<AllKeysTable
|
||||||
keys={keys}
|
keys={keys}
|
||||||
|
setKeys={setKeys}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
|
|
1
ui/litellm-dashboard/src/types.ts
Normal file
1
ui/litellm-dashboard/src/types.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export type Setter<T> = (newValueOrUpdater: T | ((previousValue: T) => T)) => void
|
14
ui/litellm-dashboard/src/utils/dataUtils.ts
Normal file
14
ui/litellm-dashboard/src/utils/dataUtils.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export function updateExistingKeys<Source extends Object>(
|
||||||
|
target: Source,
|
||||||
|
source: Object
|
||||||
|
): Source {
|
||||||
|
const clonedTarget = structuredClone(target);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(source)) {
|
||||||
|
if (key in clonedTarget) {
|
||||||
|
(clonedTarget as any)[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedTarget;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue