mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): atoms/Tooltip + SSOSettingsLoadingSkeleton + BlogDropdown
- atoms/Tooltip: @ant-design QuestionCircleOutlined \u2192 lucide HelpCircle. Retains the legacy custom positioning logic used in legacy atom consumers \u2014 new code should prefer shadcn Tooltip. - SSOSettingsLoadingSkeleton: antd Card/Descriptions/Space/Skeleton/ Typography \u2192 shadcn Card/Skeleton + semantic two-column grid with bg-muted label cells. - Navbar/BlogDropdown: antd Dropdown/Menu/Button/Typography + @ant-design LoadingOutlined \u2192 shadcn DropdownMenu/Button + lucide Loader2. Each blog post is a DropdownMenuItem wrapping an anchor; error state shows Retry button. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
dbca20ce4b
commit
d73979472c
3 changed files with 129 additions and 114 deletions
|
|
@ -1,12 +1,19 @@
|
|||
import {
|
||||
useBlogPosts,
|
||||
type BlogPost,
|
||||
} from "@/app/(dashboard)/hooks/blogPosts/useBlogPosts";
|
||||
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
|
||||
import { useBlogPosts, type BlogPost } from "@/app/(dashboard)/hooks/blogPosts/useBlogPosts";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { Button, Dropdown, Space, Typography } from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
const { Text, Title, Paragraph } = Typography;
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr + "T00:00:00");
|
||||
return date.toLocaleDateString("en-US", {
|
||||
|
|
@ -18,66 +25,76 @@ function formatDate(dateStr: string): string {
|
|||
|
||||
export const BlogDropdown: React.FC = () => {
|
||||
const disableBlogPosts = useDisableBlogPosts();
|
||||
|
||||
const { data, isLoading, isError, refetch } = useBlogPosts();
|
||||
|
||||
if (disableBlogPosts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let items: MenuProps["items"];
|
||||
|
||||
if (isLoading) {
|
||||
items = [{ key: "loading", label: <LoadingOutlined />, disabled: true }];
|
||||
} else if (isError) {
|
||||
items = [
|
||||
{
|
||||
key: "error",
|
||||
label: (
|
||||
<Space>
|
||||
<Text type="danger">Failed to load posts</Text>
|
||||
<Button size="small" onClick={() => refetch()}>
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost">Blog</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[420px] p-1">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="flex items-center justify-between gap-2 p-3">
|
||||
<span className="text-destructive text-sm">
|
||||
Failed to load posts
|
||||
</span>
|
||||
<Button size="sm" variant="outline" onClick={() => refetch()}>
|
||||
Retry
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
} else if (!data || data.posts.length === 0) {
|
||||
items = [{ key: "empty", label: <Text type="secondary">No posts available</Text>, disabled: true }];
|
||||
} else {
|
||||
items = [
|
||||
...data.posts.slice(0, 5).map((post: BlogPost) => ({
|
||||
key: post.url,
|
||||
label: (
|
||||
<a href={post.url} target="_blank" rel="noopener noreferrer" style={{ display: "block", width: 380 }}>
|
||||
<Title level={5} style={{ marginBottom: 2 }}>
|
||||
{post.title}
|
||||
</Title>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{formatDate(post.date)}
|
||||
</Text>
|
||||
<Paragraph ellipsis={{ rows: 2 }}>{post.description}</Paragraph>
|
||||
</a>
|
||||
),
|
||||
})),
|
||||
{ type: "divider" as const },
|
||||
{
|
||||
key: "view-all",
|
||||
label: (
|
||||
<a href="https://docs.litellm.ai/blog" target="_blank" rel="noopener noreferrer">
|
||||
View all posts
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items }} trigger={["hover"]} placement="bottomRight">
|
||||
<Button type="text">Blog</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
) : !data || data.posts.length === 0 ? (
|
||||
<div className="px-3 py-3 text-muted-foreground text-sm">
|
||||
No posts available
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{data.posts.slice(0, 5).map((post: BlogPost) => (
|
||||
<DropdownMenuItem
|
||||
key={post.url}
|
||||
asChild
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<a
|
||||
href={post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full px-2 py-2"
|
||||
>
|
||||
<h5 className="font-semibold text-sm mb-0.5">
|
||||
{post.title}
|
||||
</h5>
|
||||
<div className="text-[11px] text-muted-foreground mb-0.5">
|
||||
{formatDate(post.date)}
|
||||
</div>
|
||||
<p className="text-xs line-clamp-2 text-muted-foreground m-0">
|
||||
{post.description}
|
||||
</p>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href="https://docs.litellm.ai/blog"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full px-2 py-2 text-sm"
|
||||
>
|
||||
View all posts
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,66 +1,48 @@
|
|||
"use client";
|
||||
|
||||
import { Card, Descriptions, Skeleton, Space, Typography } from "antd";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Shield } from "lucide-react";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
export default function SSOSettingsLoadingSkeleton() {
|
||||
const descriptionsConfig = {
|
||||
column: {
|
||||
xxl: 1,
|
||||
xl: 1,
|
||||
lg: 1,
|
||||
md: 1,
|
||||
sm: 1,
|
||||
xs: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Space direction="vertical" size="large" className="w-full">
|
||||
{/* Header Section */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-6 h-6 text-gray-400" />
|
||||
<Shield className="w-6 h-6 text-muted-foreground" />
|
||||
<div>
|
||||
<Title level={3}>SSO Configuration</Title>
|
||||
<Text type="secondary">Manage Single Sign-On authentication settings</Text>
|
||||
<h3 className="text-lg font-semibold m-0">SSO Configuration</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Manage Single Sign-On authentication settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton.Button active size="default" style={{ width: 170, height: 32 }} />
|
||||
<Skeleton.Button active size="default" style={{ width: 190, height: 32 }} />
|
||||
<Skeleton className="h-8 w-[170px]" />
|
||||
<Skeleton className="h-8 w-[190px]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Descriptions Table Skeleton */}
|
||||
<Descriptions bordered {...descriptionsConfig}>
|
||||
{/* Provider Row */}
|
||||
<Descriptions.Item label={<Skeleton.Node active style={{ width: 80, height: 16 }} />}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Skeleton.Node active style={{ width: 100, height: 16 }} />
|
||||
<div className="border border-border rounded-md overflow-hidden">
|
||||
{[100, 200, 250, 180, 220].map((w, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`grid grid-cols-[minmax(120px,200px)_1fr] ${
|
||||
i !== 0 ? "border-t border-border" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="bg-muted px-4 py-2.5">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="px-4 py-2.5">
|
||||
<Skeleton className="h-4" style={{ width: w }} />
|
||||
</div>
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label={<Skeleton.Node active style={{ width: 80, height: 16 }} />}>
|
||||
<Skeleton.Node active style={{ width: 200, height: 16 }} />
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label={<Skeleton.Node active style={{ width: 80, height: 16 }} />}>
|
||||
<Skeleton.Node active style={{ width: 250, height: 16 }} />
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label={<Skeleton.Node active style={{ width: 80, height: 16 }} />}>
|
||||
<Skeleton.Node active style={{ width: 180, height: 16 }} />
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label={<Skeleton.Node active style={{ width: 80, height: 16 }} />}>
|
||||
<Skeleton.Node active style={{ width: 220, height: 16 }} />
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Space>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useRef } from "react";
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
|
||||
interface TooltipProps {
|
||||
content: React.ReactNode;
|
||||
|
|
@ -8,16 +8,26 @@ interface TooltipProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = ({ content, children, width = "auto", className = "" }) => {
|
||||
/**
|
||||
* Legacy custom tooltip preserved for atom-level backwards compatibility.
|
||||
* For new code, use shadcn `<Tooltip>` from @/components/ui/tooltip.
|
||||
*/
|
||||
export const Tooltip: React.FC<TooltipProps> = ({
|
||||
content,
|
||||
children,
|
||||
width = "auto",
|
||||
className = "",
|
||||
}) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipPosition, setTooltipPosition] = useState<"top" | "bottom">("top");
|
||||
const [tooltipPosition, setTooltipPosition] = useState<"top" | "bottom">(
|
||||
"top",
|
||||
);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Function to check if tooltip would fit above
|
||||
const checkTooltipPosition = () => {
|
||||
if (tooltipRef.current) {
|
||||
const rect = tooltipRef.current.getBoundingClientRect();
|
||||
const tooltipHeight = 300; // Approximate height of the tooltip
|
||||
const tooltipHeight = 300;
|
||||
const spaceAbove = rect.top;
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
|
||||
|
|
@ -32,8 +42,8 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children, width = "au
|
|||
return (
|
||||
<div className="relative inline-block" ref={tooltipRef}>
|
||||
{children || (
|
||||
<QuestionCircleOutlined
|
||||
className="ml-1 text-gray-500 cursor-help"
|
||||
<HelpCircle
|
||||
className="ml-1 h-3.5 w-3.5 text-muted-foreground cursor-help"
|
||||
onMouseEnter={() => {
|
||||
checkTooltipPosition();
|
||||
setShowTooltip(true);
|
||||
|
|
@ -46,7 +56,7 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children, width = "au
|
|||
className={`absolute left-1/2 -translate-x-1/2 z-50 bg-black/90 text-white p-2 rounded-md text-sm font-normal shadow-lg ${className}`}
|
||||
style={{
|
||||
[tooltipPosition === "top" ? "bottom" : "top"]: "100%",
|
||||
width: width,
|
||||
width,
|
||||
marginBottom: tooltipPosition === "top" ? "8px" : "0",
|
||||
marginTop: tooltipPosition === "bottom" ? "8px" : "0",
|
||||
}}
|
||||
|
|
@ -57,8 +67,14 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children, width = "au
|
|||
style={{
|
||||
top: tooltipPosition === "top" ? "100%" : "auto",
|
||||
bottom: tooltipPosition === "bottom" ? "100%" : "auto",
|
||||
borderTop: tooltipPosition === "top" ? "6px solid rgba(0, 0, 0, 0.9)" : "6px solid transparent",
|
||||
borderBottom: tooltipPosition === "bottom" ? "6px solid rgba(0, 0, 0, 0.9)" : "6px solid transparent",
|
||||
borderTop:
|
||||
tooltipPosition === "top"
|
||||
? "6px solid rgba(0, 0, 0, 0.9)"
|
||||
: "6px solid transparent",
|
||||
borderBottom:
|
||||
tooltipPosition === "bottom"
|
||||
? "6px solid rgba(0, 0, 0, 0.9)"
|
||||
: "6px solid transparent",
|
||||
borderLeft: "6px solid transparent",
|
||||
borderRight: "6px solid transparent",
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue