diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 738372ab4a2..4f63cabe534 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -53,6 +53,8 @@ "src/components/permissions/VectorStorePermissions.tsx", "src/components/permissions/AgentPermissions.tsx", "src/components/permissions/MCPServerPermissions.tsx", + "src/components/policies/impact_preview_alert.tsx", + "src/components/policies/impact_popover.tsx", "src/components/DeletedKeysPage/DeletedKeysPage.tsx", "src/components/DeletedTeamsPage/DeletedTeamsPage.tsx" ], diff --git a/ui/litellm-dashboard/src/components/AIHub/SkillHubDashboard.tsx b/ui/litellm-dashboard/src/components/AIHub/SkillHubDashboard.tsx index af6668dcbeb..f546e7e6f83 100644 --- a/ui/litellm-dashboard/src/components/AIHub/SkillHubDashboard.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/SkillHubDashboard.tsx @@ -1,7 +1,13 @@ import React, { useMemo, useState } from "react"; -import { Text } from "@tremor/react"; -import { SearchOutlined } from "@ant-design/icons"; -import { Input, Select } from "antd"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Search } from "lucide-react"; import { Plugin } from "@/components/claude_code_plugins/types"; import { ModelDataTable } from "@/components/model_dashboard/table"; import { skillHubColumns } from "@/components/skill_hub_table_columns"; @@ -16,6 +22,8 @@ interface SkillHubDashboardProps { onPublishSuccess?: () => void; } +const ALL_DOMAINS = "__all__"; + const SkillHubDashboard: React.FC = ({ skills, isLoading, @@ -25,19 +33,25 @@ const SkillHubDashboard: React.FC = ({ onPublishSuccess, }) => { const [search, setSearch] = useState(""); - const [domainFilter, setDomainFilter] = useState(undefined); + const [domainFilter, setDomainFilter] = useState( + undefined, + ); const [selectedSkill, setSelectedSkill] = useState(null); const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); }; - // Derived stats const totalSkills = skills.length; - const domains = useMemo(() => [...new Set(skills.map((s) => s.domain).filter(Boolean))], [skills]); - const namespaces = useMemo(() => [...new Set(skills.map((s) => s.namespace).filter(Boolean))], [skills]); + const domains = useMemo( + () => [...new Set(skills.map((s) => s.domain).filter(Boolean))], + [skills], + ); + const namespaces = useMemo( + () => [...new Set(skills.map((s) => s.namespace).filter(Boolean))], + [skills], + ); - // Filtered table data const filteredSkills = useMemo(() => { let result = skills; if (domainFilter) { @@ -51,7 +65,7 @@ const SkillHubDashboard: React.FC = ({ s.description?.toLowerCase().includes(q) || s.domain?.toLowerCase().includes(q) || s.namespace?.toLowerCase().includes(q) || - s.keywords?.some((k) => k.toLowerCase().includes(q)) + s.keywords?.some((k) => k.toLowerCase().includes(q)), ); } return result; @@ -70,66 +84,86 @@ const SkillHubDashboard: React.FC = ({ } if (isLoading) { - return
Loading skills...
; + return ( +
+ Loading skills... +
+ ); } return (
- {/* Stats row */}
-
-
Total Skills
-
{totalSkills}
+
+
Total Skills
+
+ {totalSkills} +
-
-
Namespaces
-
{namespaces.length}
+
+
Namespaces
+
+ {namespaces.length} +
-
-
Domains
-
{domains.length}
+
+
Domains
+
+ {domains.length} +
- {/* Search + filters + table */}
-

+

All {publicPage ? "Public " : ""}Skills

} - placeholder="Search by name, namespace, or tag…" - value={search} - onChange={(e) => setSearch(e.target.value)} - style={{ width: 280 }} - allowClear - /> + value={domainFilter ?? ALL_DOMAINS} + onValueChange={(v) => + setDomainFilter(v === ALL_DOMAINS ? undefined : v) + } + > + + + + + All Domains + {domains.map((d) => ( + + {d} + + ))} + + +
+ + setSearch(e.target.value)} + className="pl-8 h-9" + /> +
setSelectedSkill(skill), copyToClipboard, - publicPage + publicPage, )} data={filteredSkills} isLoading={false} defaultSorting={[{ id: "name", desc: false }]} />
- - Showing {filteredSkills.length} of {totalSkills} skill{totalSkills !== 1 ? "s" : ""} - + + Showing {filteredSkills.length} of {totalSkills} skill + {totalSkills !== 1 ? "s" : ""} +
diff --git a/ui/litellm-dashboard/src/components/policies/impact_popover.tsx b/ui/litellm-dashboard/src/components/policies/impact_popover.tsx index 7009f9649ff..63db9146b03 100644 --- a/ui/litellm-dashboard/src/components/policies/impact_popover.tsx +++ b/ui/litellm-dashboard/src/components/policies/impact_popover.tsx @@ -1,15 +1,32 @@ import React, { useState } from "react"; -import { Icon } from "@tremor/react"; -import { EyeIcon } from "@heroicons/react/outline"; -import { Tooltip, Tag, Popover, Spin } from "antd"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Badge } from "@/components/ui/badge"; +import { Eye, Loader2 } from "lucide-react"; import { PolicyAttachment } from "./types"; import { estimateAttachmentImpactCall } from "../networking"; -const ImpactPopover: React.FC<{ attachment: PolicyAttachment; accessToken: string | null }> = ({ - attachment, - accessToken, -}) => { - const [impact, setImpact] = useState(null); +interface ImpactInfo { + affected_keys_count: number; + affected_teams_count: number; + sample_keys: string[]; + sample_teams: string[]; +} + +const ImpactPopover: React.FC<{ + attachment: PolicyAttachment; + accessToken: string | null; +}> = ({ attachment, accessToken }) => { + const [impact, setImpact] = useState(null); const [loading, setLoading] = useState(false); const [loaded, setLoaded] = useState(false); @@ -34,49 +51,89 @@ const ImpactPopover: React.FC<{ attachment: PolicyAttachment; accessToken: strin } }; - const content = loading ? ( -
Loading...
- ) : impact ? ( -
- {impact.affected_keys_count === -1 ? ( -

Global scope — affects all keys and teams

- ) : ( - <> -

- {impact.affected_keys_count} key{impact.affected_keys_count !== 1 ? "s" : ""},{" "} - {impact.affected_teams_count} team{impact.affected_teams_count !== 1 ? "s" : ""} affected -

- {impact.sample_keys.length > 0 && ( -
- Keys: - {impact.sample_keys.map((k: string) => ( - {k} - ))} -
- )} - {impact.sample_teams.length > 0 && ( -
- Teams: - {impact.sample_teams.map((t: string) => ( - {t} - ))} -
- )} - {impact.affected_keys_count === 0 && impact.affected_teams_count === 0 && ( -

No keys or teams currently affected

- )} - - )} -
- ) : ( -

Click to load

- ); - return ( - { if (open) loadImpact(); }}> - - - + { + if (open) loadImpact(); + }} + > + + + + + + + + View blast radius + + + +
Blast Radius
+ {loading ? ( +
+ Loading... +
+ ) : impact ? ( +
+ {impact.affected_keys_count === -1 ? ( +

+ Global scope — affects all keys and teams +

+ ) : ( + <> +

+ {impact.affected_keys_count} key + {impact.affected_keys_count !== 1 ? "s" : ""},{" "} + {impact.affected_teams_count} team + {impact.affected_teams_count !== 1 ? "s" : ""} affected +

+ {impact.sample_keys.length > 0 && ( +
+ Keys: + {impact.sample_keys.map((k: string) => ( + + {k} + + ))} +
+ )} + {impact.sample_teams.length > 0 && ( +
+ Teams: + {impact.sample_teams.map((t: string) => ( + + {t} + + ))} +
+ )} + {impact.affected_keys_count === 0 && + impact.affected_teams_count === 0 && ( +

+ No keys or teams currently affected +

+ )} + + )} +
+ ) : ( +

Click to load

+ )} +
); }; diff --git a/ui/litellm-dashboard/src/components/policies/impact_preview_alert.tsx b/ui/litellm-dashboard/src/components/policies/impact_preview_alert.tsx index 3957d81460c..0d066091c53 100644 --- a/ui/litellm-dashboard/src/components/policies/impact_preview_alert.tsx +++ b/ui/litellm-dashboard/src/components/policies/impact_preview_alert.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { Alert, Tag, Typography } from "antd"; - -const { Text } = Typography; +import { Badge } from "@/components/ui/badge"; +import { AlertTriangle, Info } from "lucide-react"; +import { cn } from "@/lib/utils"; interface ImpactResult { affected_keys_count: number; @@ -14,47 +14,89 @@ interface ImpactPreviewAlertProps { impactResult: ImpactResult; } -const ImpactPreviewAlert: React.FC = ({ impactResult }) => { +const ImpactPreviewAlert: React.FC = ({ + impactResult, +}) => { + const isWarning = impactResult.affected_keys_count === -1; + return ( - Global scope — this will affect all keys and teams. +
+ {isWarning ? ( + + ) : ( + + )} +
+
Impact Preview
+ {isWarning ? ( +
+ Global scope — this will affect{" "} + all keys and teams. +
) : ( -
- - This attachment would affect {impactResult.affected_keys_count} key{impactResult.affected_keys_count !== 1 ? "s" : ""} and {impactResult.affected_teams_count} team{impactResult.affected_teams_count !== 1 ? "s" : ""}. - +
+
+ This attachment would affect{" "} + + {impactResult.affected_keys_count} key + {impactResult.affected_keys_count !== 1 ? "s" : ""} + {" "} + and{" "} + + {impactResult.affected_teams_count} team + {impactResult.affected_teams_count !== 1 ? "s" : ""} + + . +
{impactResult.sample_keys.length > 0 && (
- Keys: + Keys: {impactResult.sample_keys.slice(0, 5).map((k: string) => ( - {k} + + {k} + ))} {impactResult.affected_keys_count > 5 && ( - and {impactResult.affected_keys_count - 5} more... + + and {impactResult.affected_keys_count - 5} more... + )}
)} {impactResult.sample_teams.length > 0 && (
- Teams: + Teams: {impactResult.sample_teams.slice(0, 5).map((t: string) => ( - {t} + + {t} + ))} {impactResult.affected_teams_count > 5 && ( - and {impactResult.affected_teams_count - 5} more... + + and {impactResult.affected_teams_count - 5} more... + )}
)}
- ) - } - /> + )} +
+
); };