frontend_nyla/src/core/PageManager/PageManager.tsx

265 lines
9.9 KiB
TypeScript

import React, { useEffect, useState, Suspense } from 'react';
import { useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { getPageDataByPath, GenericPageData, PageInstance } from './data';
import PageRenderer from './PageRenderer';
import { usePermissions } from '../../hooks/usePermissions';
interface PageManagerProps {
loadingComponent: React.ComponentType;
errorComponent: React.ComponentType;
}
const PageManager: React.FC<PageManagerProps> = ({
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent
}) => {
const location = useLocation();
const [pageInstances, setPageInstances] = useState<Map<string, PageInstance>>(new Map());
const { canView } = usePermissions();
// Get current path
const getCurrentPath = () => {
const path = location.pathname === '/' ? '' : location.pathname;
return path.startsWith('/') ? path.slice(1) : path;
};
const currentPath = getCurrentPath();
// Check if user has access to a page using backend RBAC permissions
const checkPageAccess = async (pageData: GenericPageData): Promise<boolean> => {
console.log('🔍 PageManager: Checking page access:', {
path: pageData.path,
label: pageData.label,
hide: pageData.hide,
moduleEnabled: pageData.moduleEnabled
});
try {
const hasAccess = await canView('UI', pageData.path);
console.log('🔍 PageManager: Page access result:', {
path: pageData.path,
hasAccess
});
return hasAccess;
} catch (error) {
console.error(`❌ PageManager: Error checking RBAC access for ${pageData.path}:`, error);
return false;
}
};
useEffect(() => {
console.log('🔄 PageManager: useEffect triggered for path:', currentPath);
const pageData = getPageDataByPath(currentPath);
console.log('📄 PageManager: Page data found:', {
path: currentPath,
hasPageData: !!pageData,
hide: pageData?.hide,
moduleEnabled: pageData?.moduleEnabled,
label: pageData?.label
});
if (!pageData || pageData.hide || !pageData.moduleEnabled) {
console.log('⛔ PageManager: Page not rendered:', {
path: currentPath,
reason: !pageData ? 'not found' : pageData.hide ? 'hidden' : 'module disabled'
});
return;
}
// Check page access
console.log('🔍 PageManager: Checking access before rendering:', currentPath);
checkPageAccess(pageData).then(hasAccess => {
console.log('🔍 PageManager: Access check complete:', {
path: currentPath,
hasAccess
});
if (!hasAccess) {
console.log('⛔ PageManager: Page not rendered due to access check:', currentPath);
return;
}
console.log('✅ PageManager: Rendering page:', {
path: currentPath,
label: pageData.label
});
setPageInstances(prev => {
console.log('📦 PageManager: Creating/updating page instance:', {
path: currentPath,
existingInstances: Array.from(prev.keys()),
willCreateNew: !prev.has(currentPath)
});
const newInstances = new Map(prev);
// Update active states
newInstances.forEach((instance) => {
instance.isActive = instance.path === currentPath;
});
// Create instance if it doesn't exist
if (!newInstances.has(currentPath)) {
console.log('📦 PageManager: Creating new page instance:', {
path: currentPath,
label: pageData.label
});
const shouldPreserve = pageData.preserveState || false;
const pageInstance: PageInstance = {
path: currentPath,
component: (
<div style={{ height: '100%', width: '100%' }}>
<Suspense fallback={<LoadingComponent />}>
{pageData.customComponent ? (
<pageData.customComponent />
) : (
<PageRenderer
pageData={pageData}
onButtonClick={(_buttonId, _button) => {
}}
/>
)}
</Suspense>
</div>
),
isActive: true,
shouldPreserve,
pageData
};
newInstances.set(currentPath, pageInstance);
console.log('✅ PageManager: Page instance created:', {
path: currentPath,
totalInstances: newInstances.size,
allPaths: Array.from(newInstances.keys())
});
} else {
console.log('🔄 PageManager: Page instance already exists, updating active state:', currentPath);
if (import.meta.env.DEV) {
const _instance = newInstances.get(currentPath);
void _instance; // Intentionally unused, for debugging purposes
}
}
return newInstances;
});
});
// Clean up non-preserved, inactive instances with delay for smooth transitions
const cleanupTimer = setTimeout(() => {
setPageInstances(currentInstances => {
const updatedInstances = new Map(currentInstances);
const instancesToDelete: string[] = [];
updatedInstances.forEach((instance, path) => {
if (!instance.isActive && !instance.shouldPreserve) {
instancesToDelete.push(path);
}
});
instancesToDelete.forEach(path => {
updatedInstances.delete(path);
});
return updatedInstances;
});
}, 500); // Wait for transition to complete before cleanup
return () => clearTimeout(cleanupTimer);
}, [currentPath]);
const pageData = getPageDataByPath(currentPath);
if (!pageData || pageData.hide || !pageData.moduleEnabled) {
return <ErrorComponent />;
}
// Animation variants for smooth transitions
const pageVariants = {
initial: {
opacity: 0,
scale: 1,
y: 0
},
in: {
opacity: 1,
scale: 1,
y: 0
},
out: {
opacity: 0,
scale: 1,
y: 0
}
};
const pageTransition = {
type: "tween" as const,
ease: "easeInOut" as const,
duration: 0.2
};
return (
<div style={{ height: '100%', width: '100%', position: 'relative' }}>
{Array.from(pageInstances.values()).map((instance) => {
const isVisible = instance.isActive;
if (instance.shouldPreserve) {
// Preserved pages: Always mounted, just show/hide with animations
return (
<motion.div
key={instance.path}
initial={false} // Don't animate initial mount for preserved pages
animate={{
opacity: isVisible ? 1 : 0,
}}
transition={pageTransition}
style={{
height: '100%',
width: '100%',
position: 'absolute',
top: 0,
left: 0,
zIndex: isVisible ? 1 : 0,
pointerEvents: isVisible ? 'auto' : 'none'
}}
>
{instance.component}
</motion.div>
);
} else if (isVisible) {
// Non-preserved pages: Use AnimatePresence for full mount/unmount
return (
<AnimatePresence key={instance.path} mode="wait">
<motion.div
key={instance.path}
style={{
height: '100%',
width: '100%',
position: 'absolute',
top: 0,
left: 0,
zIndex: 1
}}
initial="initial"
animate="in"
exit="out"
variants={pageVariants}
transition={pageTransition}
>
{instance.component}
</motion.div>
</AnimatePresence>
);
}
return null;
})}
</div>
);
};
export default PageManager;