feat: enhance ProjectNetworkGraph layout and add tab navigation in ProjectOverview

This commit is contained in:
biersoeckli
2025-12-22 14:02:03 +00:00
parent 12c01e4e0c
commit c80fcd4b07
2 changed files with 68 additions and 26 deletions
@@ -135,28 +135,36 @@ export default function ProjectNetworkGraph({ apps }: ProjectNetworkGraphProps)
const nodes: Node[] = []; const nodes: Node[] = [];
const edges: Edge[] = []; const edges: Edge[] = [];
const radius = 200; // Separate apps with domains and without domains
const centerX = 400; const appsWithDomains = apps.filter(app => app.appDomains.length > 0);
const centerY = 300; const appsWithoutDomains = apps.filter(app => app.appDomains.length === 0);
const nodeSpacing = 250; // Horizontal spacing between nodes
const rowSpacing = 150; // Vertical spacing between rows
const internetY = 100;
const firstRowY = internetY + 200;
const secondRowY = firstRowY + rowSpacing;
// Check if we need an Internet node // Check if we need an Internet node
const hasInternetAccess = apps.some(app => app.appDomains.length > 0); const hasInternetAccess = appsWithDomains.length > 0;
const internetX = (Math.max(appsWithDomains.length, appsWithoutDomains.length) * nodeSpacing) / 2;
if (hasInternetAccess) { if (hasInternetAccess) {
nodes.push({ nodes.push({
id: 'INTERNET', id: 'INTERNET',
position: { x: centerX, y: centerY - radius - 150 }, position: { x: internetX, y: internetY },
data: { label: 'Internet' }, data: { label: 'Internet' },
style: { background: '#e0e0e0', border: '1px solid #777', padding: 10, borderRadius: '50%', width: 100, height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold' }, style: { background: '#e0e0e0', border: '1px solid #777', padding: 10, borderRadius: '50%', width: 100, height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold' },
type: 'input', // It's a source only type: 'input', // It's a source only
}); });
} }
apps.forEach((app, index) => { // First row: Apps with domains (internet accessible)
// Circular layout appsWithDomains.forEach((app, index) => {
const angle = (index / apps.length) * 2 * Math.PI; const totalWidth = (appsWithDomains.length - 1) * nodeSpacing;
const x = centerX + radius * Math.cos(angle); const startX = internetX - (totalWidth / 2);
const y = centerY + radius * Math.sin(angle); const x = startX + (index * nodeSpacing);
const y = firstRowY;
const ports = Array.from(new Set([ const ports = Array.from(new Set([
...app.appDomains, ...app.appDomains,
@@ -178,20 +186,45 @@ export default function ProjectNetworkGraph({ apps }: ProjectNetworkGraphProps)
}); });
// Edge from Internet to App // Edge from Internet to App
if (app.appDomains.length > 0) { const hostnames = app.appDomains.map(d => d.hostname).join(', ');
const hostnames = app.appDomains.map(d => d.hostname).join(', '); edges.push({
edges.push({ id: `INTERNET-${app.id}`,
id: `INTERNET-${app.id}`, source: 'INTERNET',
source: 'INTERNET', target: app.id,
target: app.id, label: `${hostnames}`,
label: `${hostnames}`, markerEnd: {
markerEnd: { type: MarkerType.ArrowClosed,
type: MarkerType.ArrowClosed, },
}, animated: true,
animated: true, style: { stroke: '#000' },
style: { stroke: '#000' }, });
}); });
}
// Second row: Apps without domains (not internet accessible)
appsWithoutDomains.forEach((app, index) => {
const totalWidth = (appsWithoutDomains.length - 1) * nodeSpacing;
const startX = internetX - (totalWidth / 2);
const x = startX + (index * nodeSpacing);
const y = secondRowY;
const ports = Array.from(new Set([
...app.appDomains,
...app.appPorts
].map(d => d.port))).join(', ');
nodes.push({
id: app.id,
position: { x, y },
data: {
label: app.name,
ingressPolicy: app.ingressNetworkPolicy,
egressPolicy: app.egressNetworkPolicy,
appId: app.id,
app,
ports
},
type: 'appNode',
});
}); });
return { nodes, edges }; return { nodes, edges };
@@ -5,6 +5,7 @@ import AppTable from "./apps-table";
import ProjectNetworkGraph from "./project-network-graph"; import ProjectNetworkGraph from "./project-network-graph";
import { App } from "@prisma/client"; import { App } from "@prisma/client";
import { UserSession } from "@/shared/model/sim-session.model"; import { UserSession } from "@/shared/model/sim-session.model";
import { useRouter, useSearchParams } from "next/navigation";
interface ProjectOverviewProps { interface ProjectOverviewProps {
apps: any[]; // Using any to avoid complex type imports, as we know the data structure is correct apps: any[]; // Using any to avoid complex type imports, as we know the data structure is correct
@@ -13,8 +14,16 @@ interface ProjectOverviewProps {
} }
export default function ProjectOverview({ apps, session, projectId }: ProjectOverviewProps) { export default function ProjectOverview({ apps, session, projectId }: ProjectOverviewProps) {
const router = useRouter();
const searchParams = useSearchParams();
const currentTab = searchParams.get('tab') || 'table';
const handleTabChange = (value: string) => {
router.push(`?tab=${value}`, { scroll: false });
};
return ( return (
<Tabs defaultValue="table" className="w-full"> <Tabs value={currentTab} onValueChange={handleTabChange} className="w-full">
<TabsList> <TabsList>
<TabsTrigger value="table">Table View</TabsTrigger> <TabsTrigger value="table">Table View</TabsTrigger>
<TabsTrigger value="graph">Network Graph</TabsTrigger> <TabsTrigger value="graph">Network Graph</TabsTrigger>