diff --git a/api/internal/features/supertokens/auth.go b/api/internal/features/supertokens/auth.go index 0cdd75976..e15b5c981 100644 --- a/api/internal/features/supertokens/auth.go +++ b/api/internal/features/supertokens/auth.go @@ -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", } @@ -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", } ) diff --git a/view/app/settings/general/components/FeatureFlagsSettings.tsx b/view/app/settings/general/components/FeatureFlagsSettings.tsx index 3f41bd35d..8a9a603b0 100644 --- a/view/app/settings/general/components/FeatureFlagsSettings.tsx +++ b/view/app/settings/general/components/FeatureFlagsSettings.tsx @@ -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(); @@ -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 { @@ -33,13 +51,36 @@ export default function FeatureFlagsSettings() { } }; - if (isLoading) { - return
{t('common.loading')}
; - } + 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(); - 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)) { @@ -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 ( + + + +
+ + {t('settings.featureFlags.title')} +
+ {t('settings.featureFlags.description')} +
+ +
+ {[1, 2, 3].map((i) => ( +
+
+
+ {[1, 2].map((j) => ( +
+
+
+
+
+
+
+ ))} +
+
+ ))} +
+
+
+
+ ); + } return ( - {t('settings.featureFlags.title')} +
+
+ {t('settings.featureFlags.title')} +
+
+ + + {enabledFeatures} + + + + {disabledFeatures} + +
+
{t('settings.featureFlags.description')}
- {Array.from(groupedFeatures.entries()).map(([group, features], index) => ( -
-
- - {t(`settings.featureFlags.groups.${group}.title`)} - -
-
- {features?.map((feature) => ( -
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+
+ {(['all', 'enabled', 'disabled'] as const).map((filter) => ( +
+ {t(`settings.featureFlags.filters.${filter}`)} + ))}
- {index !== groupedFeatures.size - 1 && }
- ))} +
+ + {groupedFeatures.size === 0 ? ( + + + + {searchTerm || filterEnabled !== 'all' + ? t('settings.featureFlags.noResults') + : t('settings.featureFlags.noFeatures') + } + + + ) : ( + Array.from(groupedFeatures.entries()).map(([group, features], index) => { + const GroupIcon = getGroupIcon(group); + const enabledInGroup = features.filter(f => f.is_enabled).length; + + return ( +
+
+
+ + + {t(`settings.featureFlags.groups.${group}.title`)} + + + {enabledInGroup}/{features.length} + +
+
+
+ {features?.map((feature) => ( +
+
+
+ + {t(`settings.featureFlags.features.${feature.feature_name}.title`)} + +
+ + {t(`settings.featureFlags.features.${feature.feature_name}.description`)} + +
+ + + handleToggleFeature(feature.feature_name, checked) + } + /> + +
+ ))} +
+ {index !== groupedFeatures.size - 1 && } +
+ ); + }) + )} diff --git a/view/lib/i18n/locales/en.json b/view/lib/i18n/locales/en.json index 1e2405a27..60eda2a87 100644 --- a/view/lib/i18n/locales/en.json +++ b/view/lib/i18n/locales/en.json @@ -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", @@ -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", diff --git a/view/lib/i18n/locales/es.json b/view/lib/i18n/locales/es.json index a8342db6b..bdf7f3dce 100644 --- a/view/lib/i18n/locales/es.json +++ b/view/lib/i18n/locales/es.json @@ -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", @@ -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", diff --git a/view/lib/i18n/locales/fr.json b/view/lib/i18n/locales/fr.json index 5b752ef6b..d33897551 100644 --- a/view/lib/i18n/locales/fr.json +++ b/view/lib/i18n/locales/fr.json @@ -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", @@ -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", diff --git a/view/lib/i18n/locales/kn.json b/view/lib/i18n/locales/kn.json index 613ef75a2..38ca25f60 100644 --- a/view/lib/i18n/locales/kn.json +++ b/view/lib/i18n/locales/kn.json @@ -12,7 +12,9 @@ "featureNotAvailable": "ಈ ವೈಶಿಷ್ಟ್ಯ ನಿಮ್ಮ ಸಂಸ್ಥೆಗೆ ಲಭ್ಯವಿಲ್ಲ", "goBack": "ಹಿಂತಿರುಗಿ", "refreshPage": "ಪುಟವನ್ನು ಪುನರಾವರ್ತನಿಸಿ", - "loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ..." + "loading": "ಲೋಡ್ ಆಗುತ್ತಿದೆ...", + "enabled": "ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ", + "disabled": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ" }, "containers": { "title": "ಕನ್ಟೇನರ್ಗಳು", @@ -287,7 +289,15 @@ "messages": { "updated": "ವೈಶಿಷ್ಟ್ಯಗಳು ಸಫಲವಾಗಿ ನವೀಕರಿಸಲಾಗಿದೆ", "updateFailed": "ವೈಶಿಷ್ಟ್ಯಗಳು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ" - } + }, + "searchPlaceholder": "ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಹುಡುಕಿ...", + "filters": { + "all": "ಎಲ್ಲಾ", + "enabled": "ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ", + "disabled": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ" + }, + "noResults": "ನಿಮ್ಮ ಹುಡುಕಾಟದ ಮಾನದಂಡಗಳಿಗೆ ಹೊಂದಾಣಿಕೆಯಾಗುವ ವೈಶಿಷ್ಟ್ಯಗಳು ಕಂಡುಬಂದಿಲ್ಲ", + "noFeatures": "ಯಾವುದೇ ವೈಶಿಷ್ಟ್ಯಗಳು ಲಭ್ಯವಿಲ್ಲ" }, "account": { "title": "ಖಾತೆ ಮಾಹಿತಿ", diff --git a/view/lib/i18n/locales/ml.json b/view/lib/i18n/locales/ml.json index 7d9b2f00c..27832d922 100644 --- a/view/lib/i18n/locales/ml.json +++ b/view/lib/i18n/locales/ml.json @@ -12,7 +12,9 @@ "featureNotAvailable": "ഈ സവിശേഷത നിങ്ങളുടെ സ്ഥാപനത്തിന് ലഭ്യമല്ല", "goBack": "മടങ്ങുക", "refreshPage": "പേജ് പുതുക്കുക", - "loading": "ലോഡുചെയ്യുന്നു..." + "loading": "ലോഡുചെയ്യുന്നു...", + "enabled": "പ്രവർത്തനസജ്ജമാക്കി", + "disabled": "പ്രവർത്തനരഹിതമാക്കി" }, "containers": { "title": "കണ്ടെയ്‌നറുകൾ", @@ -286,7 +288,15 @@ "messages": { "updated": "ഫീച്ചർ അപ്ഡേറ്റ് ചെയ്തു", "updateFailed": "ഫീച്ചർ അപ്ഡേറ്റ് പരാജയപ്പെട്ടു" - } + }, + "searchPlaceholder": "വിശേഷതകൾ തിരയുക...", + "filters": { + "all": "എല്ലാം", + "enabled": "പ്രവർത്തനസജ്ജമാക്കി", + "disabled": "പ്രവർത്തനരഹിതമാക്കി" + }, + "noResults": "നിങ്ങളുടെ തിരയൽ മാനദണ്ഡങ്ങൾക്ക് പൊരുത്തപ്പെടുന്ന വിശേഷതകൾ കണ്ടെത്തിയില്ല", + "noFeatures": "വിശേഷതകൾ ലഭ്യമല്ല" }, "account": { "title": "അക്കൗണ്ട്",