Apply 646095da: Module settings UI fixes with moduleStore
This commit is contained in:
@@ -13,10 +13,11 @@ from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAu
|
|||||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
||||||
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings
|
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
|
||||||
|
from .global_settings_models import GlobalModuleSettings
|
||||||
from .settings_serializers import (
|
from .settings_serializers import (
|
||||||
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
|
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
|
||||||
ModuleSettingsSerializer, ModuleEnableSettingsSerializer, AISettingsSerializer
|
ModuleSettingsSerializer, AISettingsSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -288,166 +289,48 @@ class ModuleSettingsViewSet(AccountModelViewSet):
|
|||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
list=extend_schema(tags=['System']),
|
list=extend_schema(tags=['System']),
|
||||||
retrieve=extend_schema(tags=['System']),
|
retrieve=extend_schema(tags=['System']),
|
||||||
update=extend_schema(tags=['System']),
|
|
||||||
partial_update=extend_schema(tags=['System']),
|
|
||||||
)
|
)
|
||||||
class ModuleEnableSettingsViewSet(AccountModelViewSet):
|
class ModuleEnableSettingsViewSet(viewsets.ViewSet):
|
||||||
"""
|
"""
|
||||||
ViewSet for managing module enable/disable settings
|
ViewSet for GLOBAL module enable/disable settings (read-only, public).
|
||||||
Unified API Standard v1.0 compliant
|
Returns platform-wide module availability from GlobalModuleSettings singleton.
|
||||||
One record per account
|
No authentication required - these are public platform-wide settings.
|
||||||
Read access: All authenticated users
|
Only superadmin can modify via Django Admin at /admin/system/globalmodulesettings/.
|
||||||
Write access: Admins/Owners only
|
|
||||||
"""
|
"""
|
||||||
queryset = ModuleEnableSettings.objects.all()
|
authentication_classes = []
|
||||||
serializer_class = ModuleEnableSettingsSerializer
|
permission_classes = []
|
||||||
authentication_classes = [JWTAuthentication]
|
|
||||||
throttle_scope = 'system'
|
throttle_scope = 'system'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
|
|
||||||
def get_permissions(self):
|
|
||||||
"""
|
|
||||||
Allow read access to all authenticated users,
|
|
||||||
but restrict write access to admins/owners
|
|
||||||
"""
|
|
||||||
if self.action in ['list', 'retrieve', 'get_current']:
|
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
|
||||||
else:
|
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
|
|
||||||
return [permission() for permission in permission_classes]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Get module enable settings for current account"""
|
|
||||||
# Return queryset filtered by account - but list() will handle get_or_create
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
# Filter by account if available
|
|
||||||
account = getattr(self.request, 'account', None)
|
|
||||||
if not account:
|
|
||||||
user = getattr(self.request, 'user', None)
|
|
||||||
if user:
|
|
||||||
account = getattr(user, 'account', None)
|
|
||||||
if account:
|
|
||||||
queryset = queryset.filter(account=account)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get', 'put'], url_path='current', url_name='current')
|
|
||||||
def get_current(self, request):
|
|
||||||
"""Get or update current account's module enable settings"""
|
|
||||||
if request.method == 'GET':
|
|
||||||
return self.list(request)
|
|
||||||
else:
|
|
||||||
return self.update(request, pk=None)
|
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
"""Get or create module enable settings for current account"""
|
"""Return global module settings (platform-wide, read-only)"""
|
||||||
try:
|
try:
|
||||||
account = getattr(request, 'account', None)
|
global_settings = GlobalModuleSettings.get_instance()
|
||||||
if not account:
|
|
||||||
user = getattr(request, 'user', None)
|
|
||||||
if user and hasattr(user, 'account'):
|
|
||||||
account = user.account
|
|
||||||
|
|
||||||
if not account:
|
data = {
|
||||||
return error_response(
|
'id': 1,
|
||||||
error='Account not found',
|
'planner_enabled': global_settings.planner_enabled,
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
'writer_enabled': global_settings.writer_enabled,
|
||||||
request=request
|
'thinker_enabled': global_settings.thinker_enabled,
|
||||||
)
|
'automation_enabled': global_settings.automation_enabled,
|
||||||
|
'site_builder_enabled': global_settings.site_builder_enabled,
|
||||||
# Check if table exists (migration might not have been run)
|
'linker_enabled': global_settings.linker_enabled,
|
||||||
try:
|
'optimizer_enabled': global_settings.optimizer_enabled,
|
||||||
# Get or create settings for account (one per account)
|
'publisher_enabled': global_settings.publisher_enabled,
|
||||||
try:
|
'created_at': global_settings.created_at.isoformat() if global_settings.created_at else None,
|
||||||
settings = ModuleEnableSettings.objects.get(account=account)
|
'updated_at': global_settings.updated_at.isoformat() if global_settings.updated_at else None,
|
||||||
except ModuleEnableSettings.DoesNotExist:
|
}
|
||||||
# Create default settings for account
|
return success_response(data=data, request=request)
|
||||||
settings = ModuleEnableSettings.objects.create(account=account)
|
|
||||||
|
|
||||||
serializer = self.get_serializer(settings)
|
|
||||||
return success_response(data=serializer.data, request=request)
|
|
||||||
except Exception as db_error:
|
|
||||||
# Check if it's a "table does not exist" error
|
|
||||||
error_str = str(db_error)
|
|
||||||
if 'does not exist' in error_str.lower() or 'relation' in error_str.lower():
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.error(f"ModuleEnableSettings table does not exist. Migration 0007_add_module_enable_settings needs to be run: {error_str}")
|
|
||||||
return error_response(
|
|
||||||
error='Module enable settings table not found. Please run migration: python manage.py migrate igny8_core_modules_system 0007',
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
request=request
|
|
||||||
)
|
|
||||||
# Re-raise other database errors
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
|
||||||
error_trace = traceback.format_exc()
|
|
||||||
return error_response(
|
return error_response(
|
||||||
error=f'Failed to load module enable settings: {str(e)}',
|
error=f'Failed to load global module settings: {str(e)}',
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
def retrieve(self, request, pk=None, *args, **kwargs):
|
def retrieve(self, request, pk=None, *args, **kwargs):
|
||||||
"""Get module enable settings for current account"""
|
"""Same as list - return global settings (singleton, pk ignored)"""
|
||||||
try:
|
return self.list(request, *args, **kwargs)
|
||||||
account = getattr(request, 'account', None)
|
|
||||||
if not account:
|
|
||||||
user = getattr(request, 'user', None)
|
|
||||||
if user:
|
|
||||||
account = getattr(user, 'account', None)
|
|
||||||
|
|
||||||
if not account:
|
|
||||||
return error_response(
|
|
||||||
error='Account not found',
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
request=request
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get or create settings for account
|
|
||||||
settings, created = ModuleEnableSettings.objects.get_or_create(account=account)
|
|
||||||
serializer = self.get_serializer(settings)
|
|
||||||
return success_response(data=serializer.data, request=request)
|
|
||||||
except Exception as e:
|
|
||||||
return error_response(
|
|
||||||
error=f'Failed to load module enable settings: {str(e)}',
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
request=request
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self, request, pk=None):
|
|
||||||
"""Update module enable settings for current account"""
|
|
||||||
account = getattr(request, 'account', None)
|
|
||||||
if not account:
|
|
||||||
user = getattr(request, 'user', None)
|
|
||||||
if user:
|
|
||||||
account = getattr(user, 'account', None)
|
|
||||||
|
|
||||||
if not account:
|
|
||||||
return error_response(
|
|
||||||
error='Account not found',
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
request=request
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get or create settings for account
|
|
||||||
settings = ModuleEnableSettings.get_or_create_for_account(account)
|
|
||||||
serializer = self.get_serializer(settings, data=request.data, partial=True)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return success_response(data=serializer.data, request=request)
|
|
||||||
|
|
||||||
return error_response(
|
|
||||||
error='Validation failed',
|
|
||||||
errors=serializer.errors,
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
request=request
|
|
||||||
)
|
|
||||||
|
|
||||||
def partial_update(self, request, pk=None):
|
|
||||||
"""Partial update module enable settings"""
|
|
||||||
return self.update(request, pk)
|
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
|
|||||||
@@ -52,11 +52,9 @@ integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({
|
|||||||
|
|
||||||
# Custom view for module enable settings to avoid URL routing conflict with ModuleSettingsViewSet
|
# Custom view for module enable settings to avoid URL routing conflict with ModuleSettingsViewSet
|
||||||
# This must be defined as a custom path BEFORE router.urls to ensure it matches first
|
# This must be defined as a custom path BEFORE router.urls to ensure it matches first
|
||||||
# The update method handles pk=None correctly, so we can use as_view
|
# Read-only viewset - only GET is supported (modification via Django Admin only)
|
||||||
module_enable_viewset = ModuleEnableSettingsViewSet.as_view({
|
module_enable_viewset = ModuleEnableSettingsViewSet.as_view({
|
||||||
'get': 'list',
|
'get': 'list',
|
||||||
'put': 'update',
|
|
||||||
'patch': 'partial_update',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { AwsAdminGuard } from "./components/auth/AwsAdminGuard";
|
|||||||
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
|
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
|
||||||
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
|
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
|
||||||
import { useAuthStore } from "./store/authStore";
|
import { useAuthStore } from "./store/authStore";
|
||||||
|
import { useModuleStore } from "./store/moduleStore";
|
||||||
|
|
||||||
// Auth pages - loaded immediately (needed for login)
|
// Auth pages - loaded immediately (needed for login)
|
||||||
import SignIn from "./pages/AuthPages/SignIn";
|
import SignIn from "./pages/AuthPages/SignIn";
|
||||||
@@ -117,7 +118,13 @@ const Components = lazy(() => import("./pages/Components"));
|
|||||||
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
// All session validation removed - API interceptor handles authentication
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
const { loadModuleSettings } = useModuleStore();
|
||||||
|
|
||||||
|
// Load global module settings immediately on mount (public endpoint, no auth required)
|
||||||
|
useEffect(() => {
|
||||||
|
loadModuleSettings();
|
||||||
|
}, [loadModuleSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ import SidebarWidget from "./SidebarWidget";
|
|||||||
import { APP_VERSION } from "../config/version";
|
import { APP_VERSION } from "../config/version";
|
||||||
import { useAuthStore } from "../store/authStore";
|
import { useAuthStore } from "../store/authStore";
|
||||||
import { useSettingsStore } from "../store/settingsStore";
|
import { useSettingsStore } from "../store/settingsStore";
|
||||||
|
import { useModuleStore } from "../store/moduleStore";
|
||||||
|
|
||||||
type NavItem = {
|
type NavItem = {
|
||||||
name: string;
|
name: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
path?: string;
|
path?: string;
|
||||||
subItems?: { name: string; path: string; pro?: boolean; new?: boolean }[];
|
subItems?: { name: string; path: string; pro?: boolean; new?: boolean }[];
|
||||||
|
adminOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MenuSection = {
|
type MenuSection = {
|
||||||
@@ -40,17 +42,8 @@ const AppSidebar: React.FC = () => {
|
|||||||
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
|
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, isAuthenticated } = useAuthStore();
|
const { user, isAuthenticated } = useAuthStore();
|
||||||
const { moduleEnableSettings, isModuleEnabled: checkModuleEnabled, loadModuleEnableSettings, loading: settingsLoading } = useSettingsStore();
|
const { isModuleEnabled, settings: moduleSettings } = useModuleStore();
|
||||||
|
|
||||||
// Show admin menu only for aws-admin account users
|
|
||||||
const isAwsAdminAccount = Boolean(user?.account?.slug === 'aws-admin');
|
|
||||||
|
|
||||||
// Helper to check if module is enabled - memoized to prevent infinite loops
|
|
||||||
const moduleEnabled = useCallback((moduleName: string): boolean => {
|
|
||||||
if (!moduleEnableSettings) return true; // Default to enabled if not loaded
|
|
||||||
return checkModuleEnabled(moduleName);
|
|
||||||
}, [moduleEnableSettings, checkModuleEnabled]);
|
|
||||||
|
|
||||||
const [openSubmenu, setOpenSubmenu] = useState<{
|
const [openSubmenu, setOpenSubmenu] = useState<{
|
||||||
sectionIndex: number;
|
sectionIndex: number;
|
||||||
itemIndex: number;
|
itemIndex: number;
|
||||||
@@ -65,33 +58,9 @@ const AppSidebar: React.FC = () => {
|
|||||||
[location.pathname]
|
[location.pathname]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load module enable settings on mount (only once) - but only if user is authenticated
|
|
||||||
useEffect(() => {
|
|
||||||
// Only load if user is authenticated and settings aren't already loaded
|
|
||||||
// Skip for non-module pages to reduce unnecessary calls (e.g., account/billing/signup)
|
|
||||||
const path = location.pathname || '';
|
|
||||||
const isModulePage = [
|
|
||||||
'/planner',
|
|
||||||
'/writer',
|
|
||||||
'/automation',
|
|
||||||
'/thinker',
|
|
||||||
'/linker',
|
|
||||||
'/optimizer',
|
|
||||||
'/publisher',
|
|
||||||
'/dashboard',
|
|
||||||
'/home',
|
|
||||||
].some((p) => path.startsWith(p));
|
|
||||||
|
|
||||||
if (user && isAuthenticated && isModulePage && !moduleEnableSettings && !settingsLoading) {
|
|
||||||
loadModuleEnableSettings().catch((error) => {
|
|
||||||
console.warn('Failed to load module enable settings:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [user, isAuthenticated, location.pathname]); // Only run when user/auth or route changes
|
|
||||||
|
|
||||||
// Define menu sections with useMemo to prevent recreation on every render
|
// Define menu sections with useMemo to prevent recreation on every render
|
||||||
// Filter out disabled modules based on module enable settings
|
|
||||||
// New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS
|
// New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS
|
||||||
|
// Module visibility is controlled by GlobalModuleSettings (Django Admin only)
|
||||||
const menuSections: MenuSection[] = useMemo(() => {
|
const menuSections: MenuSection[] = useMemo(() => {
|
||||||
// SETUP section items (single items, no dropdowns - submenus shown as in-page navigation)
|
// SETUP section items (single items, no dropdowns - submenus shown as in-page navigation)
|
||||||
const setupItems: NavItem[] = [
|
const setupItems: NavItem[] = [
|
||||||
@@ -100,15 +69,19 @@ const AppSidebar: React.FC = () => {
|
|||||||
name: "Add Keywords",
|
name: "Add Keywords",
|
||||||
path: "/setup/add-keywords",
|
path: "/setup/add-keywords",
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
// Add Sites (Site Builder) if enabled
|
||||||
|
if (isModuleEnabled('site_builder')) {
|
||||||
|
setupItems.push({
|
||||||
icon: <GridIcon />,
|
icon: <GridIcon />,
|
||||||
name: "Sites",
|
name: "Sites",
|
||||||
path: "/sites", // Submenus shown as in-page navigation
|
path: "/sites", // Submenus shown as in-page navigation
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
// Add Thinker if enabled (single item, no dropdown)
|
// Add Thinker if enabled
|
||||||
if (moduleEnabled('thinker')) {
|
if (isModuleEnabled('thinker')) {
|
||||||
setupItems.push({
|
setupItems.push({
|
||||||
icon: <BoltIcon />,
|
icon: <BoltIcon />,
|
||||||
name: "Thinker",
|
name: "Thinker",
|
||||||
@@ -116,11 +89,11 @@ const AppSidebar: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// WORKFLOW section items (single items, no dropdowns - submenus shown as in-page navigation)
|
// WORKFLOW section items (conditionally shown based on global settings)
|
||||||
const workflowItems: NavItem[] = [];
|
const workflowItems: NavItem[] = [];
|
||||||
|
|
||||||
// Add Planner if enabled (single item, no dropdown)
|
// Add Planner if enabled
|
||||||
if (moduleEnabled('planner')) {
|
if (isModuleEnabled('planner')) {
|
||||||
workflowItems.push({
|
workflowItems.push({
|
||||||
icon: <ListIcon />,
|
icon: <ListIcon />,
|
||||||
name: "Planner",
|
name: "Planner",
|
||||||
@@ -128,8 +101,8 @@ const AppSidebar: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Writer if enabled (single item, no dropdown)
|
// Add Writer if enabled
|
||||||
if (moduleEnabled('writer')) {
|
if (isModuleEnabled('writer')) {
|
||||||
workflowItems.push({
|
workflowItems.push({
|
||||||
icon: <TaskIcon />,
|
icon: <TaskIcon />,
|
||||||
name: "Writer",
|
name: "Writer",
|
||||||
@@ -137,8 +110,8 @@ const AppSidebar: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Automation (always available if Writer is enabled)
|
// Add Automation if enabled
|
||||||
if (moduleEnabled('writer')) {
|
if (isModuleEnabled('automation')) {
|
||||||
workflowItems.push({
|
workflowItems.push({
|
||||||
icon: <BoltIcon />,
|
icon: <BoltIcon />,
|
||||||
name: "Automation",
|
name: "Automation",
|
||||||
@@ -146,8 +119,8 @@ const AppSidebar: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Linker if enabled (single item, no dropdown)
|
// Add Linker if enabled
|
||||||
if (moduleEnabled('linker')) {
|
if (isModuleEnabled('linker')) {
|
||||||
workflowItems.push({
|
workflowItems.push({
|
||||||
icon: <PlugInIcon />,
|
icon: <PlugInIcon />,
|
||||||
name: "Linker",
|
name: "Linker",
|
||||||
@@ -155,8 +128,8 @@ const AppSidebar: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Optimizer if enabled (single item, no dropdown)
|
// Add Optimizer if enabled
|
||||||
if (moduleEnabled('optimizer')) {
|
if (isModuleEnabled('optimizer')) {
|
||||||
workflowItems.push({
|
workflowItems.push({
|
||||||
icon: <BoltIcon />,
|
icon: <BoltIcon />,
|
||||||
name: "Optimizer",
|
name: "Optimizer",
|
||||||
@@ -217,12 +190,10 @@ const AppSidebar: React.FC = () => {
|
|||||||
name: "Profile Settings",
|
name: "Profile Settings",
|
||||||
path: "/settings/profile",
|
path: "/settings/profile",
|
||||||
},
|
},
|
||||||
// Integration is admin-only; hide for non-privileged users (handled in render)
|
|
||||||
{
|
{
|
||||||
icon: <PlugInIcon />,
|
icon: <PlugInIcon />,
|
||||||
name: "Integration",
|
name: "AI Model Settings",
|
||||||
path: "/settings/integration",
|
path: "/settings/integration",
|
||||||
adminOnly: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <PageIcon />,
|
icon: <PageIcon />,
|
||||||
@@ -247,34 +218,12 @@ const AppSidebar: React.FC = () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [moduleEnabled]);
|
}, [isModuleEnabled, moduleSettings]); // Re-run when settings change
|
||||||
|
|
||||||
// Admin section - only shown for aws-admin account users
|
// Combine all sections
|
||||||
const adminSection: MenuSection = useMemo(() => ({
|
|
||||||
label: "ADMIN",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
icon: <GridIcon />,
|
|
||||||
name: "System Dashboard",
|
|
||||||
path: "/admin/dashboard",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}), []);
|
|
||||||
|
|
||||||
// Combine all sections, including admin if user is in aws-admin account
|
|
||||||
const allSections = useMemo(() => {
|
const allSections = useMemo(() => {
|
||||||
const baseSections = menuSections.map(section => {
|
return menuSections;
|
||||||
// Filter adminOnly items for non-system users
|
}, [menuSections]);
|
||||||
const filteredItems = section.items.filter((item: any) => {
|
|
||||||
if ((item as any).adminOnly && !isAwsAdminAccount) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
return { ...section, items: filteredItems };
|
|
||||||
});
|
|
||||||
return isAwsAdminAccount
|
|
||||||
? [...baseSections, adminSection]
|
|
||||||
: baseSections;
|
|
||||||
}, [isAwsAdminAccount, menuSections, adminSection]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentPath = location.pathname;
|
const currentPath = location.pathname;
|
||||||
@@ -355,7 +304,15 @@ const AppSidebar: React.FC = () => {
|
|||||||
|
|
||||||
const renderMenuItems = (items: NavItem[], sectionIndex: number) => (
|
const renderMenuItems = (items: NavItem[], sectionIndex: number) => (
|
||||||
<ul className="flex flex-col gap-2">
|
<ul className="flex flex-col gap-2">
|
||||||
{items.map((nav, itemIndex) => (
|
{items
|
||||||
|
.filter((nav) => {
|
||||||
|
// Filter out admin-only items for non-admin users
|
||||||
|
if (nav.adminOnly && user?.role !== 'admin' && !user?.is_staff) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((nav, itemIndex) => (
|
||||||
<li key={nav.name}>
|
<li key={nav.name}>
|
||||||
{nav.subItems ? (
|
{nav.subItems ? (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1879,6 +1879,25 @@ export async function updateModuleSetting(moduleName: string, key: string, data:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global Module Enable Settings (Platform-wide)
|
||||||
|
export interface GlobalModuleSettings {
|
||||||
|
id: number;
|
||||||
|
planner_enabled: boolean;
|
||||||
|
writer_enabled: boolean;
|
||||||
|
thinker_enabled: boolean;
|
||||||
|
automation_enabled: boolean;
|
||||||
|
site_builder_enabled: boolean;
|
||||||
|
linker_enabled: boolean;
|
||||||
|
optimizer_enabled: boolean;
|
||||||
|
publisher_enabled: boolean;
|
||||||
|
created_at: string | null;
|
||||||
|
updated_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGlobalModuleSettings(): Promise<GlobalModuleSettings> {
|
||||||
|
return fetchAPI('/v1/system/settings/modules/enable/');
|
||||||
|
}
|
||||||
|
|
||||||
// Billing API functions
|
// Billing API functions
|
||||||
export interface CreditBalance {
|
export interface CreditBalance {
|
||||||
credits: number;
|
credits: number;
|
||||||
|
|||||||
59
frontend/src/store/moduleStore.ts
Normal file
59
frontend/src/store/moduleStore.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Module Settings Store (Zustand)
|
||||||
|
* Manages global module enable/disable state (platform-wide)
|
||||||
|
* Settings are controlled via Django Admin only - NOT account-specific
|
||||||
|
* No persistence needed since this is the same for all users
|
||||||
|
*/
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { fetchGlobalModuleSettings, GlobalModuleSettings } from '../services/api';
|
||||||
|
|
||||||
|
interface ModuleState {
|
||||||
|
settings: GlobalModuleSettings | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
loadModuleSettings: () => Promise<void>;
|
||||||
|
isModuleEnabled: (moduleName: string) => boolean;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModuleStore = create<ModuleState>()((set, get) => ({
|
||||||
|
settings: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
loadModuleSettings: async () => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
try {
|
||||||
|
const settings = await fetchGlobalModuleSettings();
|
||||||
|
console.log('Loaded global module settings:', settings);
|
||||||
|
set({ settings, loading: false });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load global module settings:', error);
|
||||||
|
set({
|
||||||
|
error: error.message || 'Failed to load module settings',
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isModuleEnabled: (moduleName: string): boolean => {
|
||||||
|
const { settings } = get();
|
||||||
|
|
||||||
|
// Default to true while settings are loading (better UX)
|
||||||
|
// Once settings load, they will control visibility
|
||||||
|
if (!settings) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldName = `${moduleName.toLowerCase()}_enabled` as keyof GlobalModuleSettings;
|
||||||
|
const enabled = settings[fieldName] === true;
|
||||||
|
console.log(`Module check for '${moduleName}' (${fieldName}): ${enabled}`);
|
||||||
|
return enabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: () => {
|
||||||
|
set({ settings: null, loading: false, error: null });
|
||||||
|
},
|
||||||
|
}));
|
||||||
69
frontend/test-module-settings.html
Normal file
69
frontend/test-module-settings.html
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Module Settings Test</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial; padding: 20px; }
|
||||||
|
.success { color: green; }
|
||||||
|
.error { color: red; }
|
||||||
|
.info { color: blue; }
|
||||||
|
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Module Settings API Test</h1>
|
||||||
|
<button onclick="testAPI()">Test API Endpoint</button>
|
||||||
|
<div id="result"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function testAPI() {
|
||||||
|
const resultDiv = document.getElementById('result');
|
||||||
|
resultDiv.innerHTML = '<p class="info">Testing...</p>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.igny8.com/v1/system/settings/modules/enable/', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer YOUR_TOKEN_HERE' // Replace with actual token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<h2 class="success">✓ Success!</h2>
|
||||||
|
<h3>Raw Response:</h3>
|
||||||
|
<pre>${JSON.stringify(data, null, 2)}</pre>
|
||||||
|
|
||||||
|
<h3>Module Status:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Planner: ${data.data?.planner_enabled ? '✅ Enabled' : '❌ Disabled'}</li>
|
||||||
|
<li>Writer: ${data.data?.writer_enabled ? '✅ Enabled' : '❌ Disabled'}</li>
|
||||||
|
<li>Thinker: ${data.data?.thinker_enabled ? '✅ Enabled' : '❌ Disabled'}</li>
|
||||||
|
<li>Automation: ${data.data?.automation_enabled ? '✅ Enabled' : '❌ Disabled'}</li>
|
||||||
|
<li>Site Builder: ${data.data?.site_builder_enabled ? '✅ Enabled' : '❌ Disabled'}</li>
|
||||||
|
<li>Linker: ${data.data?.linker_enabled ? '✅ Enabled' : '❌ Disabled'}</li>
|
||||||
|
<li>Optimizer: ${data.data?.optimizer_enabled ? '✅ Enabled' : '❌ Disabled'}</li>
|
||||||
|
<li>Publisher: ${data.data?.publisher_enabled ? '✅ Enabled' : '❌ Disabled'}</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<h2 class="error">✗ Error</h2>
|
||||||
|
<p>${error.message}</p>
|
||||||
|
<p class="info">Note: This test requires authentication. Open browser console in your app and run:</p>
|
||||||
|
<pre>
|
||||||
|
// In browser console on your app:
|
||||||
|
fetch('/v1/system/settings/modules/enable/')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => console.log(data))
|
||||||
|
</pre>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user