Skip to content

Commit 2e1c857

Browse files
authored
fix: feature flag ui and feature flag writes missing RBAC permissions (#493)
* fix: feature flag write operation permissions * ui: update feature flag display styles
1 parent 71122a6 commit 2e1c857

File tree

7 files changed

+245
-48
lines changed

7 files changed

+245
-48
lines changed

api/internal/features/supertokens/auth.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ var (
4242
"container:create", "container:read", "container:update", "container:delete",
4343
"audit:create", "audit:read", "audit:update", "audit:delete",
4444
"terminal:create", "terminal:read", "terminal:update", "terminal:delete",
45+
"feature_flags:read", "feature_flags:update",
4546
"dashboard:read",
4647
}
4748

@@ -54,11 +55,12 @@ var (
5455
"notification:read",
5556
"file-manager:read",
5657
"deploy:read",
58+
"feature_flags:read",
5759
"dashboard:read",
5860
}
5961

6062
viewerPermissions = []string{
61-
"user:read", "organization:read", "container:read", "audit:read", "domain:read", "notification:read", "file-manager:read", "deploy:read", "dashboard:read",
63+
"user:read", "organization:read", "container:read", "audit:read", "domain:read", "notification:read", "file-manager:read", "deploy:read", "feature_flags:read", "dashboard:read",
6264
}
6365
)
6466

view/app/settings/general/components/FeatureFlagsSettings.tsx

Lines changed: 182 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,23 @@ import {
1111
import { Separator } from '@/components/ui/separator';
1212
import { FeatureFlag, FeatureName, featureGroups } from '@/types/feature-flags';
1313
import { 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

1632
export 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>

view/lib/i18n/locales/en.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
"featureNotAvailable": "This feature is not available for your organization",
1313
"goBack": "Go Back",
1414
"refreshPage": "Refresh Page",
15-
"loading": "Loading..."
15+
"loading": "Loading...",
16+
"enabled": "Enabled",
17+
"disabled": "Disabled"
1618
},
1719
"containers": {
1820
"title": "Containers",
@@ -287,7 +289,15 @@
287289
"messages": {
288290
"updated": "Feature flag updated successfully",
289291
"updateFailed": "Failed to update feature flag"
290-
}
292+
},
293+
"searchPlaceholder": "Search features...",
294+
"filters": {
295+
"all": "All",
296+
"enabled": "Enabled",
297+
"disabled": "Disabled"
298+
},
299+
"noResults": "No features found matching your search criteria",
300+
"noFeatures": "No features available"
291301
},
292302
"account": {
293303
"title": "Account Information",

view/lib/i18n/locales/es.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"featureNotAvailable": "Esta característica no está disponible para tu organización",
1212
"goBack": "Volver",
1313
"refreshPage": "Actualizar página",
14-
"loading": "Cargando..."
14+
"loading": "Cargando...",
15+
"enabled": "Habilitado",
16+
"disabled": "Deshabilitado"
1517
},
1618
"containers": {
1719
"title": "Contenedores",
@@ -286,7 +288,15 @@
286288
"messages": {
287289
"updated": "Característica actualizada exitosamente",
288290
"updateFailed": "Error al actualizar la característica"
289-
}
291+
},
292+
"searchPlaceholder": "Buscar características...",
293+
"filters": {
294+
"all": "Todas",
295+
"enabled": "Habilitadas",
296+
"disabled": "Deshabilitadas"
297+
},
298+
"noResults": "No se encontraron características que coincidan con tu búsqueda",
299+
"noFeatures": "No hay características disponibles"
290300
},
291301
"account": {
292302
"title": "Información de la Cuenta",

view/lib/i18n/locales/fr.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
"featureNotAvailable": "Cette caractéristique n'est pas disponible pour votre organisation",
1313
"goBack": "Retour",
1414
"refreshPage": "Actualiser la page",
15-
"loading": "Chargement..."
15+
"loading": "Chargement...",
16+
"enabled": "Activé",
17+
"disabled": "Désactivé"
1618
},
1719
"containers": {
1820
"title": "Conteneurs",
@@ -287,7 +289,15 @@
287289
"messages": {
288290
"updated": "Caractéristique mise à jour avec succès",
289291
"updateFailed": "Échec de la mise à jour de la caractéristique"
290-
}
292+
},
293+
"searchPlaceholder": "Rechercher des caractéristiques...",
294+
"filters": {
295+
"all": "Toutes",
296+
"enabled": "Activées",
297+
"disabled": "Désactivées"
298+
},
299+
"noResults": "Aucune caractéristique trouvée correspondant à votre recherche",
300+
"noFeatures": "Aucune caractéristique disponible"
291301
},
292302
"account": {
293303
"title": "Informations du Compte",

view/lib/i18n/locales/kn.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
"featureNotAvailable": "ಈ ವೈಶಿಷ್ಟ್ಯ ನಿಮ್ಮ ಸಂಸ್ಥೆಗೆ ಲಭ್ಯವಿಲ್ಲ",
1313
"goBack": "ಹಿಂತಿರುಗಿ",
1414
"refreshPage": "ಪುಟವನ್ನು ಪುನರಾವರ್ತನಿಸಿ",
15-
"loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ..."
15+
"loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ...",
16+
"enabled": "ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ",
17+
"disabled": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"
1618
},
1719
"containers": {
1820
"title": "ಕನ್ಟೇನರ್ಗಳು",
@@ -287,7 +289,15 @@
287289
"messages": {
288290
"updated": "ವೈಶಿಷ್ಟ್ಯಗಳು ಸಫಲವಾಗಿ ನವೀಕರಿಸಲಾಗಿದೆ",
289291
"updateFailed": "ವೈಶಿಷ್ಟ್ಯಗಳು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ"
290-
}
292+
},
293+
"searchPlaceholder": "ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಹುಡುಕಿ...",
294+
"filters": {
295+
"all": "ಎಲ್ಲಾ",
296+
"enabled": "ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ",
297+
"disabled": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"
298+
},
299+
"noResults": "ನಿಮ್ಮ ಹುಡುಕಾಟದ ಮಾನದಂಡಗಳಿಗೆ ಹೊಂದಾಣಿಕೆಯಾಗುವ ವೈಶಿಷ್ಟ್ಯಗಳು ಕಂಡುಬಂದಿಲ್ಲ",
300+
"noFeatures": "ಯಾವುದೇ ವೈಶಿಷ್ಟ್ಯಗಳು ಲಭ್ಯವಿಲ್ಲ"
291301
},
292302
"account": {
293303
"title": "ಖಾತೆ ಮಾಹಿತಿ",

0 commit comments

Comments
 (0)