Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/internal/features/supertokens/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var (
"container:create", "container:read", "container:update", "container:delete",
"audit:create", "audit:read", "audit:update", "audit:delete",
"terminal:create", "terminal:read", "terminal:update", "terminal:delete",
"feature_flags:read", "feature_flags:update",
"dashboard:read",
}

Expand All @@ -54,11 +55,12 @@ var (
"notification:read",
"file-manager:read",
"deploy:read",
"feature_flags:read",
"dashboard:read",
}

viewerPermissions = []string{
"user:read", "organization:read", "container:read", "audit:read", "domain:read", "notification:read", "file-manager:read", "deploy:read", "dashboard:read",
"user:read", "organization:read", "container:read", "audit:read", "domain:read", "notification:read", "file-manager:read", "deploy:read", "feature_flags:read", "dashboard:read",
}
)

Expand Down
219 changes: 182 additions & 37 deletions view/app/settings/general/components/FeatureFlagsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,23 @@ import {
import { Separator } from '@/components/ui/separator';
import { FeatureFlag, FeatureName, featureGroups } from '@/types/feature-flags';
import { RBACGuard } from '@/components/rbac/RBACGuard';
import { TypographySmall, TypographyMuted } from '@/components/ui/typography';
import { TypographySmall, TypographyMuted, TypographyH3 } from '@/components/ui/typography';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
Server,
Code,
BarChart3,
Bell,
CheckCircle2,
XCircle,
Search,
Filter,
Settings
} from 'lucide-react';
import { useState } from 'react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';

export default function FeatureFlagsSettings() {
const { t } = useTranslation();
Expand All @@ -20,6 +36,8 @@ export default function FeatureFlagsSettings() {
skip: !activeOrganization?.id
});
const [updateFeatureFlag] = useUpdateFeatureFlagMutation();
const [searchTerm, setSearchTerm] = useState('');
const [filterEnabled, setFilterEnabled] = useState<'all' | 'enabled' | 'disabled'>('all');

const handleToggleFeature = async (featureName: string, isEnabled: boolean) => {
try {
Expand All @@ -33,13 +51,36 @@ export default function FeatureFlagsSettings() {
}
};

if (isLoading) {
return <div>{t('common.loading')}</div>;
}
const getGroupIcon = (group: string) => {
const iconMap = {
infrastructure: Server,
development: Code,
monitoring: BarChart3,
notifications: Bell
};
return iconMap[group as keyof typeof iconMap] || Settings;
};

const getFilteredFeatures = () => {
if (!featureFlags) return [];

return featureFlags.filter((feature) => {
const matchesSearch = feature.feature_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
t(`settings.featureFlags.features.${feature.feature_name}.title`).toLowerCase().includes(searchTerm.toLowerCase());

const matchesFilter = filterEnabled === 'all' ||
(filterEnabled === 'enabled' && feature.is_enabled) ||
(filterEnabled === 'disabled' && !feature.is_enabled);

return matchesSearch && matchesFilter;
});
};

const getGroupedFeatures = () => {
const filteredFeatures = getFilteredFeatures();
const grouped = new Map<string, FeatureFlag[]>();
featureFlags?.forEach((feature) => {

filteredFeatures.forEach((feature) => {
for (const [group, features] of Object.entries(featureGroups)) {
if (features.includes(feature.feature_name as FeatureName)) {
if (!grouped.has(group)) {
Expand All @@ -54,51 +95,155 @@ export default function FeatureFlagsSettings() {
};

const groupedFeatures = getGroupedFeatures();
const totalFeatures = featureFlags?.length || 0;
const enabledFeatures = featureFlags?.filter(f => f.is_enabled).length || 0;
const disabledFeatures = totalFeatures - enabledFeatures;

if (isLoading) {
return (
<TabsContent value="feature-flags" className="space-y-6 mt-4">
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Settings className="h-5 w-5" />
<TypographyH3>{t('settings.featureFlags.title')}</TypographyH3>
</div>
<TypographyMuted>{t('settings.featureFlags.description')}</TypographyMuted>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="animate-pulse">
<div className="h-4 bg-muted rounded w-1/4 mb-2"></div>
<div className="space-y-2">
{[1, 2].map((j) => (
<div key={j} className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-2">
<div className="h-4 bg-muted rounded w-32"></div>
<div className="h-3 bg-muted rounded w-48"></div>
</div>
<div className="h-6 w-11 bg-muted rounded-full"></div>
</div>
))}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
);
}

return (
<RBACGuard resource="feature-flags" action="read">
<TabsContent value="feature-flags" className="space-y-6 mt-4">
<Card>
<CardHeader>
<TypographySmall>{t('settings.featureFlags.title')}</TypographySmall>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TypographyH3>{t('settings.featureFlags.title')}</TypographyH3>
</div>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="flex items-center gap-1">
<CheckCircle2 className="h-3 w-3" />
{enabledFeatures}
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<XCircle className="h-3 w-3" />
{disabledFeatures}
</Badge>
</div>
</div>
<TypographyMuted>{t('settings.featureFlags.description')}</TypographyMuted>
</CardHeader>
<CardContent className="space-y-6">
{Array.from(groupedFeatures.entries()).map(([group, features], index) => (
<div key={group} className="space-y-4">
<div className="space-y-2">
<TypographySmall>
{t(`settings.featureFlags.groups.${group}.title`)}
</TypographySmall>
</div>
<div className="space-y-4">
{features?.map((feature) => (
<div
key={feature.feature_name}
className="flex items-center justify-between p-2 rounded-lg"
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('settings.featureFlags.searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex items-center gap-2">
<div className="flex gap-1">
{(['all', 'enabled', 'disabled'] as const).map((filter) => (
<Button
key={filter}
variant={filterEnabled === filter ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterEnabled(filter)}
>
<div className="space-y-1">
<TypographySmall>
{t(`settings.featureFlags.features.${feature.feature_name}.title`)}
</TypographySmall>
<TypographyMuted>
{t(`settings.featureFlags.features.${feature.feature_name}.description`)}
</TypographyMuted>
</div>
<RBACGuard resource="feature-flags" action="update">
<Switch
checked={feature.is_enabled}
onCheckedChange={(checked) =>
handleToggleFeature(feature.feature_name, checked)
}
/>
</RBACGuard>
</div>
{t(`settings.featureFlags.filters.${filter}`)}
</Button>
))}
</div>
{index !== groupedFeatures.size - 1 && <Separator />}
</div>
))}
</div>

{groupedFeatures.size === 0 ? (
<Alert>
<Search className="h-4 w-4" />
<AlertDescription>
{searchTerm || filterEnabled !== 'all'
? t('settings.featureFlags.noResults')
: t('settings.featureFlags.noFeatures')
}
</AlertDescription>
</Alert>
) : (
Array.from(groupedFeatures.entries()).map(([group, features], index) => {
const GroupIcon = getGroupIcon(group);
const enabledInGroup = features.filter(f => f.is_enabled).length;

return (
<div key={group} className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<GroupIcon className="h-4 w-4 text-muted-foreground" />
<TypographySmall className="font-semibold">
{t(`settings.featureFlags.groups.${group}.title`)}
</TypographySmall>
<Badge variant="outline" className="text-xs">
{enabledInGroup}/{features.length}
</Badge>
</div>
</div>
<div className="space-y-3">
{features?.map((feature) => (
<div
key={feature.feature_name}
className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${'bg-muted/30 border-border'}`}
>
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
<TypographySmall className="font-medium">
{t(`settings.featureFlags.features.${feature.feature_name}.title`)}
</TypographySmall>
</div>
<TypographyMuted className="text-sm">
{t(`settings.featureFlags.features.${feature.feature_name}.description`)}
</TypographyMuted>
</div>
<RBACGuard resource="feature-flags" action="update">
<Switch
checked={feature.is_enabled}
onCheckedChange={(checked) =>
handleToggleFeature(feature.feature_name, checked)
}
/>
</RBACGuard>
</div>
))}
</div>
{index !== groupedFeatures.size - 1 && <Separator />}
</div>
);
})
)}
</CardContent>
</Card>
</TabsContent>
Expand Down
14 changes: 12 additions & 2 deletions view/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"featureNotAvailable": "This feature is not available for your organization",
"goBack": "Go Back",
"refreshPage": "Refresh Page",
"loading": "Loading..."
"loading": "Loading...",
"enabled": "Enabled",
"disabled": "Disabled"
},
"containers": {
"title": "Containers",
Expand Down Expand Up @@ -287,7 +289,15 @@
"messages": {
"updated": "Feature flag updated successfully",
"updateFailed": "Failed to update feature flag"
}
},
"searchPlaceholder": "Search features...",
"filters": {
"all": "All",
"enabled": "Enabled",
"disabled": "Disabled"
},
"noResults": "No features found matching your search criteria",
"noFeatures": "No features available"
},
"account": {
"title": "Account Information",
Expand Down
14 changes: 12 additions & 2 deletions view/lib/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"featureNotAvailable": "Esta característica no está disponible para tu organización",
"goBack": "Volver",
"refreshPage": "Actualizar página",
"loading": "Cargando..."
"loading": "Cargando...",
"enabled": "Habilitado",
"disabled": "Deshabilitado"
},
"containers": {
"title": "Contenedores",
Expand Down Expand Up @@ -286,7 +288,15 @@
"messages": {
"updated": "Característica actualizada exitosamente",
"updateFailed": "Error al actualizar la característica"
}
},
"searchPlaceholder": "Buscar características...",
"filters": {
"all": "Todas",
"enabled": "Habilitadas",
"disabled": "Deshabilitadas"
},
"noResults": "No se encontraron características que coincidan con tu búsqueda",
"noFeatures": "No hay características disponibles"
},
"account": {
"title": "Información de la Cuenta",
Expand Down
14 changes: 12 additions & 2 deletions view/lib/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"featureNotAvailable": "Cette caractéristique n'est pas disponible pour votre organisation",
"goBack": "Retour",
"refreshPage": "Actualiser la page",
"loading": "Chargement..."
"loading": "Chargement...",
"enabled": "Activé",
"disabled": "Désactivé"
},
"containers": {
"title": "Conteneurs",
Expand Down Expand Up @@ -287,7 +289,15 @@
"messages": {
"updated": "Caractéristique mise à jour avec succès",
"updateFailed": "Échec de la mise à jour de la caractéristique"
}
},
"searchPlaceholder": "Rechercher des caractéristiques...",
"filters": {
"all": "Toutes",
"enabled": "Activées",
"disabled": "Désactivées"
},
"noResults": "Aucune caractéristique trouvée correspondant à votre recherche",
"noFeatures": "Aucune caractéristique disponible"
},
"account": {
"title": "Informations du Compte",
Expand Down
14 changes: 12 additions & 2 deletions view/lib/i18n/locales/kn.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"featureNotAvailable": "ಈ ವೈಶಿಷ್ಟ್ಯ ನಿಮ್ಮ ಸಂಸ್ಥೆಗೆ ಲಭ್ಯವಿಲ್ಲ",
"goBack": "ಹಿಂತಿರುಗಿ",
"refreshPage": "ಪುಟವನ್ನು ಪುನರಾವರ್ತನಿಸಿ",
"loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ..."
"loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ...",
"enabled": "ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ",
"disabled": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"
},
"containers": {
"title": "ಕನ್ಟೇನರ್ಗಳು",
Expand Down Expand Up @@ -287,7 +289,15 @@
"messages": {
"updated": "ವೈಶಿಷ್ಟ್ಯಗಳು ಸಫಲವಾಗಿ ನವೀಕರಿಸಲಾಗಿದೆ",
"updateFailed": "ವೈಶಿಷ್ಟ್ಯಗಳು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ"
}
},
"searchPlaceholder": "ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಹುಡುಕಿ...",
"filters": {
"all": "ಎಲ್ಲಾ",
"enabled": "ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ",
"disabled": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"
},
"noResults": "ನಿಮ್ಮ ಹುಡುಕಾಟದ ಮಾನದಂಡಗಳಿಗೆ ಹೊಂದಾಣಿಕೆಯಾಗುವ ವೈಶಿಷ್ಟ್ಯಗಳು ಕಂಡುಬಂದಿಲ್ಲ",
"noFeatures": "ಯಾವುದೇ ವೈಶಿಷ್ಟ್ಯಗಳು ಲಭ್ಯವಿಲ್ಲ"
},
"account": {
"title": "ಖಾತೆ ಮಾಹಿತಿ",
Expand Down
Loading
Loading