diff --git a/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx b/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx index 7259f763ca..488913a734 100644 --- a/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx +++ b/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx @@ -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 ( - {getValue() as string} - ), + cell: ({ getValue }) => {getValue() as string}, }, { 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 ( - - {displayName} - - ); + + return {displayName}; }, }, { @@ -49,11 +43,7 @@ export const searchToolColumns = ( sortingFn: "datetime", cell: ({ row }) => { const tool = row.original; - return ( - - {tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"} - - ); + return {tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}; }, }, { @@ -62,11 +52,7 @@ export const searchToolColumns = ( sortingFn: "datetime", cell: ({ row }) => { const tool = row.original; - return ( - - {tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"} - - ); + return {tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}; }, }, { @@ -90,4 +76,3 @@ export const searchToolColumns = ( ), }, ]; - diff --git a/ui/litellm-dashboard/src/components/useful_links_management.test.tsx b/ui/litellm-dashboard/src/components/useful_links_management.test.tsx index b70d536359..7b1a40a499 100644 --- a/ui/litellm-dashboard/src/components/useful_links_management.test.tsx +++ b/ui/litellm-dashboard/src/components/useful_links_management.test.tsx @@ -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(); + + 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"); + }); }); diff --git a/ui/litellm-dashboard/src/components/useful_links_management.tsx b/ui/litellm-dashboard/src/components/useful_links_management.tsx index 8ea1655b1e..a9367812f4 100644 --- a/ui/litellm-dashboard/src/components/useful_links_management.tsx +++ b/ui/litellm-dashboard/src/components/useful_links_management.tsx @@ -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 = ({ accessTok const [editingLink, setEditingLink] = useState(null); const [loading, setLoading] = useState(false); const [isExpanded, setIsExpanded] = useState(true); + const [isRearranging, setIsRearranging] = useState(false); + const [originalLinksOrder, setOriginalLinksOrder] = useState([]); const fetchUsefulLinks = async () => { if (!accessToken) return; @@ -187,6 +190,42 @@ const UsefulLinksManagement: React.FC = ({ 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 (
setIsExpanded(!isExpanded)}> @@ -252,7 +291,32 @@ const UsefulLinksManagement: React.FC = ({ accessTok
- Manage Existing Links +
+ Manage Existing Links + {!isRearranging ? ( + + ) : ( +
+ + +
+ )} +
@@ -264,7 +328,7 @@ const UsefulLinksManagement: React.FC = ({ accessTok - {links.map((link) => ( + {links.map((link, index) => ( {editingLink && editingLink.id === link.id ? ( <> @@ -316,26 +380,47 @@ const UsefulLinksManagement: React.FC = ({ accessTok {link.displayName} {link.url} -
- - - -
+ {isRearranging ? ( +
+ handleMoveUp(index)} + tooltipText="Move up" + disabled={index === 0} + disabledTooltipText="Already at the top" + dataTestId={`move-up-${link.id}`} + /> + handleMoveDown(index)} + tooltipText="Move down" + disabled={index === links.length - 1} + disabledTooltipText="Already at the bottom" + dataTestId={`move-down-${link.id}`} + /> +
+ ) : ( +
+ setCurrentLink(link.url)} + tooltipText="Open link" + dataTestId={`open-link-${link.id}`} + /> + handleEditLink(link)} + tooltipText="Edit link" + dataTestId={`edit-link-${link.id}`} + /> + deleteLink(link.id)} + tooltipText="Delete link" + dataTestId={`delete-link-${link.id}`} + /> +
+ )}
)}