From 607add2a7b7d2abecff5efdebb2cad6d4b44319b Mon Sep 17 00:00:00 2001 From: Gert-Jan Aalderink Date: Wed, 9 Jul 2025 21:33:13 +0200 Subject: [PATCH] Initial Entry --- README.md | 29 +- jsconfig.json | 7 + next.config.js | 12 + package.json | 22 ++ postcss.config.js | 6 + src/app/globals.css | 3 + src/app/layout.js | 17 + src/app/page.jsx | 561 +++++++++++++++++++++++++++++++ src/middleware.js | 20 ++ src/utilities/runtime-helpers.js | 122 +++++++ tailwind.config.js | 10 + 11 files changed, 807 insertions(+), 2 deletions(-) create mode 100644 jsconfig.json create mode 100644 next.config.js create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 src/app/globals.css create mode 100644 src/app/layout.js create mode 100644 src/app/page.jsx create mode 100644 src/middleware.js create mode 100644 src/utilities/runtime-helpers.js create mode 100644 tailwind.config.js diff --git a/README.md b/README.md index 0cd91e1..344818c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,28 @@ -# ospfcost +This project was generated from [create.xyz](https://create.xyz/). -OSPF cost webapp simulator \ No newline at end of file +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. \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..e8b3494 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + } +} \ No newline at end of file diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..2e5f142 --- /dev/null +++ b/next.config.js @@ -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; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6edec10 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..96bb01e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/src/app/layout.js b/src/app/layout.js new file mode 100644 index 0000000..96c6a87 --- /dev/null +++ b/src/app/layout.js @@ -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 ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/app/page.jsx b/src/app/page.jsx new file mode 100644 index 0000000..c2edc5a --- /dev/null +++ b/src/app/page.jsx @@ -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 ( +
+ + {/* 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 ( + + setSelectedLink(link)} + /> + {/* Link labels */} + setSelectedLink(link)} + > + Cost: {link.cost} + + 70 + ? "#dc2626" + : traffic > 40 + ? "#d97706" + : "#059669", + }} + > + {traffic.toFixed(1)}% + + + ); + })} + + {/* Draw nodes */} + {positionedNodes.map((node) => ( + + + + {node.name} + + + ))} + +
+ ); + }; + + return ( +
+
+

+ OSPF Network Cost Simulator +

+ + {/* File Upload */} +
+

+ Upload Network Topology +

+
+ +
+

+ Upload a text file with columns: Switch A, Switch B, Cost, Bandwidth + (Gb) +

+ {error && ( +
+ {error} +
+ )} +
+ + {/* Traffic Utilization Control */} + {nodes.length > 0 && ( +
+

+ Network Traffic Utilization +

+
+ +
+ +
+ 10% + 50% + 100% +
+
+
+ + {utilizationPercentage}% + +
+
+
+

• Adjust the overall network traffic utilization level

+

+ • Higher utilization may affect link behavior and performance +

+

+ • Green: Low + utilization (<40%) | + Orange: + Medium (40-70%) | + Red: High + (>70%) +

+
+
+ )} + + {/* Network Visualization */} + {nodes.length > 0 && ( +
+
+

Network Topology

+ +
+ +
+

• Click on links to modify OSPF costs

+

+ • Line thickness and color represent traffic utilization level +

+

• Percentages show current traffic distribution

+
+
+ )} + + {/* Link Editor */} + {selectedLink && ( +
+

Edit Link Cost

+
+
+

+ Link: {selectedLink.switchA} ↔{" "} + {selectedLink.switchB} +

+

+ Current Cost: {selectedLink.cost} +

+

+ Original Cost: {selectedLink.originalCost} +

+

+ Bandwidth: {selectedLink.bandwidth} Gb +

+

+ Current Traffic:{" "} + 70 + ? "#dc2626" + : trafficDistribution[selectedLink.id] > 40 + ? "#d97706" + : "#059669", + }} + > + {(trafficDistribution[selectedLink.id] || 0).toFixed(1)}% + +

+
+
+ +
+ 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" + /> + + +
+
+
+
+ )} + + {/* Link Summary Table */} + {links.length > 0 && ( +
+

Link Summary

+
+ + + + + + + + + + + + + + {links.map((link) => { + const traffic = trafficDistribution[link.id] || 0; + return ( + + + + + + + + + + ); + })} + +
Switch ASwitch BCurrent CostOriginal CostBandwidth (Gb)Traffic %Actions
{link.switchA}{link.switchB}{link.cost} + {link.originalCost} + {link.bandwidth} + 70 + ? "#dc2626" + : traffic > 40 + ? "#d97706" + : "#059669", + }} + > + {traffic.toFixed(1)}% + + + +
+
+
+ )} +
+
+ ); +} + +export default MainComponent; \ No newline at end of file diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..17227b0 --- /dev/null +++ b/src/middleware.js @@ -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, + }, + }); +} \ No newline at end of file diff --git a/src/utilities/runtime-helpers.js b/src/utilities/runtime-helpers.js new file mode 100644 index 0000000..e04a728 --- /dev/null +++ b/src/utilities/runtime-helpers.js @@ -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, +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..1773996 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,10 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + "content": [ + "./src/**/*.{js,jsx,ts,tsx}" + ], + "theme": { + "extend": {}, + "plugins": [] + } +} \ No newline at end of file