Initial Entry

This commit is contained in:
Gert-Jan Aalderink 2025-07-09 21:33:13 +02:00
parent 8c51096bf5
commit 607add2a7b
11 changed files with 807 additions and 2 deletions

View File

@ -1,3 +1,28 @@
# ospfcost This project was generated from [create.xyz](https://create.xyz/).
OSPF cost webapp simulator It is a [Next.js](https://nextjs.org/) project built on React and TailwindCSS.
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the code in `src`. The page auto-updates as you edit the file.
To learn more, take a look at the following resources:
- [React Documentation](https://react.dev/) - learn about React
- [TailwindCSS Documentation](https://tailwindcss.com/) - learn about TailwindCSS
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

7
jsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}

12
next.config.js Normal file
View File

@ -0,0 +1,12 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
esmExternals: 'loose'
},
webpack: (config) => {
config.externals = [...config.externals, { canvas: "canvas" }]; // required to make pdfjs work
return config;
},
};
module.exports = nextConfig;

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "create-project",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "14.0.4"
},
"devDependencies": {
"autoprefixer": "^10.0.1",
"postcss": "^8",
"tailwindcss": "^3.3.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

3
src/app/globals.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

17
src/app/layout.js Normal file
View File

@ -0,0 +1,17 @@
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create.xyz App',
description: 'Generated by create.xyz',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}

561
src/app/page.jsx Normal file
View File

@ -0,0 +1,561 @@
"use client";
import React from "react";
function MainComponent() {
const [networkData, setNetworkData] = useState(null);
const [links, setLinks] = useState([]);
const [nodes, setNodes] = useState([]);
const [trafficDistribution, setTrafficDistribution] = useState({});
const [selectedLink, setSelectedLink] = useState(null);
const [newCost, setNewCost] = useState("");
const [error, setError] = useState("");
const [utilizationPercentage, setUtilizationPercentage] = useState(50);
// Parse uploaded file
const parseNetworkFile = (content) => {
try {
const lines = content.trim().split("\n");
const header = lines[0];
if (
!header.toLowerCase().includes("switch a") ||
!header.toLowerCase().includes("switch b")
) {
throw new Error('File must contain "Switch A" and "Switch B" columns');
}
const parsedLinks = [];
const nodeSet = new Set();
for (let i = 1; i < lines.length; i++) {
const parts = lines[i].split(/[,\t]/).map((p) => p.trim());
if (parts.length >= 4) {
const switchA = parts[0];
const switchB = parts[1];
const cost = parseFloat(parts[2]);
const bandwidth = parseFloat(parts[3]);
if (switchA && switchB && !isNaN(cost) && !isNaN(bandwidth)) {
parsedLinks.push({
id: `${switchA}-${switchB}`,
switchA,
switchB,
cost,
originalCost: cost,
bandwidth,
});
nodeSet.add(switchA);
nodeSet.add(switchB);
}
}
}
const nodeArray = Array.from(nodeSet).map((name) => ({ id: name, name }));
setLinks(parsedLinks);
setNodes(nodeArray);
calculateTrafficDistribution(parsedLinks, nodeArray);
setError("");
} catch (err) {
setError(`Error parsing file: ${err.message}`);
}
};
// Calculate OSPF shortest paths and traffic distribution
const calculateTrafficDistribution = (linkData, nodeData) => {
if (linkData.length === 0 || nodeData.length === 0) return;
// Build adjacency list with costs
const graph = {};
nodeData.forEach((node) => {
graph[node.id] = {};
});
linkData.forEach((link) => {
graph[link.switchA][link.switchB] = link.cost;
graph[link.switchB][link.switchA] = link.cost;
});
// Calculate traffic for each link based on shortest paths
const linkTraffic = {};
linkData.forEach((link) => {
linkTraffic[link.id] = 0;
});
// For each pair of nodes, find shortest path and add traffic
nodeData.forEach((source) => {
nodeData.forEach((destination) => {
if (source.id !== destination.id) {
const path = dijkstra(graph, source.id, destination.id);
if (path.length > 1) {
// Add traffic to each link in the path
for (let i = 0; i < path.length - 1; i++) {
const linkId1 = `${path[i]}-${path[i + 1]}`;
const linkId2 = `${path[i + 1]}-${path[i]}`;
if (linkTraffic.hasOwnProperty(linkId1)) {
linkTraffic[linkId1] += 1;
} else if (linkTraffic.hasOwnProperty(linkId2)) {
linkTraffic[linkId2] += 1;
}
}
}
}
});
});
// Apply utilization percentage scaling
const maxTraffic = Math.max(...Object.values(linkTraffic));
const trafficPercentages = {};
Object.keys(linkTraffic).forEach((linkId) => {
const baseTraffic =
maxTraffic > 0 ? (linkTraffic[linkId] / maxTraffic) * 100 : 0;
trafficPercentages[linkId] = (baseTraffic * utilizationPercentage) / 100;
});
setTrafficDistribution(trafficPercentages);
};
// Dijkstra's algorithm for shortest path
const dijkstra = (graph, start, end) => {
const distances = {};
const previous = {};
const unvisited = new Set();
Object.keys(graph).forEach((node) => {
distances[node] = Infinity;
previous[node] = null;
unvisited.add(node);
});
distances[start] = 0;
while (unvisited.size > 0) {
let current = null;
unvisited.forEach((node) => {
if (current === null || distances[node] < distances[current]) {
current = node;
}
});
if (current === end) break;
unvisited.delete(current);
Object.keys(graph[current]).forEach((neighbor) => {
if (unvisited.has(neighbor)) {
const alt = distances[current] + graph[current][neighbor];
if (alt < distances[neighbor]) {
distances[neighbor] = alt;
previous[neighbor] = current;
}
}
});
}
// Reconstruct path
const path = [];
let current = end;
while (current !== null) {
path.unshift(current);
current = previous[current];
}
return distances[end] === Infinity ? [] : path;
};
// Handle file upload
const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
parseNetworkFile(e.target.result);
};
reader.readAsText(file);
}
};
// Update link cost
const updateLinkCost = () => {
if (selectedLink && newCost !== "") {
const cost = parseFloat(newCost);
if (!isNaN(cost) && cost > 0) {
const updatedLinks = links.map((link) =>
link.id === selectedLink.id ? { ...link, cost } : link
);
setLinks(updatedLinks);
calculateTrafficDistribution(updatedLinks, nodes);
setSelectedLink(null);
setNewCost("");
}
}
};
// Reset all costs to original
const resetCosts = () => {
const resetLinks = links.map((link) => ({
...link,
cost: link.originalCost,
}));
setLinks(resetLinks);
calculateTrafficDistribution(resetLinks, nodes);
};
// Handle utilization slider change
const handleUtilizationChange = (event) => {
const newUtilization = parseInt(event.target.value);
setUtilizationPercentage(newUtilization);
// Recalculate traffic distribution with new utilization
calculateTrafficDistribution(links, nodes);
};
// Network visualization component
const NetworkVisualization = () => {
if (nodes.length === 0) return null;
const svgWidth = 800;
const svgHeight = 600;
const nodeRadius = 30;
// Position nodes in a circle for better visualization
const centerX = svgWidth / 2;
const centerY = svgHeight / 2;
const radius = Math.min(svgWidth, svgHeight) / 3;
const positionedNodes = nodes.map((node, index) => {
const angle = (2 * Math.PI * index) / nodes.length;
return {
...node,
x: centerX + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle),
};
});
return (
<div className="border border-gray-300 rounded-lg p-4 bg-white">
<svg width={svgWidth} height={svgHeight} className="border">
{/* Draw links */}
{links.map((link) => {
const nodeA = positionedNodes.find((n) => n.id === link.switchA);
const nodeB = positionedNodes.find((n) => n.id === link.switchB);
if (!nodeA || !nodeB) return null;
const traffic = trafficDistribution[link.id] || 0;
const strokeWidth = Math.max(2, traffic / 5);
const opacity = Math.max(0.3, traffic / 50);
// Color coding based on utilization level
let strokeColor = "rgba(34, 197, 94, 0.8)";
if (traffic > 70) {
strokeColor = "rgba(239, 68, 68, 0.8)";
} else if (traffic > 40) {
strokeColor = "rgba(245, 158, 11, 0.8)";
}
return (
<g key={link.id}>
<line
x1={nodeA.x}
y1={nodeA.y}
x2={nodeB.x}
y2={nodeB.y}
stroke={strokeColor}
strokeWidth={strokeWidth}
className="cursor-pointer hover:opacity-100"
onClick={() => setSelectedLink(link)}
/>
{/* Link labels */}
<text
x={(nodeA.x + nodeB.x) / 2}
y={(nodeA.y + nodeB.y) / 2 - 10}
textAnchor="middle"
className="text-xs fill-gray-600 cursor-pointer"
onClick={() => setSelectedLink(link)}
>
Cost: {link.cost}
</text>
<text
x={(nodeA.x + nodeB.x) / 2}
y={(nodeA.y + nodeB.y) / 2 + 5}
textAnchor="middle"
className="text-xs font-medium"
style={{
fill:
traffic > 70
? "#dc2626"
: traffic > 40
? "#d97706"
: "#059669",
}}
>
{traffic.toFixed(1)}%
</text>
</g>
);
})}
{/* Draw nodes */}
{positionedNodes.map((node) => (
<g key={node.id}>
<circle
cx={node.x}
cy={node.y}
r={nodeRadius}
fill="#f3f4f6"
stroke="#374151"
strokeWidth="2"
/>
<text
x={node.x}
y={node.y}
textAnchor="middle"
dominantBaseline="middle"
className="text-sm font-medium fill-gray-800"
>
{node.name}
</text>
</g>
))}
</svg>
</div>
);
};
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-6xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-8">
OSPF Network Cost Simulator
</h1>
{/* File Upload */}
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">
Upload Network Topology
</h2>
<div className="mb-4">
<input
type="file"
accept=".txt,.csv"
onChange={handleFileUpload}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
/>
</div>
<p className="text-sm text-gray-600">
Upload a text file with columns: Switch A, Switch B, Cost, Bandwidth
(Gb)
</p>
{error && (
<div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
{error}
</div>
)}
</div>
{/* Traffic Utilization Control */}
{nodes.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">
Network Traffic Utilization
</h2>
<div className="flex items-center gap-4">
<label className="text-sm font-medium text-gray-700 min-w-[120px]">
Traffic Level:
</label>
<div className="flex-1 max-w-md">
<input
type="range"
min="10"
max="100"
value={utilizationPercentage}
onChange={handleUtilizationChange}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>10%</span>
<span>50%</span>
<span>100%</span>
</div>
</div>
<div className="min-w-[80px] text-right">
<span className="text-lg font-semibold text-blue-600">
{utilizationPercentage}%
</span>
</div>
</div>
<div className="mt-3 text-sm text-gray-600">
<p> Adjust the overall network traffic utilization level</p>
<p>
Higher utilization may affect link behavior and performance
</p>
<p>
<span className="text-green-600 font-medium">Green</span>: Low
utilization (&lt;40%) |
<span className="text-orange-500 font-medium"> Orange</span>:
Medium (40-70%) |
<span className="text-red-600 font-medium"> Red</span>: High
(&gt;70%)
</p>
</div>
</div>
)}
{/* Network Visualization */}
{nodes.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Network Topology</h2>
<button
onClick={resetCosts}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
>
Reset All Costs
</button>
</div>
<NetworkVisualization />
<div className="mt-4 text-sm text-gray-600">
<p> Click on links to modify OSPF costs</p>
<p>
Line thickness and color represent traffic utilization level
</p>
<p> Percentages show current traffic distribution</p>
</div>
</div>
)}
{/* Link Editor */}
{selectedLink && (
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Edit Link Cost</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p>
<strong>Link:</strong> {selectedLink.switchA} {" "}
{selectedLink.switchB}
</p>
<p>
<strong>Current Cost:</strong> {selectedLink.cost}
</p>
<p>
<strong>Original Cost:</strong> {selectedLink.originalCost}
</p>
<p>
<strong>Bandwidth:</strong> {selectedLink.bandwidth} Gb
</p>
<p>
<strong>Current Traffic:</strong>{" "}
<span
style={{
color:
trafficDistribution[selectedLink.id] > 70
? "#dc2626"
: trafficDistribution[selectedLink.id] > 40
? "#d97706"
: "#059669",
}}
>
{(trafficDistribution[selectedLink.id] || 0).toFixed(1)}%
</span>
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
New OSPF Cost:
</label>
<div className="flex gap-2">
<input
type="number"
value={newCost}
onChange={(e) => setNewCost(e.target.value)}
placeholder="Enter new cost"
min="1"
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={updateLinkCost}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Update
</button>
<button
onClick={() => {
setSelectedLink(null);
setNewCost("");
}}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
{/* Link Summary Table */}
{links.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Link Summary</h2>
<div className="overflow-x-auto">
<table className="min-w-full table-auto">
<thead>
<tr className="bg-gray-50">
<th className="px-4 py-2 text-left">Switch A</th>
<th className="px-4 py-2 text-left">Switch B</th>
<th className="px-4 py-2 text-left">Current Cost</th>
<th className="px-4 py-2 text-left">Original Cost</th>
<th className="px-4 py-2 text-left">Bandwidth (Gb)</th>
<th className="px-4 py-2 text-left">Traffic %</th>
<th className="px-4 py-2 text-left">Actions</th>
</tr>
</thead>
<tbody>
{links.map((link) => {
const traffic = trafficDistribution[link.id] || 0;
return (
<tr key={link.id} className="border-t">
<td className="px-4 py-2">{link.switchA}</td>
<td className="px-4 py-2">{link.switchB}</td>
<td className="px-4 py-2 font-semibold">{link.cost}</td>
<td className="px-4 py-2 text-gray-600">
{link.originalCost}
</td>
<td className="px-4 py-2">{link.bandwidth}</td>
<td className="px-4 py-2">
<span
className="font-medium"
style={{
color:
traffic > 70
? "#dc2626"
: traffic > 40
? "#d97706"
: "#059669",
}}
>
{traffic.toFixed(1)}%
</span>
</td>
<td className="px-4 py-2">
<button
onClick={() => setSelectedLink(link)}
className="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600"
>
Edit
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
);
}
export default MainComponent;

20
src/middleware.js Normal file
View File

@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
export const config = {
matcher: "/integrations/:path*",
};
export function middleware(request) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-createxyz-project-id", "58c86f6b-22da-4375-8516-ea44e79c1405");
requestHeaders.set("x-createxyz-project-group-id", "cc3685e9-c027-4b1c-a03f-8eda785d867e");
request.nextUrl.href = `https://www.create.xyz/${request.nextUrl.pathname}`;
return NextResponse.rewrite(request.nextUrl, {
request: {
headers: requestHeaders,
},
});
}

View File

@ -0,0 +1,122 @@
import React from 'react';
function useHandleStreamResponse({
onChunk,
onFinish
}) {
const handleStreamResponse = React.useCallback(
async (response) => {
if (response.body) {
const reader = response.body.getReader();
if (reader) {
const decoder = new TextDecoder();
let content = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
onFinish(content);
break;
}
const chunk = decoder.decode(value, { stream: true });
content += chunk;
onChunk(content);
}
}
}
},
[onChunk, onFinish]
);
const handleStreamResponseRef = React.useRef(handleStreamResponse);
React.useEffect(() => {
handleStreamResponseRef.current = handleStreamResponse;
}, [handleStreamResponse]);
return React.useCallback((response) => handleStreamResponseRef.current(response), []);
}
function useUpload() {
const [loading, setLoading] = React.useState(false);
const upload = React.useCallback(async (input) => {
try {
setLoading(true);
let response;
if ('reactNativeAsset' in input && input.reactNativeAsset) {
if (input.reactNativeAsset.file) {
const formData = new FormData();
formData.append("file", input.reactNativeAsset.file);
response = await fetch("/_create/api/upload/", {
method: "POST",
body: formData
});
} else {
const response = await fetch("/_create/api/upload/presign/", {
method: 'POST',
})
const { secureSignature, secureExpire } = await response.json();
const result = await client.uploadFile(input.reactNativeAsset, {
fileName: input.reactNativeAsset.name ?? input.reactNativeAsset.uri.split("/").pop(),
contentType: input.reactNativeAsset.mimeType,
secureSignature,
secureExpire
});
return { url: `${process.env.EXPO_PUBLIC_BASE_CREATE_USER_CONTENT_URL}/${result.uuid}/`, mimeType: result.mimeType || null };
}
} else if ("file" in input && input.file) {
const formData = new FormData();
formData.append("file", input.file);
response = await fetch("/_create/api/upload/", {
method: "POST",
body: formData
});
} else if ("url" in input) {
response = await fetch("/_create/api/upload/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ url: input.url })
});
} else if ("base64" in input) {
response = await fetch("/_create/api/upload/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ base64: input.base64 })
});
} else {
response = await fetch("/_create/api/upload/", {
method: "POST",
headers: {
"Content-Type": "application/octet-stream"
},
body: input.buffer
});
}
if (!response.ok) {
if (response.status === 413) {
throw new Error("Upload failed: File too large.");
}
throw new Error("Upload failed");
}
const data = await response.json();
return { url: data.url, mimeType: data.mimeType || null };
} catch (uploadError) {
if (uploadError instanceof Error) {
return { error: uploadError.message };
}
if (typeof uploadError === "string") {
return { error: uploadError };
}
return { error: "Upload failed" };
} finally {
setLoading(false);
}
}, []);
return [upload, { loading }];
}
export {
useHandleStreamResponse,
useUpload,
}

10
tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
"content": [
"./src/**/*.{js,jsx,ts,tsx}"
],
"theme": {
"extend": {},
"plugins": []
}
}