Initial Entry
This commit is contained in:
parent
8c51096bf5
commit
607add2a7b
29
README.md
29
README.md
@ -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
7
jsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
next.config.js
Normal file
12
next.config.js
Normal 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
22
package.json
Normal 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
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
3
src/app/globals.css
Normal file
3
src/app/globals.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
17
src/app/layout.js
Normal file
17
src/app/layout.js
Normal 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
561
src/app/page.jsx
Normal 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 (<40%) |
|
||||||
|
<span className="text-orange-500 font-medium"> Orange</span>:
|
||||||
|
Medium (40-70%) |
|
||||||
|
<span className="text-red-600 font-medium"> Red</span>: High
|
||||||
|
(>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
20
src/middleware.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
122
src/utilities/runtime-helpers.js
Normal file
122
src/utilities/runtime-helpers.js
Normal 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
10
tailwind.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
"content": [
|
||||||
|
"./src/**/*.{js,jsx,ts,tsx}"
|
||||||
|
],
|
||||||
|
"theme": {
|
||||||
|
"extend": {},
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user