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:
Cursor Agent 2026-04-23 11:37:10 +00:00
parent dbca20ce4b
commit d73979472c
No known key found for this signature in database
3 changed files with 129 additions and 114 deletions

View file

@ -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>
);
};

View file

@ -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>
);
}

View file

@ -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",
}}