11import 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" ;
33import {
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>>
116131const 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