Disable edit, delete, info, for dynamically generated spend tags

This commit is contained in:
yuneng-jiang
2025-11-25 12:03:01 -08:00
parent 3249f6dd2d
commit 44cde2e48f
5 changed files with 279 additions and 77 deletions
@@ -0,0 +1,101 @@
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import TagTable from "./TagTable";
import { Tag } from "./types";
describe("TagTable", () => {
const mockOnEdit = vi.fn();
const mockOnDelete = vi.fn();
const mockOnSelectTag = vi.fn();
const mockTag: Tag = {
name: "test-tag",
description: "Test description",
models: ["model-1", "model-2"],
model_info: {
"model-1": "GPT-4",
"model-2": "Claude-3",
},
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
const mockDynamicSpendTag: Tag = {
name: "dynamic-spend-tag",
description:
"This is just a spend tag that was passed dynamically in a request. It does not control any LLM models.",
models: [],
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
};
const defaultProps = {
data: [],
onEdit: mockOnEdit,
onDelete: mockOnDelete,
onSelectTag: mockOnSelectTag,
};
beforeEach(() => {
vi.clearAllMocks();
});
it("should render", () => {
render(<TagTable {...defaultProps} />);
expect(screen.getByText("Tag Name")).toBeInTheDocument();
expect(screen.getByText("Description")).toBeInTheDocument();
expect(screen.getByText("Allowed Models")).toBeInTheDocument();
expect(screen.getByText("Created")).toBeInTheDocument();
expect(screen.getByText("Actions")).toBeInTheDocument();
});
it("should display no tags found message when data is empty", () => {
render(<TagTable {...defaultProps} />);
expect(screen.getByText("No tags found")).toBeInTheDocument();
});
it("should display tag name", () => {
render(<TagTable {...defaultProps} data={[mockTag]} />);
expect(screen.getByText("test-tag")).toBeInTheDocument();
});
it("should display tag description", () => {
render(<TagTable {...defaultProps} data={[mockTag]} />);
expect(screen.getByText("Test description")).toBeInTheDocument();
});
it("should display All Models badge when models array is empty", () => {
const tagWithNoModels: Tag = {
...mockTag,
models: [],
};
render(<TagTable {...defaultProps} data={[tagWithNoModels]} />);
expect(screen.getByText("All Models")).toBeInTheDocument();
});
it("should display formatted created date", () => {
render(<TagTable {...defaultProps} data={[mockTag]} />);
const formattedDate = new Date(mockTag.created_at).toLocaleDateString();
expect(screen.getByText(formattedDate)).toBeInTheDocument();
});
it("should disable tag name button for dynamic spend tags", () => {
render(<TagTable {...defaultProps} data={[mockDynamicSpendTag]} />);
const tagButton = screen.getByRole("button", { name: "dynamic-spend-tag" });
expect(tagButton).toBeDisabled();
});
it("should disable edit icon for dynamic spend tags", () => {
render(<TagTable {...defaultProps} data={[mockDynamicSpendTag]} />);
const editIcon = screen.getByLabelText("Edit tag (disabled)");
expect(editIcon).toBeInTheDocument();
expect(editIcon).toHaveClass("cursor-not-allowed");
});
it("should disable delete icon for dynamic spend tags", () => {
render(<TagTable {...defaultProps} data={[mockDynamicSpendTag]} />);
const deleteIcon = screen.getByLabelText("Delete tag (disabled)");
expect(deleteIcon).toBeInTheDocument();
expect(deleteIcon).toHaveClass("cursor-not-allowed");
});
});
@@ -1,18 +1,4 @@
import React from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Icon,
Button,
Badge,
Text,
} from "@tremor/react";
import { PencilAltIcon, TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { ChevronDownIcon, ChevronUpIcon, PencilAltIcon, SwitchVerticalIcon, TrashIcon } from "@heroicons/react/outline";
import {
ColumnDef,
flexRender,
@@ -21,6 +7,20 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
import {
Badge,
Button,
Icon,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Text,
} from "@tremor/react";
import { Tooltip } from "antd";
import React from "react";
import { Tag } from "./types";
interface TagTableProps {
@@ -30,6 +30,9 @@ interface TagTableProps {
onSelectTag: (tagName: string) => void;
}
const DYNAMIC_SPEND_TAG_DESCRIPTION =
"This is just a spend tag that was passed dynamically in a request. It does not control any LLM models.";
const TagTable: React.FC<TagTableProps> = ({ data, onEdit, onDelete, onSelectTag }) => {
const [sorting, setSorting] = React.useState<SortingState>([{ id: "created_at", desc: true }]);
@@ -39,14 +42,20 @@ const TagTable: React.FC<TagTableProps> = ({ data, onEdit, onDelete, onSelectTag
accessorKey: "name",
cell: ({ row }) => {
const tag = row.original;
const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION;
return (
<div className="overflow-hidden">
<Tooltip title={tag.name}>
<Tooltip
title={
isDynamicSpendTag ? "You cannot view the information of a dynamically generated spend tag" : tag.name
}
>
<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"
onClick={() => onSelectTag(tag.name)}
disabled={isDynamicSpendTag}
>
{tag.name}
</Button>
@@ -68,7 +77,7 @@ const TagTable: React.FC<TagTableProps> = ({ data, onEdit, onDelete, onSelectTag
},
},
{
header: "Allowed LLMs",
header: "Allowed Models",
accessorKey: "models",
cell: ({ row }) => {
const tag = row.original;
@@ -102,13 +111,50 @@ const TagTable: React.FC<TagTableProps> = ({ data, onEdit, onDelete, onSelectTag
},
{
id: "actions",
header: "",
header: "Actions",
cell: ({ row }) => {
const tag = row.original;
const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION;
return (
<div className="flex space-x-2">
<Icon icon={PencilAltIcon} size="sm" onClick={() => onEdit(tag)} className="cursor-pointer" />
<Icon icon={TrashIcon} size="sm" onClick={() => onDelete(tag.name)} className="cursor-pointer" />
{isDynamicSpendTag ? (
<Tooltip title="Dynamically generated spend tags cannot be edited">
<Icon
icon={PencilAltIcon}
size="sm"
className="opacity-50 cursor-not-allowed"
aria-label="Edit tag (disabled)"
/>
</Tooltip>
) : (
<Tooltip title="Edit tag">
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => onEdit(tag)}
className="cursor-pointer hover:text-blue-500"
/>
</Tooltip>
)}
{isDynamicSpendTag ? (
<Tooltip title="Dynamically generated spend tags cannot be deleted">
<Icon
icon={TrashIcon}
size="sm"
className="opacity-50 cursor-not-allowed"
aria-label="Delete tag (disabled)"
/>
</Tooltip>
) : (
<Tooltip title="Delete tag">
<Icon
icon={TrashIcon}
size="sm"
onClick={() => onDelete(tag.name)}
className="cursor-pointer hover:text-red-500"
/>
</Tooltip>
)}
</div>
);
},
@@ -0,0 +1,64 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import CreateTagModal from "./CreateTagModal";
describe("CreateTagModal", () => {
const mockOnCancel = vi.fn();
const mockOnSubmit = vi.fn();
const mockAvailableModels = [
{
model_name: "GPT-4",
litellm_params: { model: "gpt-4" },
model_info: { id: "model-1" },
},
{
model_name: "Claude-3",
litellm_params: { model: "claude-3" },
model_info: { id: "model-2" },
},
];
const defaultProps = {
visible: true,
onCancel: mockOnCancel,
onSubmit: mockOnSubmit,
availableModels: mockAvailableModels,
};
beforeEach(() => {
vi.clearAllMocks();
});
it("should render the modal", () => {
render(<CreateTagModal {...defaultProps} />);
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Create New Tag")).toBeInTheDocument();
});
it("should submit form with required tag name", async () => {
const user = userEvent.setup();
render(<CreateTagModal {...defaultProps} />);
const tagNameInput = screen.getByLabelText("Tag Name");
await user.type(tagNameInput, "test-tag");
const submitButton = screen.getByRole("button", { name: /Create Tag/i });
await user.click(submitButton);
expect(mockOnSubmit).toHaveBeenCalledWith({
tag_name: "test-tag",
});
});
it("should not submit form when tag name is missing", async () => {
const user = userEvent.setup();
render(<CreateTagModal {...defaultProps} />);
const submitButton = screen.getByRole("button", { name: /Create Tag/i });
await user.click(submitButton);
// Form validation should prevent submission
expect(mockOnSubmit).not.toHaveBeenCalled();
});
});
@@ -1,9 +1,9 @@
import React from "react";
import { Button, TextInput, Accordion, AccordionHeader, AccordionBody, Title } from "@tremor/react";
import { Modal, Form, Select as Select2, Tooltip, Input } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import NumericalInput from "../../shared/numerical_input";
import { Accordion, AccordionBody, AccordionHeader, Button, TextInput, Title } from "@tremor/react";
import { Form, Input, Modal, Select as Select2, Tooltip } from "antd";
import React from "react";
import BudgetDurationDropdown from "../../common_components/budget_duration_dropdown";
import NumericalInput from "../../shared/numerical_input";
interface ModelInfo {
model_name: string;
@@ -22,12 +22,7 @@ interface CreateTagModalProps {
availableModels: ModelInfo[];
}
const CreateTagModal: React.FC<CreateTagModalProps> = ({
visible,
onCancel,
onSubmit,
availableModels,
}) => {
const CreateTagModal: React.FC<CreateTagModalProps> = ({ visible, onCancel, onSubmit, availableModels }) => {
const [form] = Form.useForm();
const handleFinish = (values: any) => {
@@ -41,25 +36,9 @@ const CreateTagModal: React.FC<CreateTagModalProps> = ({
};
return (
<Modal
title="Create New Tag"
visible={visible}
width={800}
footer={null}
onCancel={handleCancel}
>
<Form
form={form}
onFinish={handleFinish}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<Form.Item
label="Tag Name"
name="tag_name"
rules={[{ required: true, message: "Please input a tag name" }]}
>
<Modal title="Create New Tag" visible={visible} width={800} footer={null} onCancel={handleCancel}>
<Form form={form} onFinish={handleFinish} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
<Form.Item label="Tag Name" name="tag_name" rules={[{ required: true, message: "Please input a tag name" }]}>
<TextInput />
</Form.Item>
@@ -70,15 +49,15 @@ const CreateTagModal: React.FC<CreateTagModalProps> = ({
<Form.Item
label={
<span>
Allowed Models{" "}
<Tooltip title="Select which LLMs are allowed to process requests from this tag">
Allowed Models
<Tooltip title="Select which models are allowed to process requests from this tag">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_llms"
>
<Select2 mode="multiple" placeholder="Select LLMs">
<Select2 mode="multiple" placeholder="Select Models">
{availableModels.map((model) => (
<Select2.Option key={model.model_info.id} value={model.model_info.id}>
<div>
@@ -150,4 +129,3 @@ const CreateTagModal: React.FC<CreateTagModalProps> = ({
};
export default CreateTagModal;
@@ -1,5 +1,15 @@
import React, { useState, useEffect } from "react";
import { Card, Text, Title, Button, Badge, Accordion, AccordionHeader, AccordionBody, Title as TremorTitle } from "@tremor/react";
import {
Card,
Text,
Title,
Button,
Badge,
Accordion,
AccordionHeader,
AccordionBody,
Title as TremorTitle,
} from "@tremor/react";
import { Form, Input, Select as Select2, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { fetchUserModels } from "../organisms/create_key_button";
@@ -131,7 +141,7 @@ const TagInfoView: React.FC<TagInfoViewProps> = ({ tagId, onClose, accessToken,
<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" }]}>
<Input />
<Input className="rounded-md border-gray-300" />
</Form.Item>
<Form.Item label="Description" name="description">
@@ -141,15 +151,15 @@ const TagInfoView: React.FC<TagInfoViewProps> = ({ tagId, onClose, accessToken,
<Form.Item
label={
<span>
Allowed LLMs{" "}
<Tooltip title="Select which LLMs are allowed to process this type of data">
Allowed Models
<Tooltip title="Select which models are allowed to process this type of data">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="models"
>
<Select2 mode="multiple" placeholder="Select LLMs">
<Select2 mode="multiple" placeholder="Select Models">
{userModels.map((modelId) => (
<Select2.Option key={modelId} value={modelId}>
{getModelDisplayName(modelId)}
@@ -228,7 +238,7 @@ const TagInfoView: React.FC<TagInfoViewProps> = ({ tagId, onClose, accessToken,
<Text>{tagDetails.description || "-"}</Text>
</div>
<div>
<Text className="font-medium">Allowed LLMs</Text>
<Text className="font-medium">Allowed Models</Text>
<div className="flex flex-wrap gap-2 mt-2">
{!tagDetails.models || tagDetails.models.length === 0 ? (
<Badge color="red">All Models</Badge>
@@ -256,30 +266,33 @@ const TagInfoView: React.FC<TagInfoViewProps> = ({ tagId, onClose, accessToken,
<Card>
<Title>Budget & Rate Limits</Title>
<div className="space-y-4 mt-4">
{tagDetails.litellm_budget_table.max_budget !== undefined && tagDetails.litellm_budget_table.max_budget !== null && (
<div>
<Text className="font-medium">Max Budget</Text>
<Text>${tagDetails.litellm_budget_table.max_budget}</Text>
</div>
)}
{tagDetails.litellm_budget_table.max_budget !== undefined &&
tagDetails.litellm_budget_table.max_budget !== null && (
<div>
<Text className="font-medium">Max Budget</Text>
<Text>${tagDetails.litellm_budget_table.max_budget}</Text>
</div>
)}
{tagDetails.litellm_budget_table.budget_duration && (
<div>
<Text className="font-medium">Budget Duration</Text>
<Text>{tagDetails.litellm_budget_table.budget_duration}</Text>
</div>
)}
{tagDetails.litellm_budget_table.tpm_limit !== undefined && tagDetails.litellm_budget_table.tpm_limit !== null && (
<div>
<Text className="font-medium">TPM Limit</Text>
<Text>{tagDetails.litellm_budget_table.tpm_limit.toLocaleString()}</Text>
</div>
)}
{tagDetails.litellm_budget_table.rpm_limit !== undefined && tagDetails.litellm_budget_table.rpm_limit !== null && (
<div>
<Text className="font-medium">RPM Limit</Text>
<Text>{tagDetails.litellm_budget_table.rpm_limit.toLocaleString()}</Text>
</div>
)}
{tagDetails.litellm_budget_table.tpm_limit !== undefined &&
tagDetails.litellm_budget_table.tpm_limit !== null && (
<div>
<Text className="font-medium">TPM Limit</Text>
<Text>{tagDetails.litellm_budget_table.tpm_limit.toLocaleString()}</Text>
</div>
)}
{tagDetails.litellm_budget_table.rpm_limit !== undefined &&
tagDetails.litellm_budget_table.rpm_limit !== null && (
<div>
<Text className="font-medium">RPM Limit</Text>
<Text>{tagDetails.litellm_budget_table.rpm_limit.toLocaleString()}</Text>
</div>
)}
</div>
</Card>
)}