mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #37010 from BerriAI/litellm_shadcn_next_0814
fix(ui): de-duplicate the reset budget option and polish shadcn surfaces
This commit is contained in:
commit
992a8123ac
28 changed files with 295 additions and 154 deletions
|
|
@ -2974,6 +2974,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/ui/button-group.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/ui/button.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
Metrics" on the Usage page or individual requests in the Logs page.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div className="mt-4 grid grid-cols-1 items-center gap-4 md:grid-cols-3">
|
||||
<Combobox
|
||||
multiple
|
||||
items={uniqueApiKeys}
|
||||
|
|
@ -207,7 +207,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput placeholder="Select Virtual Keys" className="border-0 bg-transparent" />
|
||||
<ComboboxChipsInput placeholder="Select Virtual Keys" />
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor1}>
|
||||
<ComboboxEmpty>No virtual keys found</ComboboxEmpty>
|
||||
|
|
@ -237,7 +237,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput placeholder="Select Models" className="border-0 bg-transparent" />
|
||||
<ComboboxChipsInput placeholder="Select Models" />
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor2}>
|
||||
<ComboboxEmpty>No models found</ComboboxEmpty>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
|
|||
</div>
|
||||
|
||||
<Tabs defaultValue="usage" onValueChange={handleTabChange}>
|
||||
<TabsList variant="line" className="h-auto w-full justify-start rounded-none border-b p-0">
|
||||
<TabsList variant="line" className="h-auto w-full justify-start rounded-none p-0">
|
||||
<TabsTrigger value="usage" className="flex-none rounded-none px-4 py-2">
|
||||
Overall
|
||||
</TabsTrigger>
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export function GuardrailTestPanel({
|
|||
</div>
|
||||
|
||||
{/* Input Section */}
|
||||
<div className="flex-1 overflow-auto space-y-4">
|
||||
<div className="flex-1 space-y-4 overflow-auto px-1">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<Tabs defaultValue="guardrails">
|
||||
<TabsList>
|
||||
<TabsList variant="line">
|
||||
{isAdmin && (
|
||||
<>
|
||||
<TabsTrigger value="garden" className="flex-none">
|
||||
|
|
|
|||
|
|
@ -532,10 +532,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
{option.label}
|
||||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput
|
||||
className="border-0 bg-transparent"
|
||||
placeholder={mode.length === 0 ? "Select modes" : undefined}
|
||||
/>
|
||||
<ComboboxChipsInput placeholder={mode.length === 0 ? "Select modes" : undefined} />
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No matching modes</ComboboxEmpty>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ export const CategoryFilter: React.FC<CategoryFilterProps> = ({ categories, sele
|
|||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput
|
||||
className="border-0 bg-transparent"
|
||||
placeholder={selectedCategories.length === 0 ? "Select categories to filter by" : undefined}
|
||||
/>
|
||||
</ComboboxChips>
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export default function ModelsAndEndpointsPage() {
|
|||
) : (
|
||||
<Tabs value={activeKey} onValueChange={setActiveKey}>
|
||||
<div className="flex min-w-0 flex-nowrap items-center gap-3 border-b">
|
||||
<div className="-mb-1.5 min-w-0 flex-1 overflow-x-auto pb-1.5">
|
||||
<div className="no-scrollbar scroll-fade-e -mb-1.5 min-w-0 flex-1 overflow-x-auto pb-1.5">
|
||||
<TabsList variant="line" className="w-max justify-start">
|
||||
{visibleSlugs.map((slug) => {
|
||||
const key = slug || BASE_TAB_KEY;
|
||||
|
|
|
|||
|
|
@ -891,7 +891,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
|
|||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput placeholder="Select tags" className="border-0 bg-transparent" />
|
||||
<ComboboxChipsInput placeholder="Select tags" />
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No tags found</ComboboxEmpty>
|
||||
|
|
|
|||
|
|
@ -54,15 +54,12 @@ export function ChatComposer({
|
|||
return (
|
||||
<div className={cn("relative flex w-full flex-col gap-3", className)}>
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<div
|
||||
className="flex w-full gap-2 overflow-x-auto pb-1 sm:grid sm:grid-cols-2 sm:overflow-visible"
|
||||
data-testid="chat-suggested-actions"
|
||||
>
|
||||
<div className="flex w-full flex-col gap-1.5" data-testid="chat-suggested-actions">
|
||||
{suggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
type="button"
|
||||
className="min-w-[200px] shrink-0 rounded-xl border border-border/50 bg-card/30 px-4 py-3 text-left text-[12px] leading-relaxed text-muted-foreground transition-all duration-200 hover:-translate-y-0.5 hover:bg-card/60 hover:text-foreground sm:min-w-0 sm:whitespace-normal sm:p-4 sm:text-[13px]"
|
||||
className="w-full truncate rounded-lg border border-border/50 bg-card/30 px-3 py-1.5 text-left text-[12px] leading-snug text-muted-foreground transition-colors hover:bg-card/60 hover:text-foreground"
|
||||
onClick={() => onSuggestionSelect?.(suggestion)}
|
||||
>
|
||||
{suggestion}
|
||||
|
|
|
|||
|
|
@ -49,20 +49,17 @@ export default function PlaygroundPage() {
|
|||
return (
|
||||
<div className="flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden">
|
||||
<Tabs defaultValue="chat" className="flex min-h-0 min-w-0 flex-1 flex-col gap-0 overflow-hidden">
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="mb-0 h-auto w-full shrink-0 justify-start overflow-x-auto rounded-none border-b border-border bg-transparent p-0"
|
||||
>
|
||||
<TabsTrigger value="chat" className="flex-none rounded-none px-4 py-2">
|
||||
<TabsList variant="line" className="w-full shrink-0 justify-start overflow-x-auto pb-1">
|
||||
<TabsTrigger value="chat" className="flex-none">
|
||||
Chat
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="compare" className="flex-none rounded-none px-4 py-2">
|
||||
<TabsTrigger value="compare" className="flex-none">
|
||||
Compare
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="compliance" className="flex-none rounded-none px-4 py-2">
|
||||
<TabsTrigger value="compliance" className="flex-none">
|
||||
Compliance
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="agent-builder" className="flex-none rounded-none px-4 py-2">
|
||||
<TabsTrigger value="agent-builder" className="flex-none">
|
||||
Agent Builder (Experimental)
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
|
|
|||
|
|
@ -675,18 +675,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{entityType === "team" && (
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-sm text-foreground">Filter by team</p>
|
||||
<TeamMultiSelect value={selectedTags} onChange={setSelectedTags} />
|
||||
</div>
|
||||
)}
|
||||
<UsageExportHeader
|
||||
dateValue={dateValue}
|
||||
entityType={entityType}
|
||||
spendData={spendData}
|
||||
showFilters={entityType !== "team" && entityList !== null && entityList.length > 0}
|
||||
filterLabel={getFilterLabel(entityType)}
|
||||
filterSlot={
|
||||
entityType === "team" ? <TeamMultiSelect value={selectedTags} onChange={setSelectedTags} /> : undefined
|
||||
}
|
||||
filterLabel={entityType === "team" ? "Filter by team" : getFilterLabel(entityType)}
|
||||
filterPlaceholder={getFilterPlaceholder(entityType)}
|
||||
selectedFilters={selectedTags}
|
||||
onFiltersChange={setSelectedTags}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID
|
|||
</p>
|
||||
|
||||
<Tabs defaultValue="create" onValueChange={onTabChange}>
|
||||
<TabsList variant="line" className="mb-6 h-auto w-full justify-start rounded-none border-b p-0">
|
||||
<TabsList variant="line" className="mb-6 h-auto w-full justify-start rounded-none p-0">
|
||||
<TabsTrigger value="create" className="flex-none rounded-none px-4 py-2">
|
||||
Create Vector Store
|
||||
</TabsTrigger>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,54 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Verbatim end-edge subset of the shadcn scroll-fade utility (shadcn@4.17.0 dist/tailwind.css),
|
||||
vendored so the CLI package is not a build dependency. Re-copy from upstream to update. */
|
||||
@property --scroll-fade-e {
|
||||
syntax: "<length-percentage>";
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
}
|
||||
@property --scroll-fade-mask {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@keyframes scroll-fade-reveal-e {
|
||||
from {
|
||||
--scroll-fade-e: var(--_scroll-fade-size-e, var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))));
|
||||
}
|
||||
to {
|
||||
--scroll-fade-e: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@utility scroll-fade-e {
|
||||
--_scroll-fade-size-e: var(--scroll-fade-e-size, var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))));
|
||||
--scroll-fade-mask: linear-gradient(to right, #000 0, #000 calc(100% - var(--scroll-fade-e, 0px)), transparent 100%);
|
||||
&:where([dir="rtl"], [dir="rtl"] *) {
|
||||
--scroll-fade-mask: linear-gradient(to left, #000 0, #000 calc(100% - var(--scroll-fade-e, 0px)), transparent 100%);
|
||||
}
|
||||
-webkit-mask-image: var(--scroll-fade-mask);
|
||||
mask-image: var(--scroll-fade-mask);
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
@supports (animation-timeline: scroll()) {
|
||||
animation: scroll-fade-reveal-e 1ms ease-in-out;
|
||||
animation-timeline: scroll(self inline);
|
||||
animation-range: calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))) 100%;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@supports not (animation-timeline: scroll()) {
|
||||
--scroll-fade-e: var(--_scroll-fade-size-e);
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.5rem;
|
||||
--background: oklch(1 0 0);
|
||||
|
|
@ -211,6 +259,30 @@
|
|||
outline-color: color-mix(in oklab, var(--color-ring) 50%, transparent);
|
||||
}
|
||||
|
||||
/* @tailwindcss/forms paints a 1px blue-600 ring via box-shadow on :focus, which `outline-none`
|
||||
cannot cancel. Components that declare their own focus ring override this from the utilities
|
||||
layer; the ones that do not inherit a blue rectangle. `:not([disabled])` is here only to
|
||||
outweigh the plugin's own (0,1,1) selector, which Tailwind emits after this block. Delete
|
||||
along with the plugin once no route renders a Tremor form control. */
|
||||
:is(input, textarea, select):focus:not([disabled]) {
|
||||
--tw-ring-color: initial;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* Same plugin, resting state: it also gives every bare input a border, a white fill, 0.5rem/0.75rem
|
||||
padding and 1rem text. shadcn primitives style themselves and only break where a base class omits
|
||||
those, which today is `combobox-chip-input` alone (audited against every input-rendering file in
|
||||
components/ui). These values restore Tailwind's own preflight, and `[data-slot]` outweighs the
|
||||
plugin's zero-specificity `input:where(:not([type]))`. Delete along with the plugin. */
|
||||
[data-slot="combobox-chip-input"] {
|
||||
border-width: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ interface UsageExportHeaderProps {
|
|||
onFiltersChange?: (filters: string[]) => void;
|
||||
filterOptions?: Array<{ label: string; value: string }>;
|
||||
filterMode?: "multiple" | "single";
|
||||
filterSlot?: React.ReactNode;
|
||||
customTitle?: string;
|
||||
compactLayout?: boolean;
|
||||
teams?: Team[];
|
||||
|
|
@ -48,6 +49,7 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
|
|||
onFiltersChange,
|
||||
filterOptions = [],
|
||||
filterMode = "multiple",
|
||||
filterSlot,
|
||||
customTitle,
|
||||
compactLayout = false,
|
||||
teams = [],
|
||||
|
|
@ -55,7 +57,7 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
|
|||
const anchor = useComboboxAnchor();
|
||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||
|
||||
const hasFilters = showFilters && filterOptions.length > 0;
|
||||
const hasFilters = filterSlot != null || (showFilters && filterOptions.length > 0);
|
||||
const optionValues = filterOptions.map((option) => option.value);
|
||||
const labelOf = (value: string) => filterOptions.find((option) => option.value === value)?.label ?? value;
|
||||
|
||||
|
|
@ -72,6 +74,46 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
|
|||
</ComboboxContent>
|
||||
);
|
||||
|
||||
const builtInFilter =
|
||||
filterMode === "single" ? (
|
||||
<Combobox
|
||||
items={optionValues}
|
||||
value={selectedFilters[0] ?? null}
|
||||
onValueChange={(next: string | null) => onFiltersChange?.(next ? [next] : [])}
|
||||
itemToStringLabel={labelOf}
|
||||
>
|
||||
<ComboboxInput
|
||||
className="w-full"
|
||||
placeholder={filterPlaceholder}
|
||||
aria-label={filterPlaceholder}
|
||||
showClear={selectedFilters.length > 0}
|
||||
/>
|
||||
{filterList}
|
||||
</Combobox>
|
||||
) : (
|
||||
<Combobox
|
||||
multiple
|
||||
items={optionValues}
|
||||
value={selectedFilters}
|
||||
onValueChange={(next: string[]) => onFiltersChange?.(next)}
|
||||
>
|
||||
<ComboboxChips render={<div ref={anchor} />} className="w-full">
|
||||
<ComboboxValue>
|
||||
{(selected: string[]) =>
|
||||
selected.map((value) => (
|
||||
<ComboboxChip key={value} aria-label={labelOf(value)}>
|
||||
{labelOf(value)}
|
||||
</ComboboxChip>
|
||||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput placeholder={filterPlaceholder} aria-label={filterPlaceholder} />
|
||||
{selectedFilters.length > 0 && <ComboboxClear aria-label={`Clear ${filterLabel ?? "filters"}`} />}
|
||||
</ComboboxChips>
|
||||
{filterList}
|
||||
</Combobox>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
|
|
@ -84,48 +126,7 @@ const UsageExportHeader: React.FC<UsageExportHeaderProps> = ({
|
|||
{hasFilters && (
|
||||
<div>
|
||||
{filterLabel && <label className="text-sm font-medium text-gray-700 block mb-2">{filterLabel}</label>}
|
||||
{filterMode === "single" ? (
|
||||
<Combobox
|
||||
items={optionValues}
|
||||
value={selectedFilters[0] ?? null}
|
||||
onValueChange={(next: string | null) => onFiltersChange?.(next ? [next] : [])}
|
||||
itemToStringLabel={labelOf}
|
||||
>
|
||||
<ComboboxInput
|
||||
className="w-full"
|
||||
placeholder={filterPlaceholder}
|
||||
aria-label={filterPlaceholder}
|
||||
showClear={selectedFilters.length > 0}
|
||||
/>
|
||||
{filterList}
|
||||
</Combobox>
|
||||
) : (
|
||||
<Combobox
|
||||
multiple
|
||||
items={optionValues}
|
||||
value={selectedFilters}
|
||||
onValueChange={(next: string[]) => onFiltersChange?.(next)}
|
||||
>
|
||||
<ComboboxChips render={<div ref={anchor} />} className="w-full">
|
||||
<ComboboxValue>
|
||||
{(selected: string[]) =>
|
||||
selected.map((value) => (
|
||||
<ComboboxChip key={value} aria-label={labelOf(value)}>
|
||||
{labelOf(value)}
|
||||
</ComboboxChip>
|
||||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput
|
||||
className="border-0 bg-transparent"
|
||||
placeholder={filterPlaceholder}
|
||||
aria-label={filterPlaceholder}
|
||||
/>
|
||||
{selectedFilters.length > 0 && <ComboboxClear aria-label={`Clear ${filterLabel ?? "filters"}`} />}
|
||||
</ComboboxChips>
|
||||
{filterList}
|
||||
</Combobox>
|
||||
)}
|
||||
{filterSlot ?? builtInFilter}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -256,11 +256,7 @@ export const ModelSelect = (props: ModelSelectProps) => {
|
|||
</>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput
|
||||
placeholder="Select Models"
|
||||
aria-label="Select Models"
|
||||
className="h-5 min-w-24 flex-1 border-0 bg-transparent py-0 text-sm"
|
||||
/>
|
||||
<ComboboxChipsInput placeholder="Select Models" aria-label="Select Models" className="min-w-24" />
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No models found</ComboboxEmpty>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,25 @@
|
|||
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { ButtonGroup } from "@/components/ui/button-group";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
import { Github, Slack } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
const iconBtnClass =
|
||||
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md border-0 bg-transparent text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 cursor-pointer";
|
||||
const COMMUNITY_LINKS = [
|
||||
{
|
||||
href: "https://www.litellm.ai/support",
|
||||
label: "Join Slack",
|
||||
tooltip: "LiteLLM Slack community",
|
||||
Icon: Slack,
|
||||
},
|
||||
{
|
||||
href: "https://github.com/BerriAI/litellm",
|
||||
label: "LiteLLM on GitHub",
|
||||
tooltip: "LiteLLM on GitHub",
|
||||
Icon: Github,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const CommunityEngagementButtons: React.FC = () => {
|
||||
const disableShowPrompts = useDisableShowPrompts();
|
||||
|
|
@ -14,44 +29,27 @@ export const CommunityEngagementButtons: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-0.5 rounded-md border border-gray-200/80 bg-gray-50 px-0.5 py-0"
|
||||
aria-label="Community links"
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<a
|
||||
href="https://www.litellm.ai/support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={iconBtnClass}
|
||||
aria-label="Join Slack"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Slack className="size-[18px]" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>LiteLLM Slack community</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<a
|
||||
href="https://github.com/BerriAI/litellm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={iconBtnClass}
|
||||
aria-label="LiteLLM on GitHub"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Github className="size-[18px]" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>LiteLLM on GitHub</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<ButtonGroup aria-label="Community links">
|
||||
{COMMUNITY_LINKS.map(({ href, label, tooltip, Icon }) => (
|
||||
<Tooltip key={href}>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={label}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "icon" }), "text-muted-foreground")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Icon />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -384,11 +384,7 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
|
|||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput
|
||||
className="border-0 bg-transparent"
|
||||
placeholder="Select permissions"
|
||||
aria-label="Team Member Permissions"
|
||||
/>
|
||||
<ComboboxChipsInput placeholder="Select permissions" aria-label="Team Member Permissions" />
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxList>
|
||||
|
|
|
|||
|
|
@ -87,12 +87,7 @@ const TeamMultiSelect: React.FC<TeamMultiSelectProps> = ({
|
|||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput
|
||||
className="border-0 bg-transparent"
|
||||
placeholder={placeholder}
|
||||
aria-label={placeholder}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ComboboxChipsInput placeholder={placeholder} aria-label={placeholder} disabled={disabled} />
|
||||
{value.length > 0 && <ComboboxClear aria-label="Clear all teams" disabled={disabled} />}
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
|
|
|
|||
|
|
@ -251,9 +251,17 @@ vi.mock("../molecules/notifications_manager", () => ({
|
|||
vi.mock("../agent_management/AgentSelector", () => ({ default: () => null }));
|
||||
vi.mock("../common_components/budget_duration_dropdown", () => ({
|
||||
NEVER_RESETS_BUDGET_DURATION: "none",
|
||||
default: ({ showNeverResets, onChange }: { showNeverResets?: boolean; onChange?: (value: string) => void }) => (
|
||||
default: ({
|
||||
showNeverResets,
|
||||
placeholder,
|
||||
onChange,
|
||||
}: {
|
||||
showNeverResets?: boolean;
|
||||
placeholder?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}) => (
|
||||
<select data-testid="budget-duration-dropdown" onChange={(event) => onChange?.(event.target.value)}>
|
||||
<option value="">n/a</option>
|
||||
<option value="">{placeholder ?? "n/a"}</option>
|
||||
{showNeverResets ? <option value="none">Never resets</option> : null}
|
||||
<option value="30d">monthly</option>
|
||||
</select>
|
||||
|
|
@ -871,6 +879,19 @@ describe("CreateKey", () => {
|
|||
expect(formValues.budget_duration).toBeNull();
|
||||
});
|
||||
|
||||
it("should label the omit option distinctly from 'Never resets'", async () => {
|
||||
await openModal();
|
||||
|
||||
const optionLabels = Array.from(
|
||||
screen.getByTestId("budget-duration-dropdown").querySelectorAll("option"),
|
||||
(option) => option.textContent,
|
||||
);
|
||||
|
||||
expect(optionLabels).toContain("Never resets");
|
||||
expect(new Set(optionLabels).size).toBe(optionLabels.length);
|
||||
expect(screen.getByRole("option", { name: "Never resets" })).toHaveValue("none");
|
||||
});
|
||||
|
||||
it("should omit budget_duration entirely when the reset dropdown is untouched", async () => {
|
||||
await openModal();
|
||||
|
||||
|
|
|
|||
|
|
@ -1069,7 +1069,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
>
|
||||
<BudgetDurationDropdown
|
||||
showNeverResets
|
||||
placeholder="Never resets"
|
||||
placeholder="Not set"
|
||||
onChange={(value) => form.setFieldValue("budget_duration", value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
|
|
|||
|
|
@ -637,7 +637,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
<ComboboxChipsInput
|
||||
placeholder="Select providers"
|
||||
aria-label="Select providers"
|
||||
className="h-5 min-w-24 flex-1 border-0 bg-transparent py-0 text-sm"
|
||||
className="min-w-24"
|
||||
/>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
|
|
|
|||
|
|
@ -79,12 +79,7 @@ const SearchToolSelector: React.FC<SearchToolSelectorProps> = ({
|
|||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput
|
||||
className="border-0 bg-transparent"
|
||||
placeholder={placeholder}
|
||||
aria-label={placeholder}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ComboboxChipsInput placeholder={placeholder} aria-label={placeholder} disabled={disabled} />
|
||||
{value && value.length > 0 && <ComboboxClear aria-label="Clear all search tools" disabled={disabled} />}
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
|
|
|
|||
|
|
@ -303,7 +303,10 @@ function DataTableBodyRow<TData>({
|
|||
function MessageRow({ colSpan, children }: { colSpan: number; children: React.ReactNode }) {
|
||||
return (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={colSpan} className="h-24 text-center align-middle text-sm text-muted-foreground">
|
||||
<TableCell
|
||||
colSpan={colSpan}
|
||||
className="h-24 text-center align-middle text-sm whitespace-normal text-muted-foreground"
|
||||
>
|
||||
{children}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export function MultiSelect({
|
|||
))}
|
||||
<ComboboxChipsInput
|
||||
placeholder={loading ? "Loading..." : placeholder}
|
||||
className="h-5 min-w-24 flex-1 border-0 bg-transparent py-0 text-sm"
|
||||
className="min-w-24"
|
||||
aria-label={placeholder}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
76
ui/litellm-dashboard/src/components/ui/button-group.tsx
Normal file
76
ui/litellm-dashboard/src/components/ui/button-group.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { type VariantProps } from "cva";
|
||||
|
||||
import { cn, cva } from "@/lib/cva.config";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const buttonGroupVariants = cva({
|
||||
base: "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
|
||||
vertical:
|
||||
"flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
});
|
||||
|
||||
function ButtonGroup({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupText({ className, render, ...props }: useRender.ComponentProps<"div">) {
|
||||
return useRender({
|
||||
defaultTagName: "div",
|
||||
props: mergeProps<"div">(
|
||||
{
|
||||
className: cn(
|
||||
"flex items-center gap-2 rounded-md border bg-muted px-2.5 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "button-group-text",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function ButtonGroupSeparator({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants };
|
||||
|
|
@ -397,11 +397,7 @@ const UserAgentActivity: React.FC<UserAgentActivityProps> = ({ accessToken, user
|
|||
))
|
||||
}
|
||||
</ComboboxValue>
|
||||
<ComboboxChipsInput
|
||||
className="border-0 bg-transparent"
|
||||
placeholder="All User Agents"
|
||||
aria-label="All User Agents"
|
||||
/>
|
||||
<ComboboxChipsInput placeholder="All User Agents" aria-label="All User Agents" />
|
||||
{selectedTags.length > 0 && <ComboboxClear aria-label="Clear user agent filter" />}
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { JsonView, defaultStyles } from "react-json-view-lite";
|
||||
import "react-json-view-lite/dist/index.css";
|
||||
import { JSON_MAX_HEIGHT, COLOR_BG_LIGHT, SPACING_LARGE } from "./constants";
|
||||
import { JSON_MAX_HEIGHT, SPACING_LARGE } from "./constants";
|
||||
|
||||
interface JsonViewerProps {
|
||||
data: any;
|
||||
|
|
@ -16,15 +16,15 @@ export function JsonViewer({ data }: JsonViewerProps) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="bg-background"
|
||||
style={{
|
||||
maxHeight: JSON_MAX_HEIGHT,
|
||||
overflow: "auto",
|
||||
background: COLOR_BG_LIGHT,
|
||||
padding: SPACING_LARGE,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<div className="**:[[role='tree']]:bg-white **:[[role='tree']]:text-slate-900">
|
||||
<div className="**:[[role='tree']]:bg-background! **:[[role='tree']]:text-foreground">
|
||||
<JsonView data={data} style={defaultStyles} clickToExpandNode={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue