refactor(ui): migrate router settings and shared badges off antd and tremor

Replaces Ant Design and Tremor in the fallbacks views, the router general
settings panel, and the two shared banner and badge components.

- Tremor Card, Table and Icon become the ui/card, ui/table and lucide
  equivalents, reproducing Tremor's icon box so click targets keep their size
- antd Alert becomes a composed role="alert" region, since the shadcn CLI's
  alert pulls in class-variance-authority, which this repo does not have
- antd InputNumber becomes a native number input, and Switch onChange becomes
  onCheckedChange
- shadcn TableCell ships whitespace-nowrap where Tremor's did not, so cells
  holding model names and setting descriptions get whitespace-normal back
- adds a DeprecationBanner test covering naming, the link, and dismissal,
  proven against the antd version first and mutation checked
- drops the eslint suppressions these files no longer need
This commit is contained in:
Yuneng Jiang 2026-08-14 03:52:41 -07:00
parent 423b791ee0
commit 3465ba4914
No known key found for this signature in database
8 changed files with 306 additions and 213 deletions

View file

@ -1405,9 +1405,6 @@
"no-nested-ternary": {
"count": 1
},
"no-restricted-imports": {
"count": 2
},
"prefer-const": {
"count": 2
}
@ -1761,11 +1758,6 @@
"count": 1
}
},
"src/components/BetaBadge.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/CloudZeroCostTracking/CloudZeroCreateModal.tsx": {
"no-restricted-imports": {
"count": 1
@ -1789,11 +1781,6 @@
"count": 1
}
},
"src/components/DeprecationBanner.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/EntityUsageExport/ExportSummary.tsx": {
"no-restricted-imports": {
"count": 1
@ -1995,11 +1982,6 @@
"count": 1
}
},
"src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx": {
"local/no-complex-jsx-arrow": {
"count": 1
@ -2017,9 +1999,6 @@
}
},
"src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx": {
"no-restricted-imports": {
"count": 2
},
"prefer-const": {
"count": 2
}

View file

@ -62,6 +62,8 @@ const settingsRow = async (fieldName: string) => {
return row as HTMLElement;
};
const numericValueIn = (row: HTMLElement) => Number((within(row).getByRole("spinbutton") as HTMLInputElement).value);
describe("GeneralSettings General tab", () => {
beforeEach(() => {
vi.mocked(getGeneralSettingsCall).mockResolvedValue([...SETTINGS_FIXTURE.map((s) => ({ ...s }))]);
@ -87,7 +89,7 @@ describe("GeneralSettings General tab", () => {
await user.click(screen.getByText("General"));
const row = await settingsRow("max_ui_session_budget");
expect(within(row).getByRole("spinbutton")).toHaveValue("7.50");
expect(numericValueIn(row)).toBe(7.5);
const actionCell = row.querySelectorAll("td")[3];
const resetIcon = actionCell.querySelector("svg");
@ -95,7 +97,7 @@ describe("GeneralSettings General tab", () => {
await user.click(resetIcon as unknown as Element);
expect(deleteConfigFieldSetting).toHaveBeenCalledWith("token", "max_ui_session_budget");
expect(within(row).getByRole("spinbutton")).toHaveValue("1.00");
expect(numericValueIn(row)).toBe(1);
});
});

View file

@ -1,22 +1,14 @@
import React, { useState, useEffect } from "react";
import {
Card,
Table,
TableHead,
TableRow,
TableHeaderCell,
TableCell,
TableBody,
Title,
Text,
Button,
Icon,
Switch,
} from "@tremor/react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "@/components/networking";
import { InputNumber, Select as AntdSelect } from "antd";
import { TrashIcon } from "@heroicons/react/outline";
import { Trash2 } from "lucide-react";
import { StatusBadge } from "@/components/shared/table_cells";
import RouterSettings from "@/components/router_settings";
@ -44,16 +36,22 @@ export interface generalSettingsItem {
field_default_value?: any;
}
const NUMERIC_INPUT_WIDTH = "w-36";
const toNumericValue = (raw: string): number | null => (raw === "" ? null : Number(raw));
const SettingValueEditor: React.FC<{
setting: generalSettingsItem;
onChange: (fieldName: string, newValue: any) => void;
}> = ({ setting, onChange }) => {
if (setting.field_type === "Integer") {
return (
<InputNumber
<Input
type="number"
step={1}
value={setting.field_value}
onChange={(newValue) => onChange(setting.field_name, newValue)}
className={NUMERIC_INPUT_WIDTH}
value={setting.field_value ?? ""}
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
/>
);
}
@ -61,42 +59,55 @@ const SettingValueEditor: React.FC<{
return (
<Switch
checked={setting.field_value === true || setting.field_value === "true"}
onChange={(checked) => onChange(setting.field_name, checked)}
onCheckedChange={(checked) => onChange(setting.field_name, checked)}
/>
);
}
if (setting.field_type === "Float") {
return (
<InputNumber
<Input
type="number"
min={0}
max={1}
step={0.05}
value={setting.field_value}
onChange={(newValue) => onChange(setting.field_name, newValue)}
className={NUMERIC_INPUT_WIDTH}
value={setting.field_value ?? ""}
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
/>
);
}
if (setting.field_type === "Dollar") {
return (
<InputNumber
min={0.01}
step={0.25}
prefix="$"
value={setting.field_value}
onChange={(newValue) => onChange(setting.field_name, newValue)}
/>
<InputGroup className={NUMERIC_INPUT_WIDTH}>
<InputGroupAddon>$</InputGroupAddon>
<InputGroupInput
type="number"
min={0.01}
step={0.25}
value={setting.field_value ?? ""}
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
/>
</InputGroup>
);
}
if (setting.field_type === "Select") {
return (
<AntdSelect
allowClear
style={{ minWidth: "8rem" }}
placeholder="Default"
value={setting.field_value || undefined}
options={(setting.field_options ?? []).map((option) => ({ label: option, value: option }))}
onChange={(newValue) => onChange(setting.field_name, newValue ?? "")}
/>
<Select
value={setting.field_value || null}
onValueChange={(newValue) => onChange(setting.field_name, newValue ?? "")}
>
<SelectTrigger className="min-w-32">
<SelectValue placeholder="Default" />
</SelectTrigger>
<SelectContent>
<SelectItem value={null}>Default</SelectItem>
{(setting.field_options ?? []).map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
return null;
@ -131,33 +142,43 @@ export const PromptCachingPanel: React.FC<{
return (
<Card>
<Title>Prompt Caching</Title>
<CardContent>
<CardTitle>Prompt Caching</CardTitle>
<div className="mt-6 flex items-start justify-between gap-8">
<div className="max-w-2xl">
<Text className="font-medium">Automatic Anthropic prompt caching</Text>
<p className="mt-1 text-xs text-gray-500">{enableSetting.field_description}</p>
</div>
<Switch checked={enabled} onChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
</div>
{ttlSetting && (
<div className="mt-6 flex items-start justify-between gap-8">
<div className="max-w-2xl">
<Text className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</Text>
<p className="mt-1 text-xs text-gray-500">{ttlSetting.field_description}</p>
<div className="min-w-0 max-w-2xl">
<p className="font-medium">Automatic Anthropic prompt caching</p>
<p className="mt-1 break-words text-xs text-gray-500">{enableSetting.field_description}</p>
</div>
<AntdSelect
allowClear
disabled={!enabled}
style={{ minWidth: "10rem" }}
placeholder="5m (default)"
value={ttlSetting.field_value || undefined}
options={(ttlSetting.field_options ?? []).map((option) => ({ label: option, value: option }))}
onChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")}
/>
<Switch checked={enabled} onCheckedChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
</div>
)}
{ttlSetting && (
<div className="mt-6 flex items-start justify-between gap-8">
<div className="min-w-0 max-w-2xl">
<p className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</p>
<p className="mt-1 break-words text-xs text-gray-500">{ttlSetting.field_description}</p>
</div>
<Select
disabled={!enabled}
value={ttlSetting.field_value || null}
onValueChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")}
>
<SelectTrigger className="min-w-40">
<SelectValue placeholder="5m (default)" />
</SelectTrigger>
<SelectContent>
<SelectItem value={null}>5m (default)</SelectItem>
{(ttlSetting.field_options ?? []).map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</CardContent>
</Card>
);
};
@ -254,55 +275,60 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
</TabsContent>
<TabsContent value="general" className="px-8 py-6">
<Card>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Setting</TableHeaderCell>
<TableHeaderCell>Value</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Action</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{generalSettings
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
.map((value, index) => (
<TableRow key={index}>
<TableCell>
<Text>{value.field_name}</Text>
<p
style={{
fontSize: "0.65rem",
color: "#808080",
fontStyle: "italic",
}}
className="mt-1"
>
{value.field_description}
</p>
</TableCell>
<TableCell>
<SettingValueEditor setting={value} onChange={handleInputChange} />
</TableCell>
<TableCell>
{value.stored_in_db == true ? (
<StatusBadge tone="success" label="In DB" />
) : value.stored_in_db == false ? (
<StatusBadge tone="neutral" label="In Config" />
) : (
<StatusBadge tone="neutral" label="Not Set" />
)}
</TableCell>
<TableCell>
<Button onClick={() => handleUpdateField(value.field_name)}>Update</Button>
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name)}>
Reset
</Icon>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Setting</TableHead>
<TableHead>Value</TableHead>
<TableHead>Status</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{generalSettings
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
.map((value, index) => (
<TableRow key={index}>
<TableCell className="whitespace-normal">
<p className="break-words">{value.field_name}</p>
<p
style={{
fontSize: "0.65rem",
color: "#808080",
fontStyle: "italic",
}}
className="mt-1 break-words"
>
{value.field_description}
</p>
</TableCell>
<TableCell>
<SettingValueEditor setting={value} onChange={handleInputChange} />
</TableCell>
<TableCell>
{value.stored_in_db == true ? (
<StatusBadge tone="success" label="In DB" />
) : value.stored_in_db == false ? (
<StatusBadge tone="neutral" label="In Config" />
) : (
<StatusBadge tone="neutral" label="Not Set" />
)}
</TableCell>
<TableCell>
<Button onClick={() => handleUpdateField(value.field_name)}>Update</Button>
<span
onClick={() => handleResetField(value.field_name)}
className="inline-flex shrink-0 cursor-pointer items-center justify-center px-1.5 py-1.5 text-red-500"
>
<Trash2 className="h-5 w-5 shrink-0" />
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>

View file

@ -1,4 +1,4 @@
import { Badge } from "antd";
import { Badge } from "@/components/ui/badge";
import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge";
export default function BetaBadge({ children, dot = false }: { children?: React.ReactNode; dot?: boolean }) {
@ -8,11 +8,14 @@ export default function BetaBadge({ children, dot = false }: { children?: React.
return children ? <>{children}</> : null;
}
const badge = dot ? <Badge className="size-1.5 p-0" /> : <Badge>Beta</Badge>;
return children ? (
<Badge color="blue" count={dot ? undefined : "Beta"} dot={dot}>
<span className="inline-flex items-center gap-1.5">
{children}
</Badge>
{badge}
</span>
) : (
<Badge color="blue" count={dot ? undefined : "Beta"} dot={dot} />
badge
);
}

View file

@ -0,0 +1,44 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { DeprecationBanner } from "./DeprecationBanner";
describe("DeprecationBanner", () => {
it("names the deprecated feature in the heading and the body", () => {
render(<DeprecationBanner featureName="Memory" />);
expect(screen.getByText("Memory is on a draft deprecation list")).toBeInTheDocument();
expect(screen.getByText(/Memory is one of several experimental features/)).toBeInTheDocument();
});
it("states the target removal date and that the list is not final", () => {
render(<DeprecationBanner featureName="Memory" />);
expect(screen.getByText(/as early as September 1, 2026/)).toBeInTheDocument();
expect(screen.getByText(/This list is a draft and is not final/)).toBeInTheDocument();
});
it("links to the deprecation discussion in a new tab without leaking the opener", () => {
render(<DeprecationBanner featureName="Memory" />);
const link = screen.getByRole("link", { name: "deprecation discussion" });
expect(link).toHaveAttribute("href", "https://github.com/BerriAI/litellm/discussions/32090");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
it("exposes a named close control", () => {
render(<DeprecationBanner featureName="Memory" />);
expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument();
});
it("hides the banner once the close control is used", async () => {
const user = userEvent.setup();
render(<DeprecationBanner featureName="Memory" />);
await user.click(screen.getByRole("button", { name: /close/i }));
expect(screen.queryByText("Memory is on a draft deprecation list")).not.toBeInTheDocument();
});
});

View file

@ -1,8 +1,8 @@
"use client";
import React from "react";
import React, { useState } from "react";
import Link from "next/link";
import { Alert } from "antd";
import { Info, X } from "lucide-react";
const DEPRECATION_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/32090";
const DEPRECATION_TARGET_DATE = "September 1, 2026";
@ -11,21 +11,42 @@ interface DeprecationBannerProps {
featureName: string;
}
export const DeprecationBanner: React.FC<DeprecationBannerProps> = ({ featureName }) => (
<Alert
message={`${featureName} is on a draft deprecation list`}
description={
<>
{`${featureName} is one of several experimental features we're considering removing, potentially as early as ${DEPRECATION_TARGET_DATE}. This list is a draft and is not final. If you rely on this feature, please share feedback on the `}
<Link href={DEPRECATION_DISCUSSION_URL} target="_blank" rel="noopener noreferrer">
deprecation discussion
</Link>
.
</>
}
type="info"
showIcon
closable
style={{ marginBottom: 16 }}
/>
);
export const DeprecationBanner: React.FC<DeprecationBannerProps> = ({ featureName }) => {
const [isClosed, setIsClosed] = useState(false);
if (isClosed) {
return null;
}
return (
<div
role="alert"
className="mb-4 flex items-start gap-3 rounded-lg border border-border bg-muted/50 px-4 py-3 text-sm"
>
<Info className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="font-medium">{`${featureName} is on a draft deprecation list`}</p>
<p className="mt-1 break-words text-muted-foreground">
{`${featureName} is one of several experimental features we're considering removing, potentially as early as ${DEPRECATION_TARGET_DATE}. This list is a draft and is not final. If you rely on this feature, please share feedback on the `}
<Link
href={DEPRECATION_DISCUSSION_URL}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-4"
>
deprecation discussion
</Link>
.
</p>
</div>
<button
type="button"
aria-label="Close"
onClick={() => setIsClosed(true)}
className="shrink-0 rounded-md p-0.5 text-muted-foreground transition-colors hover:text-foreground"
>
<X className="size-4" />
</button>
</div>
);
};

View file

@ -4,9 +4,9 @@
* Reuses FallbackGroupConfig with the primary model locked
*/
import { Button } from "antd";
import { Button } from "@/components/ui/button";
import { useQuery } from "@tanstack/react-query";
import { Pencil } from "lucide-react";
import { LoaderCircle, Pencil } from "lucide-react";
import React, { useMemo, useState } from "react";
import { fetchAvailableModels } from "@/components/llm_calls/fetch_models";
import NotificationManager from "../../../molecules/notifications_manager";
@ -88,16 +88,11 @@ export default function EditFallbacks({
disablePrimaryModel
/>
<div className="flex items-center justify-end space-x-3 pt-6 mt-6 border-t border-gray-100">
<Button type="default" onClick={onClose} disabled={isSaving}>
<Button variant="outline" onClick={onClose} disabled={isSaving}>
Cancel
</Button>
<Button
type="primary"
icon={<Pencil className="w-4 h-4" />}
onClick={handleSave}
disabled={isSaving || group.fallbackModels.length === 0}
loading={isSaving}
>
<Button onClick={handleSave} disabled={isSaving || group.fallbackModels.length === 0}>
{isSaving ? <LoaderCircle className="w-4 h-4 animate-spin" /> : <Pencil className="w-4 h-4" />}
{isSaving ? "Saving Changes..." : "Save Changes"}
</Button>
</div>

View file

@ -1,7 +1,7 @@
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
import { ArrowRightIcon, PencilAltIcon, PlayIcon, TrashIcon } from "@heroicons/react/outline";
import { Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
import { Tooltip, Typography } from "antd";
import { ArrowRight, Pencil, Play, Trash2 } from "lucide-react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import openai from "openai";
import React, { useEffect, useState } from "react";
import DeleteResourceModal from "../../../common_components/DeleteResourceModal";
@ -18,12 +18,14 @@ type Fallbacks = FallbackEntry[];
const modelCardClass =
"inline-flex items-center gap-2 px-2.5 py-1 rounded-md border border-gray-200 bg-gray-50 text-sm font-medium text-gray-800 shrink-0";
const iconWrapperClass = "inline-flex shrink-0 items-center justify-center px-1.5 py-1.5";
function renderModelNameCell(modelName: string, getProviderFromModel?: (modelName: string) => string): React.ReactNode {
const provider = getProviderFromModel?.(modelName) ?? modelName;
return (
<span className={modelCardClass}>
<ProviderLogo provider={provider} className="w-4 h-4 shrink-0" />
<span>{modelName}</span>
<span className="break-words">{modelName}</span>
</span>
);
}
@ -41,19 +43,23 @@ function renderFallbacksChain(
return (
<span className={modelCardClass}>
<ProviderLogo provider={provider} className="w-4 h-4 shrink-0" />
<span>{modelName}</span>
<span className="break-words">{modelName}</span>
</span>
);
};
return (
<span className="grid grid-cols-[auto_1fr] items-start gap-x-2 w-full min-w-0">
<span className="inline-flex items-center justify-center w-8 h-8 shrink-0 self-start text-blue-600" aria-hidden>
<ArrowRightIcon className="w-5 h-5 stroke-[2.5]" />
<ArrowRight className="w-5 h-5 stroke-[2.5]" />
</span>
<span className="flex flex-wrap items-start gap-1 min-w-0">
{list.map((model, i) => (
<React.Fragment key={model}>
{i > 0 && <Icon icon={ArrowRightIcon} size="xs" className="shrink-0 text-gray-400" />}
{i > 0 && (
<span className={`${iconWrapperClass} text-gray-400`}>
<ArrowRight className="h-3 w-3 shrink-0" />
</span>
)}
<ChainCard modelName={model} />
</React.Fragment>
))}
@ -248,7 +254,7 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
const canModify = isProxyAdminRole(userRole ?? "");
return (
<>
<TooltipProvider>
{canModify && (
<AddFallbacks
accessToken={accessToken || ""}
@ -258,62 +264,79 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
)}
{!hasFallbacks ? (
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-6 text-center">
<Typography.Text type="secondary">
<span className="text-muted-foreground">
No fallbacks configured. Add fallbacks to automatically try another model when the primary fails.
</Typography.Text>
</span>
</div>
) : (
<Table>
<TableHead>
<TableHeader>
<TableRow>
<TableHeaderCell>Model Name</TableHeaderCell>
<TableHeaderCell>Fallbacks</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
<TableHead>Model Name</TableHead>
<TableHead>Fallbacks</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHead>
</TableHeader>
<TableBody>
{routerSettings["fallbacks"].map((item: FallbackEntry, index: number) =>
Object.entries(item).map(([key, value]) => (
<TableRow key={index.toString() + key}>
<TableCell className="align-top">{renderModelNameCell(key, getProviderFromModel)}</TableCell>
<TableCell className="align-top">
<TableCell className="align-top whitespace-normal">
{renderModelNameCell(key, getProviderFromModel)}
</TableCell>
<TableCell className="align-top whitespace-normal">
{renderFallbacksChain(key, Array.isArray(value) ? value : [], getProviderFromModel)}
</TableCell>
<TableCell className="align-top">
{canModify && (
<>
<Tooltip title="Test fallback">
<Icon
icon={PlayIcon}
size="sm"
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "")}
className="cursor-pointer hover:text-blue-600"
/>
</Tooltip>
<Tooltip title="Edit fallback">
<span
data-testid="edit-fallback-button"
role="button"
tabIndex={0}
onClick={() => handleEditClick(item)}
onKeyDown={(e) => e.key === "Enter" && handleEditClick(item)}
className="cursor-pointer inline-flex"
<Tooltip>
<TooltipTrigger
render={
<span
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "")}
className={`${iconWrapperClass} cursor-pointer hover:text-blue-600`}
/>
}
>
<Icon icon={PencilAltIcon} size="sm" className="hover:text-blue-600" />
</span>
<Play className="h-5 w-5 shrink-0" />
</TooltipTrigger>
<TooltipContent>Test fallback</TooltipContent>
</Tooltip>
<Tooltip title="Delete fallback">
<span
data-testid="delete-fallback-button"
role="button"
tabIndex={0}
onClick={() => handleDeleteClick(item)}
onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)}
className="cursor-pointer inline-flex"
<Tooltip>
<TooltipTrigger
render={
<span
data-testid="edit-fallback-button"
role="button"
tabIndex={0}
onClick={() => handleEditClick(item)}
onKeyDown={(e) => e.key === "Enter" && handleEditClick(item)}
className={`${iconWrapperClass} cursor-pointer hover:text-blue-600`}
/>
}
>
<Icon icon={TrashIcon} size="sm" className="hover:text-red-600" />
</span>
<Pencil className="h-5 w-5 shrink-0" />
</TooltipTrigger>
<TooltipContent>Edit fallback</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<span
data-testid="delete-fallback-button"
role="button"
tabIndex={0}
onClick={() => handleDeleteClick(item)}
onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)}
className={`${iconWrapperClass} cursor-pointer hover:text-red-600`}
/>
}
>
<Trash2 className="h-5 w-5 shrink-0" />
</TooltipTrigger>
<TooltipContent>Delete fallback</TooltipContent>
</Tooltip>
</>
)}
@ -350,7 +373,7 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
onOk={handleDeleteConfirm}
confirmLoading={isDeleting}
/>
</>
</TooltipProvider>
);
};