265 lines
9.9 KiB
TypeScript
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;
|