Skip to content

Commit 0b978f5

Browse files
committed
replace region/type radio filters with tag sidebar checkboxes
1 parent de7d003 commit 0b978f5

File tree

1 file changed

+130
-62
lines changed

1 file changed

+130
-62
lines changed

ui/litellm-dashboard/src/components/policies/policy_templates.tsx

Lines changed: 130 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect, useMemo } from "react";
2-
import { Card, Button, Spin, message, Radio } from "antd";
2+
import { Card, Button, Spin, message, Checkbox, Badge } from "antd";
33
import {
44
ShieldCheckIcon,
55
ShieldExclamationIcon,
@@ -16,6 +16,7 @@ interface PolicyTemplateCardProps {
1616
iconColor: string;
1717
iconBg: string;
1818
guardrails: string[];
19+
tags: string[];
1920
inherits?: string;
2021
complexity: "Low" | "Medium" | "High";
2122
onUseTemplate: () => void;
@@ -28,6 +29,7 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
2829
iconColor,
2930
iconBg,
3031
guardrails,
32+
tags,
3133
inherits,
3234
complexity,
3335
onUseTemplate,
@@ -60,7 +62,20 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
6062
</div>
6163

6264
<h3 className="text-base font-semibold text-gray-900 mb-2">{title}</h3>
63-
<p className="text-sm text-gray-500 mb-6 flex-grow">{description}</p>
65+
<p className="text-sm text-gray-500 mb-4 flex-grow">{description}</p>
66+
67+
{tags.length > 0 && (
68+
<div className="flex flex-wrap gap-1.5 mb-4">
69+
{tags.map((tag) => (
70+
<span
71+
key={tag}
72+
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100"
73+
>
74+
{tag}
75+
</span>
76+
))}
77+
</div>
78+
)}
6479

6580
{inherits && (
6681
<div className="mb-4 text-xs">
@@ -116,26 +131,45 @@ const iconMap: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>
116131
const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, accessToken }) => {
117132
const [templates, setTemplates] = useState<any[]>([]);
118133
const [isLoading, setIsLoading] = useState(false);
119-
const [selectedRegion, setSelectedRegion] = useState<string>("All");
120-
const [selectedType, setSelectedType] = useState<string>("All");
121-
122-
const availableRegions = useMemo(() => {
123-
const regions = new Set(templates.map(t => t.region || "Global"));
124-
return ["All", ...Array.from(regions).sort()];
125-
}, [templates]);
134+
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
126135

127-
const availableTypes = useMemo(() => {
128-
const types = new Set(templates.map(t => t.type || "General"));
129-
return ["All", ...Array.from(types).sort()];
136+
// Compute all unique tags with counts
137+
const tagCounts = useMemo(() => {
138+
const counts: Record<string, number> = {};
139+
templates.forEach((t) => {
140+
const tags: string[] = t.tags || [];
141+
tags.forEach((tag: string) => {
142+
counts[tag] = (counts[tag] || 0) + 1;
143+
});
144+
});
145+
// Sort alphabetically
146+
return Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
130147
}, [templates]);
131148

149+
// Filter templates: show templates that have ALL selected tags (AND logic)
132150
const filteredTemplates = useMemo(() => {
133-
return templates.filter(t => {
134-
const regionMatch = selectedRegion === "All" || (t.region || "Global") === selectedRegion;
135-
const typeMatch = selectedType === "All" || (t.type || "General") === selectedType;
136-
return regionMatch && typeMatch;
151+
if (selectedTags.size === 0) return templates;
152+
return templates.filter((t) => {
153+
const tags: string[] = t.tags || [];
154+
return Array.from(selectedTags).every((selectedTag) => tags.includes(selectedTag));
155+
});
156+
}, [templates, selectedTags]);
157+
158+
const handleTagToggle = (tag: string) => {
159+
setSelectedTags((prev) => {
160+
const next = new Set(prev);
161+
if (next.has(tag)) {
162+
next.delete(tag);
163+
} else {
164+
next.add(tag);
165+
}
166+
return next;
137167
});
138-
}, [templates, selectedRegion, selectedType]);
168+
};
169+
170+
const handleClearAll = () => {
171+
setSelectedTags(new Set());
172+
};
139173

140174
useEffect(() => {
141175
const fetchTemplates = async () => {
@@ -178,54 +212,88 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, access
178212
</div>
179213
</div>
180214

181-
<div className="flex items-center gap-6 mb-4">
182-
<div className="flex items-center gap-3">
183-
<span className="text-sm font-medium text-gray-700">Region:</span>
184-
<Radio.Group
185-
value={selectedRegion}
186-
onChange={(e) => setSelectedRegion(e.target.value)}
187-
buttonStyle="solid"
188-
>
189-
{availableRegions.map(region => (
190-
<Radio.Button key={region} value={region}>
191-
{region}
192-
</Radio.Button>
193-
))}
194-
</Radio.Group>
195-
</div>
196-
{availableTypes.length > 2 && (
197-
<div className="flex items-center gap-3">
198-
<span className="text-sm font-medium text-gray-700">Type:</span>
199-
<Radio.Group
200-
value={selectedType}
201-
onChange={(e) => setSelectedType(e.target.value)}
202-
buttonStyle="solid"
203-
>
204-
{availableTypes.map(type => (
205-
<Radio.Button key={type} value={type}>
206-
{type}
207-
</Radio.Button>
208-
))}
209-
</Radio.Group>
215+
<div className="flex gap-6">
216+
{/* Left sidebar - tag filters */}
217+
{tagCounts.length > 0 && (
218+
<div className="w-52 flex-shrink-0">
219+
<div className="sticky top-4">
220+
<div className="flex items-center justify-between mb-3">
221+
<span className="text-sm font-semibold text-gray-900">
222+
Categories
223+
</span>
224+
{selectedTags.size > 0 && (
225+
<button
226+
onClick={handleClearAll}
227+
className="text-xs text-blue-600 hover:text-blue-800"
228+
>
229+
Clear all
230+
</button>
231+
)}
232+
</div>
233+
<div className="space-y-1">
234+
{tagCounts.map(([tag, count]) => (
235+
<label
236+
key={tag}
237+
className={`flex items-center justify-between px-2 py-1.5 rounded-md cursor-pointer transition-colors ${
238+
selectedTags.has(tag)
239+
? "bg-blue-50"
240+
: "hover:bg-gray-50"
241+
}`}
242+
>
243+
<div className="flex items-center gap-2">
244+
<Checkbox
245+
checked={selectedTags.has(tag)}
246+
onChange={() => handleTagToggle(tag)}
247+
/>
248+
<span className="text-sm text-gray-700">{tag}</span>
249+
</div>
250+
<span className="text-xs text-gray-400 font-medium">
251+
{count}
252+
</span>
253+
</label>
254+
))}
255+
</div>
256+
</div>
210257
</div>
211258
)}
212-
</div>
213259

214-
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
215-
{filteredTemplates.map((template, index) => (
216-
<PolicyTemplateCard
217-
key={template.id || index}
218-
title={template.title}
219-
description={template.description}
220-
icon={iconMap[template.icon] || ShieldCheckIcon}
221-
iconColor={template.iconColor}
222-
iconBg={template.iconBg}
223-
guardrails={template.guardrails}
224-
inherits={template.inherits}
225-
complexity={template.complexity}
226-
onUseTemplate={() => onUseTemplate(template)}
227-
/>
228-
))}
260+
{/* Right content - template cards */}
261+
<div className="flex-1">
262+
{selectedTags.size > 0 && (
263+
<div className="mb-4 text-sm text-gray-500">
264+
Showing {filteredTemplates.length} of {templates.length} templates
265+
</div>
266+
)}
267+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
268+
{filteredTemplates.map((template, index) => (
269+
<PolicyTemplateCard
270+
key={template.id || index}
271+
title={template.title}
272+
description={template.description}
273+
icon={iconMap[template.icon] || ShieldCheckIcon}
274+
iconColor={template.iconColor}
275+
iconBg={template.iconBg}
276+
guardrails={template.guardrails}
277+
tags={template.tags || []}
278+
inherits={template.inherits}
279+
complexity={template.complexity}
280+
onUseTemplate={() => onUseTemplate(template)}
281+
/>
282+
))}
283+
</div>
284+
285+
{filteredTemplates.length === 0 && (
286+
<div className="text-center py-12 text-gray-500">
287+
<p>No templates match the selected filters.</p>
288+
<button
289+
onClick={handleClearAll}
290+
className="text-blue-600 hover:text-blue-800 mt-2 text-sm"
291+
>
292+
Clear all filters
293+
</button>
294+
</div>
295+
)}
296+
</div>
229297
</div>
230298
</div>
231299
);

0 commit comments

Comments
 (0)