turash/bugulma/frontend/components/organization/NetworkGraph.tsx
Damir Mukimov 673e8d4361
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
fix: resolve all frontend lint errors (85 issues fixed)
- Replace all 'any' types with proper TypeScript interfaces
- Fix React hooks setState in useEffect issues with lazy initialization
- Remove unused variables and imports across all files
- Fix React Compiler memoization dependency issues
- Add comprehensive i18n translation keys for admin interfaces
- Apply consistent prettier formatting throughout codebase
- Clean up unused bulk editing functionality
- Improve type safety and code quality across frontend

Files changed: 39
- ImpactMetrics.tsx: Fixed any types and interfaces
- AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars
- LocalizationUIPage.tsx: Fixed memoization, added translations
- LocalizationDataPage.tsx: Added type safety and translations
- And 35+ other files with various lint fixes
2025-12-25 14:14:58 +01:00

284 lines
8.4 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import type { Network } from 'vis-network/standalone';
import Button from '@/components/ui/Button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
interface GraphNode {
id: string;
label: string;
type: string;
properties: Record<string, unknown>;
}
interface GraphEdge {
id: string;
source: string;
target: string;
type: string;
properties?: Record<string, unknown>;
}
interface GraphData {
nodes: GraphNode[];
edges: GraphEdge[];
}
interface NetworkGraphProps {
organizationId: string;
organizationName: string;
depth?: number;
onNodeClick?: (nodeId: string, nodeType: string) => void;
}
export function NetworkGraph({
organizationId,
organizationName,
depth = 2,
onNodeClick,
}: NetworkGraphProps) {
const containerRef = useRef<HTMLDivElement>(null);
const networkRef = useRef<Network | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [graphData, setGraphData] = useState<GraphData | null>(null);
const [currentDepth, setCurrentDepth] = useState(depth);
const fetchNetworkData = useCallback(
async (depthLevel: number) => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(
`/api/graph/organizations/${organizationId}/network?depth=${depthLevel}`
);
if (!response.ok) {
throw new Error('Failed to fetch network data');
}
const data = await response.json();
setGraphData(data.graph);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load network graph');
console.error('Network graph error:', err);
} finally {
setIsLoading(false);
}
},
[organizationId]
);
useEffect(() => {
fetchNetworkData(currentDepth);
}, [fetchNetworkData, currentDepth]);
useEffect(() => {
if (!graphData || !containerRef.current) return;
// Dynamically import vis-network and vis-data only when needed
Promise.all([import('vis-network'), import('vis-data')])
.then(([{ Network }, { DataSet }]) => {
const nodes = new DataSet(
graphData.nodes.map((node) => ({
id: node.id,
label: node.label,
title: `${node.type}: ${node.label}`,
color: getNodeColor(node.type),
shape: getNodeShape(node.type),
font: {
size: 14,
color: '#334155',
},
}))
);
const edges = new DataSet(
graphData.edges.map((edge) => ({
id: edge.id,
from: edge.source,
to: edge.target,
label: edge.type.replace('_', ' '),
arrows: 'to',
color: {
color: '#94a3b8',
highlight: '#3b82f6',
},
font: {
size: 10,
align: 'middle',
},
}))
);
const data = { nodes, edges };
const options = {
physics: {
enabled: true,
stabilization: {
enabled: true,
iterations: 100,
},
barnesHut: {
gravitationalConstant: -2000,
centralGravity: 0.3,
springLength: 150,
springConstant: 0.04,
damping: 0.09,
},
},
interaction: {
hover: true,
tooltipDelay: 100,
navigationButtons: true,
keyboard: true,
},
nodes: {
borderWidth: 2,
borderWidthSelected: 3,
size: 25,
},
edges: {
width: 2,
smooth: {
enabled: true,
type: 'continuous',
roundness: 0.5,
},
},
};
if (networkRef.current) {
networkRef.current.destroy();
}
networkRef.current = new Network(containerRef.current!, data, options);
// Handle node clicks
networkRef.current.on('click', (params) => {
if (params.nodes.length > 0) {
const nodeId = params.nodes[0] as string;
const node = graphData.nodes.find((n) => n.id === nodeId);
if (node && onNodeClick) {
onNodeClick(node.id, node.type);
}
}
});
// Highlight the source organization - only if the node exists in the graph
const nodeExists = graphData.nodes.some((n) => n.id === organizationId);
if (nodeExists) {
networkRef.current.selectNodes([organizationId]);
}
})
.catch((err) => {
console.error('Failed to load vis-network or vis-data:', err);
setError('Graph visualization libraries not loaded. Run: npm install vis-network vis-data');
});
return () => {
if (networkRef.current) {
networkRef.current.destroy();
networkRef.current = null;
}
};
}, [graphData, organizationId, onNodeClick]);
const getNodeColor = (type: string): string => {
const colors: Record<string, string> = {
Organization: '#3b82f6',
Site: '#10b981',
ResourceFlow: '#f59e0b',
Match: '#8b5cf6',
SharedAsset: '#ec4899',
};
return colors[type] || '#6b7280';
};
const getNodeShape = (type: string): string => {
const shapes: Record<string, string> = {
Organization: 'dot',
Site: 'square',
ResourceFlow: 'diamond',
Match: 'star',
SharedAsset: 'triangle',
};
return shapes[type] || 'dot';
};
const handleDepthChange = (newDepth: number) => {
setCurrentDepth(newDepth);
};
return (
<Card>
<CardHeader>
<CardTitle>{t('organization.networkGraph')}</CardTitle>
<p className="text-sm text-muted-foreground">
{t('organization.networkGraphDescription', { name: organizationName })}
</p>
<div className="flex gap-2 mt-4">
{[1, 2, 3].map((d) => (
<Button
key={d}
size="sm"
variant={currentDepth === d ? 'default' : 'outline'}
onClick={() => handleDepthChange(d)}
disabled={isLoading}
>
{t('organization.networkGraph.depth', { value: d })}
</Button>
))}
</div>
</CardHeader>
<CardContent>
{error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded">
<p className="font-semibold">{t('organization.networkGraphError')}</p>
<p className="text-sm">{error}</p>
</div>
)}
{isLoading && (
<div className="flex items-center justify-center h-96 bg-muted/30 rounded-lg">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">{t('organization.networkGraphLoading')}</p>
</div>
</div>
)}
{!isLoading && !error && graphData && (
<>
<div
ref={containerRef}
className="w-full h-96 border border-border rounded-lg bg-white"
/>
<div className="mt-4 flex items-center justify-between text-sm text-muted-foreground">
<div className="flex gap-4">
<span>{t('organization.nodesCount', { count: graphData.nodes.length })}</span>
<span>{t('organization.connectionsCount', { count: graphData.edges.length })}</span>
</div>
<div className="flex gap-3 text-xs">
<div className="flex items-center gap-1">
<div className="w-3 h-3 rounded-full bg-[#3b82f6]"></div>
<span>{t('organization.legend.organization')}</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 bg-[#10b981]"></div>
<span>{t('organization.legend.site')}</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-3 bg-[#f59e0b] rotate-45"></div>
<span>{t('organization.legend.resource')}</span>
</div>
</div>
</div>
</>
)}
</CardContent>
</Card>
);
}