mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
Merge pull request #41644 from BerriAI/litellm_remove_dead_helplink_helpicon
refactor(ui): remove unused HelpLink and HelpIcon components
This commit is contained in:
commit
57a59889ac
7 changed files with 139 additions and 349 deletions
|
|
@ -40,7 +40,7 @@ vi.mock("@/components/llm_calls/fetch_models", () => ({
|
|||
fetchAvailableModels: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/HelpLink", () => ({
|
||||
vi.mock("@/components/DocsMenu", () => ({
|
||||
DocsMenu: () => null,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ vi.mock("@/components/llm_calls/fetch_models", () => ({
|
|||
fetchAvailableModels: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/HelpLink", () => ({
|
||||
vi.mock("@/components/DocsMenu", () => ({
|
||||
DocsMenu: () => null,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import AddProviderForm from "./add_provider_form";
|
|||
import ProviderMarginTable from "./provider_margin_table";
|
||||
import AddMarginForm from "./add_margin_form";
|
||||
import PricingCalculator from "./pricing_calculator/index";
|
||||
import { DocsMenu } from "@/components/HelpLink";
|
||||
import { DocsMenu } from "@/components/DocsMenu";
|
||||
import HowItWorks from "./how_it_works";
|
||||
import { useDiscountConfig } from "./use_discount_config";
|
||||
import { useMarginConfig } from "./use_margin_config";
|
||||
|
|
|
|||
69
ui/litellm-dashboard/src/components/DocsMenu.test.tsx
Normal file
69
ui/litellm-dashboard/src/components/DocsMenu.test.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import React from "react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../tests/test-utils";
|
||||
import { DocsMenu } from "./DocsMenu";
|
||||
|
||||
describe("DocsMenu", () => {
|
||||
const items = [
|
||||
{ label: "Custom pricing", href: "https://docs.example.com/pricing" },
|
||||
{ label: "Cost tracking", href: "https://docs.example.com/cost" },
|
||||
];
|
||||
|
||||
it("should render the menu button with default text", () => {
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /docs/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide menu items initially", () => {
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
expect(screen.queryByText("Custom pricing")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show menu items when button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /docs/i }));
|
||||
|
||||
expect(screen.getByText("Custom pricing")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cost tracking")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close the menu when an item is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /docs/i }));
|
||||
await user.click(screen.getByText("Custom pricing"));
|
||||
|
||||
expect(screen.queryByText("Cost tracking")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should set aria-expanded correctly based on menu state", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: /docs/i });
|
||||
expect(button).toHaveAttribute("aria-expanded", "false");
|
||||
|
||||
await user.click(button);
|
||||
expect(button).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
it("should close menu when clicking outside", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<div>
|
||||
<DocsMenu items={items} />
|
||||
<button>Outside</button>
|
||||
</div>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: /docs/i }));
|
||||
expect(screen.getByText("Custom pricing")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: /outside/i }));
|
||||
expect(screen.queryByText("Custom pricing")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
67
ui/litellm-dashboard/src/components/DocsMenu.tsx
Normal file
67
ui/litellm-dashboard/src/components/DocsMenu.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { ExternalLink, ChevronDown } from "lucide-react";
|
||||
|
||||
interface DocMenuItem {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface DocsMenuProps {
|
||||
items: DocMenuItem[];
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DocsMenu: React.FC<DocsMenuProps> = ({ items, children = "Docs", className = "" }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div className={`relative inline-block ${className}`} ref={menuRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="inline-flex items-center gap-1 text-muted-foreground hover:text-foreground text-xs transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-1 rounded-sm px-2 py-1"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<span>{children}</span>
|
||||
<ChevronDown className={`h-3 w-3 transition-transform ${isOpen ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-1 w-56 bg-card rounded-lg shadow-lg border border-border py-1 z-floating">
|
||||
{items.map((item, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between px-4 py-2 text-sm text-foreground hover:bg-accent transition-colors"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground shrink-0 ml-2" aria-hidden="true" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
import React from "react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../tests/test-utils";
|
||||
import { HelpLink, HelpIcon, DocsMenu } from "./HelpLink";
|
||||
|
||||
describe("HelpLink", () => {
|
||||
it("should render with default children and open in new tab", () => {
|
||||
renderWithProviders(<HelpLink href="https://docs.example.com" />);
|
||||
|
||||
const link = screen.getByRole("link", { name: /learn more/i });
|
||||
expect(link).toHaveAttribute("href", "https://docs.example.com");
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
it("should render custom children text", () => {
|
||||
renderWithProviders(<HelpLink href="https://docs.example.com">Custom docs link</HelpLink>);
|
||||
|
||||
expect(screen.getByText("Custom docs link")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have the correct href", () => {
|
||||
renderWithProviders(<HelpLink href="https://docs.example.com/test" />);
|
||||
expect(screen.getByRole("link")).toHaveAttribute("href", "https://docs.example.com/test");
|
||||
});
|
||||
|
||||
it("should include a screen-reader-only label for accessibility", () => {
|
||||
renderWithProviders(<HelpLink href="https://docs.example.com" />);
|
||||
|
||||
expect(screen.getByText("(opens in a new tab)")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("HelpIcon", () => {
|
||||
it("should render a help button with accessible label", () => {
|
||||
renderWithProviders(<HelpIcon content="Some help text" />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /help information/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show tooltip content on hover", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<HelpIcon content="Tooltip help text" />);
|
||||
|
||||
await user.hover(screen.getByRole("button", { name: /help information/i }));
|
||||
|
||||
expect(screen.getByText("Tooltip help text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide tooltip content when not hovered", () => {
|
||||
renderWithProviders(<HelpIcon content="Hidden tooltip" />);
|
||||
expect(screen.queryByText("Hidden tooltip")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show learn more link when learnMoreHref is provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<HelpIcon content="Help text" learnMoreHref="https://docs.example.com" />);
|
||||
await user.hover(screen.getByRole("button", { name: /help information/i }));
|
||||
expect(screen.getByText("Learn more")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should use custom learn more text when provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<HelpIcon content="Help text" learnMoreHref="https://docs.example.com" learnMoreText="Read docs" />,
|
||||
);
|
||||
|
||||
await user.hover(screen.getByRole("button", { name: /help information/i }));
|
||||
|
||||
const link = screen.getByRole("link", { name: /read docs/i });
|
||||
expect(link).toHaveAttribute("href", "https://docs.example.com");
|
||||
});
|
||||
|
||||
it("should not show learn more link when learnMoreHref is not provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<HelpIcon content="Help text" />);
|
||||
|
||||
await user.hover(screen.getByRole("button", { name: /help information/i }));
|
||||
|
||||
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DocsMenu", () => {
|
||||
const items = [
|
||||
{ label: "Custom pricing", href: "https://docs.example.com/pricing" },
|
||||
{ label: "Cost tracking", href: "https://docs.example.com/cost" },
|
||||
];
|
||||
|
||||
it("should render the menu button with default text", () => {
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /docs/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide menu items initially", () => {
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
expect(screen.queryByText("Custom pricing")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show menu items when button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /docs/i }));
|
||||
|
||||
expect(screen.getByText("Custom pricing")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cost tracking")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close the menu when an item is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /docs/i }));
|
||||
await user.click(screen.getByText("Custom pricing"));
|
||||
|
||||
expect(screen.queryByText("Cost tracking")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should set aria-expanded correctly based on menu state", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<DocsMenu items={items} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: /docs/i });
|
||||
expect(button).toHaveAttribute("aria-expanded", "false");
|
||||
|
||||
await user.click(button);
|
||||
expect(button).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
it("should close menu when clicking outside", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<div>
|
||||
<DocsMenu items={items} />
|
||||
<button>Outside</button>
|
||||
</div>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: /docs/i }));
|
||||
expect(screen.getByText("Custom pricing")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: /outside/i }));
|
||||
expect(screen.queryByText("Custom pricing")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { ExternalLink, ChevronDown } from "lucide-react";
|
||||
|
||||
interface HelpLinkProps {
|
||||
href: string;
|
||||
children?: React.ReactNode;
|
||||
variant?: "inline" | "subtle" | "button";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface DocMenuItem {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface DocsMenuProps {
|
||||
items: DocMenuItem[];
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reusable component for linking to documentation, styled similar to Linear's help links.
|
||||
*
|
||||
* @example
|
||||
* // Inline "Learn more" style
|
||||
* <HelpLink href="https://docs.litellm.ai/docs/proxy/custom_pricing">
|
||||
* Learn more about custom pricing
|
||||
* </HelpLink>
|
||||
*
|
||||
* @example
|
||||
* // Subtle link (just icon + text, minimal styling)
|
||||
* <HelpLink href="https://docs.litellm.ai/docs/proxy/cost_tracking" variant="subtle">
|
||||
* View docs
|
||||
* </HelpLink>
|
||||
*
|
||||
* @example
|
||||
* // Button style (more prominent)
|
||||
* <HelpLink href="https://docs.litellm.ai/docs/proxy/custom_pricing" variant="button">
|
||||
* Custom Pricing Documentation
|
||||
* </HelpLink>
|
||||
*/
|
||||
export const HelpLink: React.FC<HelpLinkProps> = ({
|
||||
href,
|
||||
children = "Learn more",
|
||||
variant = "inline",
|
||||
className = "",
|
||||
}) => {
|
||||
const baseClasses =
|
||||
"inline-flex items-center gap-1.5 transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-1 rounded-sm";
|
||||
|
||||
const variantClasses = {
|
||||
inline: "text-info text-sm font-medium hover:underline",
|
||||
subtle: "text-muted-foreground hover:text-foreground text-xs",
|
||||
button:
|
||||
"text-info border border-border px-3 py-1.5 rounded-md bg-card hover:bg-accent text-sm font-medium shadow-xs",
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
|
||||
title="Open documentation in a new tab"
|
||||
>
|
||||
<span>{children}</span>
|
||||
<ExternalLink className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
||||
<span className="sr-only">(opens in a new tab)</span>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A minimal help icon with tooltip for inline contextual help.
|
||||
* Similar to Linear's "?" icons that appear next to labels.
|
||||
*/
|
||||
interface HelpIconProps {
|
||||
content: React.ReactNode;
|
||||
learnMoreHref?: string;
|
||||
learnMoreText?: string;
|
||||
}
|
||||
|
||||
export const HelpIcon: React.FC<HelpIconProps> = ({ content, learnMoreHref, learnMoreText = "Learn more" }) => {
|
||||
const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block ml-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center w-4 h-4 text-muted-foreground hover:text-foreground transition-colors cursor-help focus:outline-hidden focus:ring-2 focus:ring-ring rounded-full"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
onFocus={() => setShowTooltip(true)}
|
||||
onBlur={() => setShowTooltip(false)}
|
||||
aria-label="Help information"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" strokeWidth="1.5" />
|
||||
<path strokeLinecap="round" d="M12 17h0M12 13.5a1.5 1.5 0 0 1 1-1.415A1.5 1.5 0 1 0 12 9" strokeWidth="1.5" />
|
||||
</svg>
|
||||
</button>
|
||||
{showTooltip && (
|
||||
<div
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-floating bg-gray-900 text-white p-3 rounded-lg text-xs shadow-lg w-64"
|
||||
style={{ pointerEvents: "none" }}
|
||||
>
|
||||
<div className="mb-2">{content}</div>
|
||||
{learnMoreHref && (
|
||||
<a
|
||||
href={learnMoreHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-info hover:text-blue-200 font-medium"
|
||||
style={{ pointerEvents: "auto" }}
|
||||
>
|
||||
{learnMoreText}
|
||||
<ExternalLink className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
<div
|
||||
className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0"
|
||||
style={{
|
||||
borderTop: "6px solid rgb(17 24 39)",
|
||||
borderLeft: "6px solid transparent",
|
||||
borderRight: "6px solid transparent",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A dropdown menu for multiple documentation links.
|
||||
* Linear-style: Single "Docs" button that expands to show multiple relevant links.
|
||||
*
|
||||
* @example
|
||||
* <DocsMenu items={[
|
||||
* { label: "Custom pricing for models", href: "https://docs.litellm.ai/docs/proxy/custom_pricing" },
|
||||
* { label: "Spend tracking", href: "https://docs.litellm.ai/docs/proxy/cost_tracking" }
|
||||
* ]}>
|
||||
* Docs
|
||||
* </DocsMenu>
|
||||
*/
|
||||
export const DocsMenu: React.FC<DocsMenuProps> = ({ items, children = "Docs", className = "" }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div className={`relative inline-block ${className}`} ref={menuRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="inline-flex items-center gap-1 text-muted-foreground hover:text-foreground text-xs transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-1 rounded-sm px-2 py-1"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<span>{children}</span>
|
||||
<ChevronDown className={`h-3 w-3 transition-transform ${isOpen ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-1 w-56 bg-card rounded-lg shadow-lg border border-border py-1 z-floating">
|
||||
{items.map((item, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between px-4 py-2 text-sm text-foreground hover:bg-accent transition-colors"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground shrink-0 ml-2" aria-hidden="true" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue