Change modals to reusable component

This commit is contained in:
yuneng-jiang
2025-11-24 20:37:33 -08:00
parent d2b90e6644
commit d53bc7b9a0
7 changed files with 212 additions and 105 deletions
@@ -30,8 +30,7 @@ import {
Text,
TextInput,
} from "@tremor/react";
import { Button as Button2, Form, Input, Modal, Select as Select2, Tooltip, Typography } from "antd";
import { AlertTriangleIcon, XIcon } from "lucide-react";
import { Button as Button2, Form, Input, Modal, Select as Select2, Switch, Tooltip, Typography } from "antd";
import React, { useEffect, useState } from "react";
import { formatNumberWithCommas } from "../utils/dataUtils";
import { fetchTeams } from "./common_components/fetch_teams";
@@ -77,6 +76,7 @@ interface EditTeamModalProps {
}
import { updateExistingKeys } from "@/utils/dataUtils";
import DeleteResourceModal from "./common_components/DeleteResourceModal";
import { Member, teamCreateCall, v2TeamListCall } from "./networking";
interface TeamInfo {
@@ -57,10 +57,6 @@ export default function DeleteResourceModal({
>
<div className="space-y-4">
{alertMessage && <Alert message={alertMessage} type="warning" />}
<div>
<Text>{message}</Text>
</div>
<div className="mt-4 p-4 bg-red-50 rounded-lg border border-red-200">
<Title level={5} className="mb-3 text-gray-900">
{resourceInformationTitle}
@@ -74,18 +70,23 @@ export default function DeleteResourceModal({
))}
</Descriptions>
</div>
<div>
<Text>{message}</Text>
</div>
{requiredConfirmation && (
<div className="mb-5">
<div className="mb-6 mt-4 pt-4 border-t border-gray-200">
<Text className="block text-base font-medium text-gray-700 mb-2">
{`Type `}
<span className="underline">{requiredConfirmation}</span>
{` to confirm deletion:`}
<Text>Type </Text>
<Text strong type="danger">
{requiredConfirmation}
</Text>
<Text> to confirm deletion:</Text>
</Text>
<Input
value={requiredConfirmationInput}
onChange={(e) => setRequiredConfirmationInput(e.target.value)}
placeholder={requiredConfirmation}
className="rounded-md"
className="rounded-md text-base border-gray-200"
autoFocus
/>
</div>
@@ -0,0 +1,104 @@
import { render, screen } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import GuardrailsPanel from "./guardrails";
import { getGuardrailsList } from "./networking";
vi.mock("./networking", () => ({
getGuardrailsList: vi.fn(),
deleteGuardrailCall: vi.fn(),
}));
vi.mock("./guardrails/add_guardrail_form", () => ({
__esModule: true,
default: () => <div>Mock Add Guardrail Form</div>,
}));
vi.mock("./guardrails/guardrail_table", () => ({
__esModule: true,
default: ({ guardrailsList, onDeleteClick }: any) => (
<div>
<div>Mock Guardrail Table</div>
{guardrailsList.length > 0 && (
<button
data-testid="delete-button"
onClick={() => onDeleteClick(guardrailsList[0].guardrail_id, guardrailsList[0].guardrail_name)}
>
Delete
</button>
)}
</div>
),
}));
vi.mock("./guardrails/guardrail_info", () => ({
__esModule: true,
default: () => <div>Mock Guardrail Info View</div>,
}));
vi.mock("./guardrails/GuardrailTestPlayground", () => ({
__esModule: true,
default: () => <div>Mock Guardrail Test Playground</div>,
}));
vi.mock("@/utils/roles", () => ({
isAdminRole: vi.fn((role: string) => role === "admin"),
}));
vi.mock("./guardrails/guardrail_info_helpers", () => ({
getGuardrailLogoAndName: vi.fn(() => ({
logo: null,
displayName: "Test Provider",
})),
}));
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});
describe("GuardrailsPanel", () => {
const defaultProps = {
accessToken: "test-token",
userRole: "admin",
};
const mockGetGuardrailsList = vi.mocked(getGuardrailsList);
beforeEach(() => {
vi.clearAllMocks();
mockGetGuardrailsList.mockResolvedValue({
guardrails: [
{
guardrail_id: "test-guardrail-1",
guardrail_name: "Test Guardrail",
litellm_params: {
guardrail: "test-provider",
mode: "async",
default_on: true,
},
guardrail_info: null,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
guardrail_definition_location: "database" as any,
},
],
});
});
it("should render the component", async () => {
render(<GuardrailsPanel {...defaultProps} />);
expect(screen.getByText("Guardrails")).toBeInTheDocument();
expect(screen.getByText("+ Add New Guardrail")).toBeInTheDocument();
});
});
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { Modal } from "antd";
import { getGuardrailsList, deleteGuardrailCall } from "./networking";
import AddGuardrailForm from "./guardrails/add_guardrail_form";
import GuardrailTable from "./guardrails/guardrail_table";
@@ -9,6 +8,8 @@ import GuardrailInfoView from "./guardrails/guardrail_info";
import GuardrailTestPlayground from "./guardrails/GuardrailTestPlayground";
import NotificationsManager from "./molecules/notifications_manager";
import { Guardrail, GuardrailDefinitionLocation } from "./guardrails/types";
import DeleteResourceModal from "./common_components/DeleteResourceModal";
import { getGuardrailLogoAndName } from "./guardrails/guardrail_info_helpers";
interface GuardrailsPanelProps {
accessToken: string | null;
@@ -38,7 +39,8 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [guardrailToDelete, setGuardrailToDelete] = useState<{ id: string; name: string } | null>(null);
const [guardrailToDelete, setGuardrailToDelete] = useState<Guardrail | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedGuardrailId, setSelectedGuardrailId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<number>(0);
@@ -81,7 +83,9 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
};
const handleDeleteClick = (guardrailId: string, guardrailName: string) => {
setGuardrailToDelete({ id: guardrailId, name: guardrailName });
const guardrail = guardrailsList.find((g) => g.guardrail_id === guardrailId) || null;
setGuardrailToDelete(guardrail);
setIsDeleteModalOpen(true);
};
const handleDeleteConfirm = async () => {
@@ -90,22 +94,29 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
// Log removed to maintain clean production code
setIsDeleting(true);
try {
await deleteGuardrailCall(accessToken, guardrailToDelete.id);
NotificationsManager.success(`Guardrail "${guardrailToDelete.name}" deleted successfully`);
fetchGuardrails(); // Refresh the list
await deleteGuardrailCall(accessToken, guardrailToDelete.guardrail_id);
NotificationsManager.success(`Guardrail "${guardrailToDelete.guardrail_name}" deleted successfully`);
await fetchGuardrails(); // Refresh the list
} catch (error) {
console.error("Error deleting guardrail:", error);
NotificationsManager.fromBackend("Failed to delete guardrail");
} finally {
setIsDeleting(false);
setIsDeleteModalOpen(false);
setGuardrailToDelete(null);
}
};
const handleDeleteCancel = () => {
setIsDeleteModalOpen(false);
setGuardrailToDelete(null);
};
const providerDisplayName =
guardrailToDelete && guardrailToDelete.litellm_params
? getGuardrailLogoAndName(guardrailToDelete.litellm_params.guardrail).displayName
: undefined;
return (
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
<TabGroup index={activeTab} onIndexChange={setActiveTab}>
@@ -148,20 +159,25 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
onSuccess={handleSuccess}
/>
{guardrailToDelete && (
<Modal
title="Delete Guardrail"
open={guardrailToDelete !== null}
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
confirmLoading={isDeleting}
okText="Delete"
okButtonProps={{ danger: true }}
>
<p>Are you sure you want to delete guardrail: {guardrailToDelete.name} ?</p>
<p>This action cannot be undone.</p>
</Modal>
)}
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Guardrail"
message={`Are you sure you want to delete guardrail: ${guardrailToDelete?.guardrail_name}? This action cannot be undone.`}
resourceInformationTitle="Guardrail Information"
resourceInformation={[
{ label: "Name", value: guardrailToDelete?.guardrail_name },
{ label: "ID", value: guardrailToDelete?.guardrail_id, code: true },
{ label: "Provider", value: providerDisplayName },
{ label: "Mode", value: guardrailToDelete?.litellm_params.mode },
{
label: "Default On",
value: guardrailToDelete?.litellm_params.default_on ? "Yes" : "No",
},
]}
onCancel={handleDeleteCancel}
onOk={handleDeleteConfirm}
confirmLoading={isDeleting}
/>
</TabPanel>
<TabPanel>
@@ -32,6 +32,7 @@ import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
import { formatNumberWithCommas } from "../utils/dataUtils";
import NotificationsManager from "./molecules/notifications_manager";
import DeleteResourceModal from "./common_components/DeleteResourceModal";
interface OrganizationsTableProps {
organizations: Organization[];
@@ -70,6 +71,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
const [editOrg, setEditOrg] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [orgToDelete, setOrgToDelete] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isOrgModalVisible, setIsOrgModalVisible] = useState(false);
const [form] = Form.useForm();
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
@@ -91,15 +93,18 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
if (!orgToDelete || !accessToken) return;
try {
setIsDeleting(true);
await organizationDeleteCall(accessToken, orgToDelete);
NotificationsManager.success("Organization deleted successfully");
setIsDeleteModalOpen(false);
setOrgToDelete(null);
// Refresh organizations list
fetchOrganizations(accessToken, setOrganizations);
await fetchOrganizations(accessToken, setOrganizations);
} catch (error) {
console.error("Error deleting organization:", error);
} finally {
setIsDeleting(false);
}
};
@@ -506,40 +511,16 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
</Form>
</Modal>
{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="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>
<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 Organization</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">Are you sure you want to delete this organization?</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={confirmDelete} color="red" className="ml-2">
Delete
</Button>
<Button onClick={cancelDelete}>Cancel</Button>
</div>
</div>
</div>
</div>
) : (
<></>
)}
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Organization?"
message="Are you sure you want to delete this organization? This action cannot be undone."
resourceInformationTitle="Organization Information"
resourceInformation={[{ label: "Organization ID", value: orgToDelete, code: true }]}
onCancel={cancelDelete}
onOk={confirmDelete}
confirmLoading={isDeleting}
/>
</div>
);
};
@@ -38,6 +38,7 @@ import {
import { LoggingCallbacksTable } from "./Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable";
import { AlertingObject } from "./Settings/LoggingAndAlerts/LoggingCallbacks/types";
import { parseErrorMessage } from "./shared/errorUtils";
import DeleteResourceModal from "./common_components/DeleteResourceModal";
interface SettingsPageProps {
accessToken: string | null;
userRole: string | null;
@@ -240,9 +241,10 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
const [showEditCallback, setShowEditCallback] = useState(false);
const [selectedEditCallback, setSelectedEditCallback] = useState<any | null>(null);
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
const [callbackToDelete, setCallbackToDelete] = useState<string | null>(null);
const [callbackToDelete, setCallbackToDelete] = useState<any | null>(null);
const [isUpdatingCallback, setIsUpdatingCallback] = useState(false);
const [isAddingCallback, setIsAddingCallback] = useState(false);
const [isDeletingCallback, setIsDeletingCallback] = useState(false);
useEffect(() => {
if (!accessToken) {
@@ -525,8 +527,8 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
});
};
const handleDeleteCallback = (callbackName: string) => {
setCallbackToDelete(callbackName);
const handleDeleteCallback = (callback: any) => {
setCallbackToDelete(callback);
setShowDeleteConfirmModal(true);
};
@@ -536,8 +538,9 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
}
try {
await deleteCallback(accessToken, callbackToDelete);
NotificationsManager.success(`Callback ${callbackToDelete} deleted successfully`);
setIsDeletingCallback(true);
await deleteCallback(accessToken, callbackToDelete.name);
NotificationsManager.success(`Callback ${callbackToDelete.name} deleted successfully`);
// Refresh the callbacks list
if (userID && userRole) {
@@ -550,6 +553,8 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
} catch (error) {
console.error("Failed to delete callback:", error);
NotificationsManager.fromBackend(error);
} finally {
setIsDeletingCallback(false);
}
};
@@ -577,7 +582,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
setSelectedEditCallback(cb);
setShowEditCallback(true);
}}
onDelete={(cb) => handleDeleteCallback(cb.name)}
onDelete={(cb) => handleDeleteCallback(cb)}
onTest={async (cb) => {
try {
await serviceHealthCheck(accessToken, cb.name);
@@ -804,20 +809,22 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
</Form>
</Modal>
<Modal
title="Confirm Delete"
open={showDeleteConfirmModal}
onOk={confirmDeleteCallback}
<DeleteResourceModal
isOpen={showDeleteConfirmModal}
title="Delete Callback"
message="Are you sure you want to delete this callback? This action cannot be undone."
resourceInformationTitle="Callback Information"
resourceInformation={[
{ label: "Callback Name", value: callbackToDelete?.name },
{ label: "Mode", value: callbackToDelete?.mode || "success" },
]}
onCancel={() => {
setShowDeleteConfirmModal(false);
setCallbackToDelete(null);
}}
okText="Delete"
cancelText="Cancel"
okButtonProps={{ danger: true }}
>
<p>Are you sure you want to delete the callback - {callbackToDelete}? This action cannot be undone.</p>
</Modal>
onOk={confirmDeleteCallback}
confirmLoading={isDeletingCallback}
/>
</div>
);
};
@@ -25,7 +25,7 @@ import {
teamUpdateCall,
getGuardrailsList,
} from "@/components/networking";
import { Button, Form, Input, Select, Switch, message, Modal, Tooltip } from "antd";
import { Button, Form, Input, Select, Switch, message, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import MemberModal from "./edit_membership";
@@ -44,6 +44,7 @@ import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
import NotificationsManager from "../molecules/notifications_manager";
import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
export interface TeamMembership {
user_id: string;
@@ -139,6 +140,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [memberToDelete, setMemberToDelete] = useState<Member | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
console.log("userModels in team info", userModels);
@@ -272,6 +274,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
const handleMemberDelete = (member: Member) => {
setMemberToDelete(member);
setIsDeleteModalOpen(true);
};
const handleDeleteConfirm = async () => {
@@ -294,11 +297,13 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
console.error("Error removing team member:", error);
} finally {
setIsDeleting(false);
setIsDeleteModalOpen(false);
setMemberToDelete(null);
}
};
const handleDeleteCancel = () => {
setIsDeleteModalOpen(false);
setMemberToDelete(null);
};
@@ -924,28 +929,21 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
/>
{/* Delete Member Confirmation Modal */}
{memberToDelete && (
<Modal
title="Delete Team Member"
open={memberToDelete !== null}
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
confirmLoading={isDeleting}
okText={isDeleting ? "Deleting..." : "Delete"}
okButtonProps={{ danger: true }}
>
<p>Are you sure you want to remove this member from the team?</p>
<p className="mt-2">
<strong>User ID:</strong> {memberToDelete.user_id}
</p>
{memberToDelete.user_email && (
<p>
<strong>Email:</strong> {memberToDelete.user_email}
</p>
)}
<p className="mt-2 text-red-600">This action cannot be undone.</p>
</Modal>
)}
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Team Member"
alertMessage="Removing team members will also delete any keys created by or created for this member."
message="Are you sure you want to remove this member from the team? This action cannot be undone."
resourceInformationTitle="Team Member Information"
resourceInformation={[
{ label: "User ID", value: memberToDelete?.user_id, code: true },
{ label: "Email", value: memberToDelete?.user_email },
{ label: "Role", value: memberToDelete?.role },
]}
onCancel={handleDeleteCancel}
onOk={handleDeleteConfirm}
confirmLoading={isDeleting}
/>
</div>
);
};