@@ -11,7 +11,23 @@ import {
1111import { Separator } from '@/components/ui/separator' ;
1212import { FeatureFlag , FeatureName , featureGroups } from '@/types/feature-flags' ;
1313import { RBACGuard } from '@/components/rbac/RBACGuard' ;
14- import { TypographySmall , TypographyMuted } from '@/components/ui/typography' ;
14+ import { TypographySmall , TypographyMuted , TypographyH3 } from '@/components/ui/typography' ;
15+ import { Badge } from '@/components/ui/badge' ;
16+ import { Alert , AlertDescription } from '@/components/ui/alert' ;
17+ import {
18+ Server ,
19+ Code ,
20+ BarChart3 ,
21+ Bell ,
22+ CheckCircle2 ,
23+ XCircle ,
24+ Search ,
25+ Filter ,
26+ Settings
27+ } from 'lucide-react' ;
28+ import { useState } from 'react' ;
29+ import { Input } from '@/components/ui/input' ;
30+ import { Button } from '@/components/ui/button' ;
1531
1632export default function FeatureFlagsSettings ( ) {
1733 const { t } = useTranslation ( ) ;
@@ -20,6 +36,8 @@ export default function FeatureFlagsSettings() {
2036 skip : ! activeOrganization ?. id
2137 } ) ;
2238 const [ updateFeatureFlag ] = useUpdateFeatureFlagMutation ( ) ;
39+ const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
40+ const [ filterEnabled , setFilterEnabled ] = useState < 'all' | 'enabled' | 'disabled' > ( 'all' ) ;
2341
2442 const handleToggleFeature = async ( featureName : string , isEnabled : boolean ) => {
2543 try {
@@ -33,13 +51,36 @@ export default function FeatureFlagsSettings() {
3351 }
3452 } ;
3553
36- if ( isLoading ) {
37- return < div > { t ( 'common.loading' ) } </ div > ;
38- }
54+ const getGroupIcon = ( group : string ) => {
55+ const iconMap = {
56+ infrastructure : Server ,
57+ development : Code ,
58+ monitoring : BarChart3 ,
59+ notifications : Bell
60+ } ;
61+ return iconMap [ group as keyof typeof iconMap ] || Settings ;
62+ } ;
63+
64+ const getFilteredFeatures = ( ) => {
65+ if ( ! featureFlags ) return [ ] ;
66+
67+ return featureFlags . filter ( ( feature ) => {
68+ const matchesSearch = feature . feature_name . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
69+ t ( `settings.featureFlags.features.${ feature . feature_name } .title` ) . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ;
70+
71+ const matchesFilter = filterEnabled === 'all' ||
72+ ( filterEnabled === 'enabled' && feature . is_enabled ) ||
73+ ( filterEnabled === 'disabled' && ! feature . is_enabled ) ;
74+
75+ return matchesSearch && matchesFilter ;
76+ } ) ;
77+ } ;
3978
4079 const getGroupedFeatures = ( ) => {
80+ const filteredFeatures = getFilteredFeatures ( ) ;
4181 const grouped = new Map < string , FeatureFlag [ ] > ( ) ;
42- featureFlags ?. forEach ( ( feature ) => {
82+
83+ filteredFeatures . forEach ( ( feature ) => {
4384 for ( const [ group , features ] of Object . entries ( featureGroups ) ) {
4485 if ( features . includes ( feature . feature_name as FeatureName ) ) {
4586 if ( ! grouped . has ( group ) ) {
@@ -54,51 +95,155 @@ export default function FeatureFlagsSettings() {
5495 } ;
5596
5697 const groupedFeatures = getGroupedFeatures ( ) ;
98+ const totalFeatures = featureFlags ?. length || 0 ;
99+ const enabledFeatures = featureFlags ?. filter ( f => f . is_enabled ) . length || 0 ;
100+ const disabledFeatures = totalFeatures - enabledFeatures ;
101+
102+ if ( isLoading ) {
103+ return (
104+ < TabsContent value = "feature-flags" className = "space-y-6 mt-4" >
105+ < Card >
106+ < CardHeader >
107+ < div className = "flex items-center gap-2" >
108+ < Settings className = "h-5 w-5" />
109+ < TypographyH3 > { t ( 'settings.featureFlags.title' ) } </ TypographyH3 >
110+ </ div >
111+ < TypographyMuted > { t ( 'settings.featureFlags.description' ) } </ TypographyMuted >
112+ </ CardHeader >
113+ < CardContent >
114+ < div className = "space-y-4" >
115+ { [ 1 , 2 , 3 ] . map ( ( i ) => (
116+ < div key = { i } className = "animate-pulse" >
117+ < div className = "h-4 bg-muted rounded w-1/4 mb-2" > </ div >
118+ < div className = "space-y-2" >
119+ { [ 1 , 2 ] . map ( ( j ) => (
120+ < div key = { j } className = "flex items-center justify-between p-4 border rounded-lg" >
121+ < div className = "space-y-2" >
122+ < div className = "h-4 bg-muted rounded w-32" > </ div >
123+ < div className = "h-3 bg-muted rounded w-48" > </ div >
124+ </ div >
125+ < div className = "h-6 w-11 bg-muted rounded-full" > </ div >
126+ </ div >
127+ ) ) }
128+ </ div >
129+ </ div >
130+ ) ) }
131+ </ div >
132+ </ CardContent >
133+ </ Card >
134+ </ TabsContent >
135+ ) ;
136+ }
57137
58138 return (
59139 < RBACGuard resource = "feature-flags" action = "read" >
60140 < TabsContent value = "feature-flags" className = "space-y-6 mt-4" >
61141 < Card >
62142 < CardHeader >
63- < TypographySmall > { t ( 'settings.featureFlags.title' ) } </ TypographySmall >
143+ < div className = "flex items-center justify-between" >
144+ < div className = "flex items-center gap-2" >
145+ < TypographyH3 > { t ( 'settings.featureFlags.title' ) } </ TypographyH3 >
146+ </ div >
147+ < div className = "flex items-center gap-2" >
148+ < Badge variant = "secondary" className = "flex items-center gap-1" >
149+ < CheckCircle2 className = "h-3 w-3" />
150+ { enabledFeatures }
151+ </ Badge >
152+ < Badge variant = "outline" className = "flex items-center gap-1" >
153+ < XCircle className = "h-3 w-3" />
154+ { disabledFeatures }
155+ </ Badge >
156+ </ div >
157+ </ div >
64158 < TypographyMuted > { t ( 'settings.featureFlags.description' ) } </ TypographyMuted >
65159 </ CardHeader >
66160 < CardContent className = "space-y-6" >
67- { Array . from ( groupedFeatures . entries ( ) ) . map ( ( [ group , features ] , index ) => (
68- < div key = { group } className = "space-y-4" >
69- < div className = "space-y-2" >
70- < TypographySmall >
71- { t ( `settings.featureFlags.groups.${ group } .title` ) }
72- </ TypographySmall >
73- </ div >
74- < div className = "space-y-4" >
75- { features ?. map ( ( feature ) => (
76- < div
77- key = { feature . feature_name }
78- className = "flex items-center justify-between p-2 rounded-lg"
161+ < div className = "flex items-center gap-4" >
162+ < div className = "relative flex-1" >
163+ < Search className = "absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
164+ < Input
165+ placeholder = { t ( 'settings.featureFlags.searchPlaceholder' ) }
166+ value = { searchTerm }
167+ onChange = { ( e ) => setSearchTerm ( e . target . value ) }
168+ className = "pl-10"
169+ />
170+ </ div >
171+ < div className = "flex items-center gap-2" >
172+ < div className = "flex gap-1" >
173+ { ( [ 'all' , 'enabled' , 'disabled' ] as const ) . map ( ( filter ) => (
174+ < Button
175+ key = { filter }
176+ variant = { filterEnabled === filter ? 'default' : 'outline' }
177+ size = "sm"
178+ onClick = { ( ) => setFilterEnabled ( filter ) }
79179 >
80- < div className = "space-y-1" >
81- < TypographySmall >
82- { t ( `settings.featureFlags.features.${ feature . feature_name } .title` ) }
83- </ TypographySmall >
84- < TypographyMuted >
85- { t ( `settings.featureFlags.features.${ feature . feature_name } .description` ) }
86- </ TypographyMuted >
87- </ div >
88- < RBACGuard resource = "feature-flags" action = "update" >
89- < Switch
90- checked = { feature . is_enabled }
91- onCheckedChange = { ( checked ) =>
92- handleToggleFeature ( feature . feature_name , checked )
93- }
94- />
95- </ RBACGuard >
96- </ div >
180+ { t ( `settings.featureFlags.filters.${ filter } ` ) }
181+ </ Button >
97182 ) ) }
98183 </ div >
99- { index !== groupedFeatures . size - 1 && < Separator /> }
100184 </ div >
101- ) ) }
185+ </ div >
186+
187+ { groupedFeatures . size === 0 ? (
188+ < Alert >
189+ < Search className = "h-4 w-4" />
190+ < AlertDescription >
191+ { searchTerm || filterEnabled !== 'all'
192+ ? t ( 'settings.featureFlags.noResults' )
193+ : t ( 'settings.featureFlags.noFeatures' )
194+ }
195+ </ AlertDescription >
196+ </ Alert >
197+ ) : (
198+ Array . from ( groupedFeatures . entries ( ) ) . map ( ( [ group , features ] , index ) => {
199+ const GroupIcon = getGroupIcon ( group ) ;
200+ const enabledInGroup = features . filter ( f => f . is_enabled ) . length ;
201+
202+ return (
203+ < div key = { group } className = "space-y-4" >
204+ < div className = "flex items-center justify-between" >
205+ < div className = "flex items-center gap-2" >
206+ < GroupIcon className = "h-4 w-4 text-muted-foreground" />
207+ < TypographySmall className = "font-semibold" >
208+ { t ( `settings.featureFlags.groups.${ group } .title` ) }
209+ </ TypographySmall >
210+ < Badge variant = "outline" className = "text-xs" >
211+ { enabledInGroup } /{ features . length }
212+ </ Badge >
213+ </ div >
214+ </ div >
215+ < div className = "space-y-3" >
216+ { features ?. map ( ( feature ) => (
217+ < div
218+ key = { feature . feature_name }
219+ className = { `flex items-center justify-between p-4 rounded-lg border transition-colors ${ 'bg-muted/30 border-border' } ` }
220+ >
221+ < div className = "space-y-1 flex-1" >
222+ < div className = "flex items-center gap-2" >
223+ < TypographySmall className = "font-medium" >
224+ { t ( `settings.featureFlags.features.${ feature . feature_name } .title` ) }
225+ </ TypographySmall >
226+ </ div >
227+ < TypographyMuted className = "text-sm" >
228+ { t ( `settings.featureFlags.features.${ feature . feature_name } .description` ) }
229+ </ TypographyMuted >
230+ </ div >
231+ < RBACGuard resource = "feature-flags" action = "update" >
232+ < Switch
233+ checked = { feature . is_enabled }
234+ onCheckedChange = { ( checked ) =>
235+ handleToggleFeature ( feature . feature_name , checked )
236+ }
237+ />
238+ </ RBACGuard >
239+ </ div >
240+ ) ) }
241+ </ div >
242+ { index !== groupedFeatures . size - 1 && < Separator /> }
243+ </ div >
244+ ) ;
245+ } )
246+ ) }
102247 </ CardContent >
103248 </ Card >
104249 </ TabsContent >
0 commit comments