Rearrange Links UI

This commit is contained in:
yuneng-jiang
2025-12-11 20:23:02 -08:00
parent 4a8629ce8b
commit 7ea407b239
4 changed files with 161 additions and 45 deletions
@@ -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>
</>
)}