@@ -14,12 +14,16 @@ import {
1414 Stack ,
1515} from '@trycompai/design-system' ;
1616import { Search } from '@trycompai/design-system/icons' ;
17+ import { Loader2 } from 'lucide-react' ;
1718import { useMemo , useState } from 'react' ;
19+ import { usePolicyOnboardingStatus } from '../../(overview)/hooks/use-policy-onboarding-status' ;
1820import { PoliciesTableDS } from './PoliciesTableDS' ;
21+ import { PolicyTailoringProvider } from './policy-tailoring-context' ;
1922import { comparePoliciesByName } from './policy-name-sort' ;
2023
2124interface PolicyFiltersProps {
2225 policies : Policy [ ] ;
26+ onboardingRunId ?: string | null ;
2327}
2428
2529const STATUS_OPTIONS : { value : PolicyStatus | 'all' | 'archived' ; label : string } [ ] = [
@@ -30,13 +34,21 @@ const STATUS_OPTIONS: { value: PolicyStatus | 'all' | 'archived'; label: string
3034 { value : 'archived' , label : 'Archived' } ,
3135] ;
3236
33- export function PolicyFilters ( { policies } : PolicyFiltersProps ) {
37+ export function PolicyFilters ( { policies, onboardingRunId } : PolicyFiltersProps ) {
3438 const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
3539 const [ statusFilter , setStatusFilter ] = useState < PolicyStatus | 'all' | 'archived' > ( 'all' ) ;
3640 const [ departmentFilter , setDepartmentFilter ] = useState < string > ( 'all' ) ;
3741 const [ sortColumn , setSortColumn ] = useState < 'name' | 'status' | 'updatedAt' > ( 'name' ) ;
3842 const [ sortDirection , setSortDirection ] = useState < 'asc' | 'desc' > ( 'asc' ) ;
3943
44+ // ENG-108: subscribe to the onboarding run so PoliciesTableDS can surface
45+ // per-row "Tailoring/Queued/Preparing" state and we can render the banner
46+ // while AI is still personalizing the policy pack. Mirrors the existing
47+ // pattern in RisksTable/VendorsTable.
48+ const { itemStatuses, progress, isActive } = usePolicyOnboardingStatus ( onboardingRunId ) ;
49+ const hasActivePolicies = policies . length > 0 ;
50+ const showTailoringBanner = isActive && hasActivePolicies && progress !== null ;
51+
4052 // Get unique departments from policies
4153 const departments = useMemo ( ( ) => {
4254 const depts = new Set < string > ( ) ;
@@ -103,63 +115,78 @@ export function PolicyFilters({ policies }: PolicyFiltersProps) {
103115 departmentFilter === 'all' ? 'All Departments' : formatDepartment ( departmentFilter ) ;
104116
105117 return (
106- < Stack gap = "md" >
107- < div className = "flex flex-col gap-3 md:flex-row md:items-end" >
108- { /* Search - full width on mobile, constrained on desktop */ }
109- < div className = "w-full md:max-w-[300px]" >
110- < InputGroup >
111- < InputGroupAddon >
112- < Search size = { 16 } />
113- </ InputGroupAddon >
114- < InputGroupInput
115- placeholder = "Search policies..."
116- value = { searchQuery }
117- onChange = { ( e ) => setSearchQuery ( e . target . value ) }
118- />
119- </ InputGroup >
120- </ div >
121- { /* Filters - side by side on mobile, inline with search on desktop */ }
122- < div className = "flex gap-2" >
123- < div className = "flex-1 md:w-[160px] md:flex-none" >
124- < Select
125- value = { statusFilter }
126- onValueChange = { ( v ) => setStatusFilter ( ( v ?? 'all' ) as PolicyStatus | 'all' | 'archived' ) }
127- >
128- < SelectTrigger >
129- < SelectValue placeholder = "Status" > { statusLabel } </ SelectValue >
130- </ SelectTrigger >
131- < SelectContent >
132- { STATUS_OPTIONS . map ( ( opt ) => (
133- < SelectItem key = { opt . value } value = { opt . value } >
134- { opt . label }
135- </ SelectItem >
136- ) ) }
137- </ SelectContent >
138- </ Select >
118+ < PolicyTailoringProvider statuses = { itemStatuses } >
119+ < Stack gap = "md" >
120+ < div className = "flex flex-col gap-3 md:flex-row md:items-end" >
121+ { /* Search - full width on mobile, constrained on desktop */ }
122+ < div className = "w-full md:max-w-[300px]" >
123+ < InputGroup >
124+ < InputGroupAddon >
125+ < Search size = { 16 } />
126+ </ InputGroupAddon >
127+ < InputGroupInput
128+ placeholder = "Search policies..."
129+ value = { searchQuery }
130+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
131+ />
132+ </ InputGroup >
139133 </ div >
140- < div className = "flex-1 md:w-[160px] md:flex-none" >
141- < Select value = { departmentFilter } onValueChange = { ( v ) => setDepartmentFilter ( v ?? 'all' ) } >
142- < SelectTrigger >
143- < SelectValue placeholder = "Department" > { departmentLabel } </ SelectValue >
144- </ SelectTrigger >
145- < SelectContent >
146- < SelectItem value = "all" > All Departments</ SelectItem >
147- { departments . map ( ( dept ) => (
148- < SelectItem key = { dept } value = { dept } >
149- { formatDepartment ( dept ) }
150- </ SelectItem >
151- ) ) }
152- </ SelectContent >
153- </ Select >
134+ { /* Filters - side by side on mobile, inline with search on desktop */ }
135+ < div className = "flex gap-2" >
136+ < div className = "flex-1 md:w-[160px] md:flex-none" >
137+ < Select
138+ value = { statusFilter }
139+ onValueChange = { ( v ) => setStatusFilter ( ( v ?? 'all' ) as PolicyStatus | 'all' | 'archived' ) }
140+ >
141+ < SelectTrigger >
142+ < SelectValue placeholder = "Status" > { statusLabel } </ SelectValue >
143+ </ SelectTrigger >
144+ < SelectContent >
145+ { STATUS_OPTIONS . map ( ( opt ) => (
146+ < SelectItem key = { opt . value } value = { opt . value } >
147+ { opt . label }
148+ </ SelectItem >
149+ ) ) }
150+ </ SelectContent >
151+ </ Select >
152+ </ div >
153+ < div className = "flex-1 md:w-[160px] md:flex-none" >
154+ < Select value = { departmentFilter } onValueChange = { ( v ) => setDepartmentFilter ( v ?? 'all' ) } >
155+ < SelectTrigger >
156+ < SelectValue placeholder = "Department" > { departmentLabel } </ SelectValue >
157+ </ SelectTrigger >
158+ < SelectContent >
159+ < SelectItem value = "all" > All Departments</ SelectItem >
160+ { departments . map ( ( dept ) => (
161+ < SelectItem key = { dept } value = { dept } >
162+ { formatDepartment ( dept ) }
163+ </ SelectItem >
164+ ) ) }
165+ </ SelectContent >
166+ </ Select >
167+ </ div >
154168 </ div >
155169 </ div >
156- </ div >
157- < PoliciesTableDS
158- policies = { filteredPolicies }
159- sortColumn = { sortColumn }
160- sortDirection = { sortDirection }
161- onSort = { handleSort }
162- />
163- </ Stack >
170+ { showTailoringBanner && progress !== null && (
171+ < div className = "flex items-center gap-3 rounded-xl border border-primary/20 bg-linear-to-r from-primary/10 via-primary/5 to-transparent px-4 py-3" >
172+ < div className = "flex h-10 w-10 items-center justify-center rounded-full bg-primary/15 text-primary" >
173+ < Loader2 className = "h-5 w-5 animate-spin" />
174+ </ div >
175+ < div className = "flex flex-col" >
176+ < span className = "text-sm font-medium text-primary" > Tailoring your policies</ span >
177+ < span className = "text-xs text-muted-foreground" >
178+ Personalized { progress . completed } /{ progress . total } policies
179+ </ span >
180+ </ div >
181+ </ div >
182+ ) }
183+ < PoliciesTableDS
184+ policies = { filteredPolicies }
185+ sortColumn = { sortColumn }
186+ sortDirection = { sortDirection }
187+ onSort = { handleSort }
188+ />
189+ </ Stack >
190+ </ PolicyTailoringProvider >
164191 ) ;
165192}
0 commit comments