mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
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
- 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
284 lines
8.4 KiB
TypeScript
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>
|
|
);
|
|
}
|