mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-16 20:18:21 +00:00
Rearrange Links UI
This commit is contained in:
+12
-1
@@ -1,4 +1,12 @@
|
||||
import { PencilAltIcon, PlayIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
PencilAltIcon,
|
||||
PlayIcon,
|
||||
RefreshIcon,
|
||||
TrashIcon,
|
||||
ChevronUpIcon,
|
||||
ChevronDownIcon,
|
||||
ExternalLinkIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { Tooltip } from "antd";
|
||||
import BaseActionButton from "../BaseActionButton";
|
||||
|
||||
@@ -21,6 +29,9 @@ export const TableIconActionButtonMap: Record<string, TableIconActionButtonBaseP
|
||||
Delete: { icon: TrashIcon, className: "hover:text-red-600" },
|
||||
Test: { icon: PlayIcon, className: "hover:text-blue-600" },
|
||||
Regenerate: { icon: RefreshIcon, className: "hover:text-green-600" },
|
||||
Up: { icon: ChevronUpIcon, className: "hover:text-blue-600" },
|
||||
Down: { icon: ChevronDownIcon, className: "hover:text-blue-600" },
|
||||
Open: { icon: ExternalLinkIcon, className: "hover:text-green-600" },
|
||||
};
|
||||
|
||||
export default function TableIconActionButton({
|
||||
|
||||
@@ -24,23 +24,17 @@ export const searchToolColumns = (
|
||||
{
|
||||
accessorKey: "search_tool_name",
|
||||
header: "Name",
|
||||
cell: ({ getValue }) => (
|
||||
<span className="font-medium">{getValue() as string}</span>
|
||||
),
|
||||
cell: ({ getValue }) => <span className="font-medium">{getValue() as string}</span>,
|
||||
},
|
||||
{
|
||||
id: "provider",
|
||||
header: "Provider",
|
||||
cell: ({ row }) => {
|
||||
const provider = row.original.litellm_params.search_provider;
|
||||
const providerInfo = availableProviders.find(p => p.provider_name === provider);
|
||||
const providerInfo = availableProviders.find((p) => p.provider_name === provider);
|
||||
const displayName = providerInfo?.ui_friendly_name || provider;
|
||||
|
||||
return (
|
||||
<span className="text-sm">
|
||||
{displayName}
|
||||
</span>
|
||||
);
|
||||
|
||||
return <span className="text-sm">{displayName}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -49,11 +43,7 @@ export const searchToolColumns = (
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => {
|
||||
const tool = row.original;
|
||||
return (
|
||||
<span className="text-xs">
|
||||
{tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}
|
||||
</span>
|
||||
);
|
||||
return <span className="text-xs">{tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -62,11 +52,7 @@ export const searchToolColumns = (
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => {
|
||||
const tool = row.original;
|
||||
return (
|
||||
<span className="text-xs">
|
||||
{tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}
|
||||
</span>
|
||||
);
|
||||
return <span className="text-xs">{tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -90,4 +76,3 @@ export const searchToolColumns = (
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -71,4 +71,39 @@ describe("UsefulLinksManagement", () => {
|
||||
expect(screen.getByText("https://docs.example.com")).toBeInTheDocument();
|
||||
expect(mockedNotifications.success).toHaveBeenCalledWith("Link added successfully");
|
||||
});
|
||||
|
||||
it("should rearrange links and save the new order", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockedGetPublicModelHubInfo.mockResolvedValue({
|
||||
docs_title: "Docs",
|
||||
custom_docs_description: null,
|
||||
litellm_version: "1.0.0",
|
||||
useful_links: {
|
||||
"First Link": "https://first.example.com",
|
||||
"Second Link": "https://second.example.com",
|
||||
"Third Link": "https://third.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
render(<UsefulLinksManagement accessToken="token" userRole="Admin" />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText("First Link")).toBeInTheDocument());
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /rearrange order/i }));
|
||||
|
||||
const secondLinkMoveUpButton = screen.getByTestId("move-up-1-Second Link");
|
||||
await user.click(secondLinkMoveUpButton);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /save order/i }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedUpdateUsefulLinksCall).toHaveBeenCalledWith("token", {
|
||||
"Second Link": "https://second.example.com",
|
||||
"First Link": "https://first.example.com",
|
||||
"Third Link": "https://third.example.com",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockedNotifications.success).toHaveBeenCalledWith("Link order saved successfully");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal } from "antd";
|
||||
import { PlusCircleIcon, PencilIcon, TrashIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { PlusCircleIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { isAdminRole } from "../utils/roles";
|
||||
import { getPublicModelHubInfo, updateUsefulLinksCall, getProxyBaseUrl } from "./networking";
|
||||
import { Card, Title, Text, Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
|
||||
|
||||
interface UsefulLinksManagementProps {
|
||||
accessToken: string | null;
|
||||
@@ -23,6 +24,8 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
||||
const [editingLink, setEditingLink] = useState<Link | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [isRearranging, setIsRearranging] = useState(false);
|
||||
const [originalLinksOrder, setOriginalLinksOrder] = useState<Link[]>([]);
|
||||
|
||||
const fetchUsefulLinks = async () => {
|
||||
if (!accessToken) return;
|
||||
@@ -187,6 +190,42 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
|
||||
const handleStartRearranging = () => {
|
||||
if (editingLink) {
|
||||
setEditingLink(null);
|
||||
}
|
||||
setOriginalLinksOrder([...links]);
|
||||
setIsRearranging(true);
|
||||
};
|
||||
|
||||
const handleCancelRearranging = () => {
|
||||
setLinks([...originalLinksOrder]);
|
||||
setIsRearranging(false);
|
||||
setOriginalLinksOrder([]);
|
||||
};
|
||||
|
||||
const handleSaveRearranging = async () => {
|
||||
if (await saveLinksToBackend(links)) {
|
||||
setIsRearranging(false);
|
||||
setOriginalLinksOrder([]);
|
||||
NotificationsManager.success("Link order saved successfully");
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveUp = (index: number) => {
|
||||
if (index === 0) return;
|
||||
const newLinks = [...links];
|
||||
[newLinks[index - 1], newLinks[index]] = [newLinks[index], newLinks[index - 1]];
|
||||
setLinks(newLinks);
|
||||
};
|
||||
|
||||
const handleMoveDown = (index: number) => {
|
||||
if (index === links.length - 1) return;
|
||||
const newLinks = [...links];
|
||||
[newLinks[index], newLinks[index + 1]] = [newLinks[index + 1], newLinks[index]];
|
||||
setLinks(newLinks);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mb-6">
|
||||
<div className="flex items-center justify-between cursor-pointer" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
@@ -252,7 +291,32 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">Manage Existing Links</Text>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm font-medium text-gray-700">Manage Existing Links</Text>
|
||||
{!isRearranging ? (
|
||||
<button
|
||||
onClick={handleStartRearranging}
|
||||
className="text-xs bg-purple-50 text-purple-600 px-3 py-1.5 rounded hover:bg-purple-100 flex items-center"
|
||||
>
|
||||
Rearrange Order
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={handleSaveRearranging}
|
||||
className="text-xs bg-green-600 text-white px-3 py-1.5 rounded hover:bg-green-700"
|
||||
>
|
||||
Save Order
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelRearranging}
|
||||
className="text-xs bg-gray-50 text-gray-600 px-3 py-1.5 rounded hover:bg-gray-100"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
@@ -264,7 +328,7 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{links.map((link) => (
|
||||
{links.map((link, index) => (
|
||||
<TableRow key={link.id} className="h-8">
|
||||
{editingLink && editingLink.id === link.id ? (
|
||||
<>
|
||||
@@ -316,26 +380,47 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
||||
<TableCell className="py-0.5 text-sm text-gray-900">{link.displayName}</TableCell>
|
||||
<TableCell className="py-0.5 text-sm text-gray-500">{link.url}</TableCell>
|
||||
<TableCell className="py-0.5 whitespace-nowrap">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setCurrentLink(link.url)}
|
||||
className="text-xs bg-green-50 text-green-600 px-2 py-1 rounded hover:bg-green-100"
|
||||
>
|
||||
Use
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEditLink(link)}
|
||||
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
|
||||
>
|
||||
<PencilIcon className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteLink(link.id)}
|
||||
className="text-xs bg-red-50 text-red-600 px-2 py-1 rounded hover:bg-red-100"
|
||||
>
|
||||
<TrashIcon className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
{isRearranging ? (
|
||||
<div className="flex space-x-2">
|
||||
<TableIconActionButton
|
||||
variant="Up"
|
||||
onClick={() => handleMoveUp(index)}
|
||||
tooltipText="Move up"
|
||||
disabled={index === 0}
|
||||
disabledTooltipText="Already at the top"
|
||||
dataTestId={`move-up-${link.id}`}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Down"
|
||||
onClick={() => handleMoveDown(index)}
|
||||
tooltipText="Move down"
|
||||
disabled={index === links.length - 1}
|
||||
disabledTooltipText="Already at the bottom"
|
||||
dataTestId={`move-down-${link.id}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex space-x-2">
|
||||
<TableIconActionButton
|
||||
variant="Open"
|
||||
onClick={() => setCurrentLink(link.url)}
|
||||
tooltipText="Open link"
|
||||
dataTestId={`open-link-${link.id}`}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
onClick={() => handleEditLink(link)}
|
||||
tooltipText="Edit link"
|
||||
dataTestId={`edit-link-${link.id}`}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
onClick={() => deleteLink(link.id)}
|
||||
tooltipText="Delete link"
|
||||
dataTestId={`delete-link-${link.id}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user