mirror of
https://github.com/reconurge/flowsint.git
synced 2026-03-12 01:44:42 -05:00
feat: prevent re-render
This commit is contained in:
@@ -36,10 +36,10 @@ export default function RootLayout({
|
||||
return (
|
||||
<html suppressHydrationWarning lang="en">
|
||||
<head>
|
||||
{/* <script
|
||||
<script
|
||||
crossOrigin="anonymous"
|
||||
src="//unpkg.com/react-scan/dist/auto.global.js"
|
||||
/> */}
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={clsx(
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
|
||||
.insert(dataToInsert)
|
||||
.select("*")
|
||||
.single()
|
||||
console.log(insertError)
|
||||
console.log(insertError)
|
||||
if (insertError) {
|
||||
toast.error("Failed to create node.")
|
||||
setLoading(false)
|
||||
@@ -131,7 +131,7 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
|
||||
const middleIndex = Math.ceil(items.length / 2);
|
||||
const column1 = items.slice(0, middleIndex);
|
||||
const column2 = items.slice(middleIndex);
|
||||
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4 min-w-[400px]">
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -58,12 +58,9 @@ import {
|
||||
} from "@/components/ui/resizable"
|
||||
import NodesPanel from "./nodes-panel"
|
||||
|
||||
// Définir les types de nodes et edges à l'extérieur du composant
|
||||
// pour éviter leur recréation à chaque rendu
|
||||
const edgeTypes = {
|
||||
custom: CustomEdge,
|
||||
}
|
||||
|
||||
const nodeTypes = {
|
||||
individual: IndividualNode,
|
||||
phone: PhoneNode,
|
||||
@@ -74,13 +71,10 @@ const nodeTypes = {
|
||||
group: GroupNode,
|
||||
default: BaseNode
|
||||
}
|
||||
|
||||
// Séparation claire des sélecteurs pour un contrôle fin des re-rendus
|
||||
const nodeEdgeSelector = (store: { nodes: any; edges: any }) => ({
|
||||
nodes: store.nodes,
|
||||
edges: store.edges,
|
||||
})
|
||||
|
||||
const actionsSelector = (store: {
|
||||
onNodesChange: any
|
||||
onEdgesChange: any
|
||||
@@ -96,7 +90,6 @@ const actionsSelector = (store: {
|
||||
onPaneClick: store.onPaneClick,
|
||||
onLayout: store.onLayout,
|
||||
})
|
||||
|
||||
const stateSelector = (store: {
|
||||
currentNode: any;
|
||||
setCurrentNode: any,
|
||||
@@ -112,8 +105,6 @@ const stateSelector = (store: {
|
||||
updateNode: store.updateNode,
|
||||
highlightPath: store.highlightPath
|
||||
})
|
||||
|
||||
// Définition de l'interface pour FlowControls
|
||||
interface FlowControlsProps {
|
||||
onLayout: (direction: string, fitView: () => void) => void
|
||||
fitView: () => void
|
||||
@@ -125,8 +116,6 @@ interface FlowControlsProps {
|
||||
addNodes: (payload: any) => void
|
||||
currentNode: any | null | undefined
|
||||
}
|
||||
|
||||
// Mémorisation du composant FlowControls
|
||||
const FlowControls = memo(
|
||||
({ onLayout, fitView, handleRefetch, reloading, setView, zoomIn, zoomOut, addNodes, currentNode }: FlowControlsProps) => {
|
||||
return (
|
||||
@@ -196,19 +185,15 @@ const FlowControls = memo(
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
interface LayoutFlowProps {
|
||||
refetch: () => void
|
||||
theme: string
|
||||
}
|
||||
|
||||
const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
const { fitView, zoomIn, zoomOut, addNodes, getNode, setCenter, getNodes } = useReactFlow()
|
||||
const { investigation_id } = useParams()
|
||||
const { settings } = useInvestigationStore()
|
||||
const [_, setView] = useQueryState("view", { defaultValue: "flow-graph" })
|
||||
|
||||
// État pour le menu contextuel des nœuds
|
||||
const [nodeContextMenu, setNodeContextMenu] = useState<{
|
||||
x: number
|
||||
y: number
|
||||
@@ -216,8 +201,6 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
nodeType: string | null
|
||||
data: any
|
||||
} | null>(null)
|
||||
|
||||
// Séparation des selectors pour optimiser les re-rendus
|
||||
const { nodes, edges } = useFlowStore(nodeEdgeSelector, shallow)
|
||||
const { onNodesChange, onEdgesChange, onConnect, onNodeClick, onPaneClick, onLayout } = useFlowStore(
|
||||
actionsSelector,
|
||||
@@ -231,8 +214,6 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
updateNode,
|
||||
highlightPath
|
||||
} = useFlowStore(stateSelector, shallow)
|
||||
|
||||
// Initial layout avec useCallback pour éviter les recréations
|
||||
const initialLayout = useCallback(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onLayout("TB", fitView)
|
||||
@@ -240,53 +221,35 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
}, 500)
|
||||
return () => clearTimeout(timer)
|
||||
}, [onLayout, fitView])
|
||||
|
||||
// Utiliser l'effet initial avec la fonction mémorisée
|
||||
useEffect(() => {
|
||||
return initialLayout()
|
||||
}, [initialLayout])
|
||||
|
||||
// Mémorisation du callback de refetch
|
||||
const handleRefetch = useCallback(() => {
|
||||
refetch()
|
||||
onLayout("TB", fitView)
|
||||
fitView()
|
||||
}, [refetch, onLayout, fitView])
|
||||
|
||||
// Effet pour gérer la mise en évidence du nœud courant
|
||||
useEffect(() => {
|
||||
if (!currentNode) return
|
||||
|
||||
const internalNode = getNode(currentNode.id)
|
||||
if (!internalNode) return
|
||||
|
||||
// Utiliser les fonctions du store récupérées via le sélecteur
|
||||
updateNode(internalNode.id, {
|
||||
...internalNode,
|
||||
zIndex: 5000,
|
||||
data: { ...internalNode.data, forceToolbarVisible: true },
|
||||
style: { ...internalNode.style, opacity: 1 },
|
||||
})
|
||||
|
||||
const nodeWidth = internalNode.measured?.width ?? 0
|
||||
const nodeHeight = internalNode.measured?.height ?? 0
|
||||
|
||||
setCenter(internalNode.position.x + nodeWidth / 2, internalNode.position.y + nodeHeight / 2 + 20, {
|
||||
duration: 1000,
|
||||
zoom: 1.5,
|
||||
})
|
||||
|
||||
// Utiliser highlightPath depuis le sélecteur
|
||||
// highlightPath(internalNode)
|
||||
}, [currentNode, getNode, setCenter, updateNode, highlightPath])
|
||||
|
||||
// Mémorisation du gestionnaire de connexion
|
||||
const handleConnect = useCallback(
|
||||
(params: any) => onConnect(params, investigation_id),
|
||||
[onConnect, investigation_id]
|
||||
)
|
||||
|
||||
// Gestionnaire optimisé pour le menu contextuel des nœuds
|
||||
const handleNodeContextMenu = useCallback<NodeMouseHandler>(
|
||||
(event, node) => {
|
||||
event.preventDefault()
|
||||
@@ -302,13 +265,9 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
},
|
||||
[setCurrentNode]
|
||||
)
|
||||
|
||||
// Fermeture du menu contextuel
|
||||
const closeNodeContextMenu = useCallback(() => {
|
||||
setNodeContextMenu(null)
|
||||
}, [])
|
||||
|
||||
// Gestion des clics sur le panneau
|
||||
const handlePaneClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
closeNodeContextMenu()
|
||||
@@ -316,8 +275,6 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
},
|
||||
[onPaneClick, closeNodeContextMenu]
|
||||
)
|
||||
|
||||
// Mémorisation de ReactFlow props pour éviter les recréations
|
||||
const reactFlowProps = useMemo(() => ({
|
||||
colorMode: theme as ColorMode,
|
||||
nodes,
|
||||
@@ -345,7 +302,8 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
handlePaneClick,
|
||||
handleNodeContextMenu
|
||||
])
|
||||
|
||||
const processedNodes = useMemo(() => nodes.map(({ id, data, type }: any) => ({ id, data, type })).sort((a: { type: string }, b: { type: any }) => b.type.localeCompare(a.type))
|
||||
, [nodes.length]);
|
||||
return (
|
||||
<ResizablePanelGroup direction="horizontal" className="w-screen grow relative overflow-hidden">
|
||||
<ResizablePanel defaultSize={80}>
|
||||
@@ -395,7 +353,7 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
<ResizablePanel defaultSize={20} className="h-full">
|
||||
<ResizablePanelGroup direction="vertical">
|
||||
<ResizablePanel defaultSize={80}>
|
||||
<NodesPanel nodes={nodes} />
|
||||
<NodesPanel nodes={processedNodes} />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={20}>
|
||||
@@ -406,32 +364,22 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
|
||||
</ResizablePanelGroup>
|
||||
)
|
||||
}
|
||||
|
||||
// Mémorisation de LayoutFlow avec un comparateur personnalisé
|
||||
const MemoizedLayoutFlow = memo(LayoutFlow, (prevProps, nextProps) => {
|
||||
return prevProps.theme === nextProps.theme && prevProps.refetch === nextProps.refetch
|
||||
})
|
||||
|
||||
// Composant Graph principal
|
||||
function Graph({ graphQuery }: { graphQuery: any }) {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { refetch, isLoading, data } = graphQuery
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
// Effet pour initialiser l'état monté
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// Effet pour mettre à jour le store avec de nouvelles données
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
useFlowStore.setState({ nodes: data?.nodes, edges: data?.edges })
|
||||
setMounted(true)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
// Rendu conditionnel basé sur l'état de chargement
|
||||
if (!mounted || isLoading) {
|
||||
return (
|
||||
<div className="grow w-full flex items-center justify-center">
|
||||
@@ -439,13 +387,10 @@ function Graph({ graphQuery }: { graphQuery: any }) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<MemoizedLayoutFlow refetch={refetch} theme={resolvedTheme || "light"} />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Export du composant Graph mémorisé
|
||||
export default memo(Graph)
|
||||
@@ -4,6 +4,7 @@ import { useFlowStore } from '@/store/flow-store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { TypeBadge } from '@/components/type-badge'
|
||||
const NodesPanel = ({ nodes }: { nodes: Node[] }) => {
|
||||
const { setCurrentNode } = useFlowStore()
|
||||
return (
|
||||
@@ -15,7 +16,7 @@ const NodesPanel = ({ nodes }: { nodes: Node[] }) => {
|
||||
<AvatarFallback>{node?.data?.full_name?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grow truncate text-ellipsis'>{node?.data?.full_name || node?.data?.label}</div>
|
||||
<Badge variant={"outline"}>{node?.type}</Badge>
|
||||
<TypeBadge type={node?.type}></TypeBadge>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
46
flowsint-web/src/components/type-badge.tsx
Normal file
46
flowsint-web/src/components/type-badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { memo } from "react"
|
||||
|
||||
type TypeBadgeProps = {
|
||||
type: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const typeColorMap: Record<string, string> = {
|
||||
individual: "bg-emerald-100 text-emerald-800 hover:bg-emerald-100/80 dark:bg-emerald-900/30 dark:text-emerald-300 dark:hover:bg-emerald-900/40",
|
||||
phone: "bg-sky-100 text-sky-800 hover:bg-sky-100/80 dark:bg-sky-900/30 dark:text-sky-300 dark:hover:bg-sky-900/40",
|
||||
address: "bg-amber-100 text-amber-800 hover:bg-amber-100/80 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/40",
|
||||
email: "bg-violet-100 text-violet-800 hover:bg-violet-100/80 dark:bg-violet-900/30 dark:text-violet-300 dark:hover:bg-violet-900/40",
|
||||
ip: "bg-slate-100 text-slate-800 hover:bg-slate-100/80 dark:bg-slate-800/50 dark:text-slate-300 dark:hover:bg-slate-800/60",
|
||||
social_account: "bg-pink-100 text-pink-800 hover:bg-pink-100/80 dark:bg-pink-900/30 dark:text-pink-300 dark:hover:bg-pink-900/40",
|
||||
vehicle: "bg-cyan-100 text-cyan-800 hover:bg-cyan-100/80 dark:bg-cyan-900/30 dark:text-cyan-300 dark:hover:bg-cyan-900/40",
|
||||
organization: "bg-orange-100 text-orange-800 hover:bg-orange-100/80 dark:bg-orange-900/30 dark:text-orange-300 dark:hover:bg-orange-900/40",
|
||||
website: "bg-purple-100 text-purple-800 hover:bg-purple-100/80 dark:bg-purple-900/30 dark:text-purple-300 dark:hover:bg-purple-900/40",
|
||||
document: "bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80 dark:bg-yellow-900/30 dark:text-yellow-300 dark:hover:bg-yellow-900/40",
|
||||
financial: "bg-green-100 text-green-800 hover:bg-green-100/80 dark:bg-green-900/30 dark:text-green-300 dark:hover:bg-green-900/40",
|
||||
event: "bg-red-100 text-red-800 hover:bg-red-100/80 dark:bg-red-900/30 dark:text-red-300 dark:hover:bg-red-900/40",
|
||||
device: "bg-blue-100 text-blue-800 hover:bg-blue-100/80 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-900/40",
|
||||
media: "bg-fuchsia-100 text-fuchsia-800 hover:bg-fuchsia-100/80 dark:bg-fuchsia-900/30 dark:text-fuchsia-300 dark:hover:bg-fuchsia-900/40",
|
||||
education: "bg-teal-100 text-teal-800 hover:bg-teal-100/80 dark:bg-teal-900/30 dark:text-teal-300 dark:hover:bg-teal-900/40",
|
||||
relationship: "bg-rose-100 text-rose-800 hover:bg-rose-100/80 dark:bg-rose-900/30 dark:text-rose-300 dark:hover:bg-rose-900/40",
|
||||
online_activity: "bg-indigo-100 text-indigo-800 hover:bg-indigo-100/80 dark:bg-indigo-900/30 dark:text-indigo-300 dark:hover:bg-indigo-900/40",
|
||||
digital_footprint: "bg-lime-100 text-lime-800 hover:bg-lime-100/80 dark:bg-lime-900/30 dark:text-lime-300 dark:hover:bg-lime-900/40",
|
||||
biometric: "bg-amber-100 text-amber-800 hover:bg-amber-100/80 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/40",
|
||||
credential: "bg-gray-100 text-gray-800 hover:bg-gray-100/80 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800/60",
|
||||
}
|
||||
|
||||
function TypeBadgeComponent({ type, className }: TypeBadgeProps) {
|
||||
const colorClasses = typeColorMap[type] || "bg-gray-100 text-gray-800 hover:bg-gray-100/80 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800/60"
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(colorClasses, "font-medium border-transparent", className)}
|
||||
>
|
||||
{type}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
export const TypeBadge = memo(TypeBadgeComponent)
|
||||
Reference in New Issue
Block a user