diff --git a/.env.example b/.env.example index 120c42a14e4d9a38560aa07e25d65a9288939b73..933b4a6d2ead1895b1b0c4b86722bbd9bf1072b0 100644 --- a/.env.example +++ b/.env.example @@ -17,8 +17,12 @@ NEXTAUTH_WELL_KNOWN_OVERRIDE="http://localhost:8085/auth/.well-known/openid-conf NEXTAUTH_AUTHORIZATION_SCOPE_OVERRIDE="openid profile" NEXTAUTH_URL=http://localhost:3000/api/auth +# OPA Settings OPA_PUBLIC_BUNDLE_URL=http://localhost:8181/v1/data/opa/public-bundle +#Maps Settings +NETWORK_TOPOLOGY_API_URL="https://orchestrator.uat.gap.geant.org/api/v1/networks/topology" + # docker-compose variables KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin diff --git a/components/NetworkMap/NetworkMap.tsx b/components/NetworkMap/NetworkMap.tsx new file mode 100644 index 0000000000000000000000000000000000000000..73e058efa1ec069eca21467ab7768ce1105a7f76 --- /dev/null +++ b/components/NetworkMap/NetworkMap.tsx @@ -0,0 +1,381 @@ +import { + EuiToolTip, + EuiSelect, + EuiLoadingSpinner, + EuiPanel, + EuiFormRow, +} from '@elastic/eui'; +import cytoscape, { ElementDefinition, Core } from 'cytoscape'; +import fcose from 'cytoscape-fcose'; +import React, { useEffect, useState, useRef } from 'react'; + +cytoscape.use(fcose); + +interface IptrunkSideNode { + subscription_instance_id: string; + router_fqdn: string; + router_access_via_ts: boolean; + router_lo_ipv4_address: string; + router_lo_ipv6_address: string; + router_lo_iso_address: string; + router_role: string; + vendor: string; + router_site: { + site_internal_id: string; + site_name: string; + site_city: string; + site_country: string; + site_latitude: number; + site_longitude: number; + }; +} + +interface Iptrunk { + subscription_instance_id: string; + iptrunk_isis_metric: string; + iptrunk_capacity: string; + iptrunk_type: string; + iptrunk_ipv4_network: string; + iptrunk_ipv6_network: string; + iptrunk_sides: Array<{ iptrunk_side_node: IptrunkSideNode }>; +} + +export interface NetworkTopologyData { + iptrunks: Array<{ iptrunk: Iptrunk; insync: boolean }>; +} + +export interface NetworkMapProps { + data: NetworkTopologyData | null; +} + +interface FcoseLayoutOptions { + name: 'fcose'; + quality: string; + nodeRepulsion: (node: cytoscape.NodeSingular) => number; + idealEdgeLength: (edge: cytoscape.EdgeSingular) => number; +} + +const NetworkMap: React.FC<NetworkMapProps> = ({ data }) => { + const [elements, setElements] = useState<ElementDefinition[]>([]); + const [labelType, setLabelType] = useState('iptrunkIsisMetric'); + const [loading, setLoading] = useState(true); + const [tooltipContent, setTooltipContent] = useState<JSX.Element | null>( + null, + ); + const [tooltipPosition, setTooltipPosition] = useState<{ + x: number; + y: number; + } | null>(null); + const cyRef = useRef<Core | null>(null); + + useEffect(() => { + if (data) { + const transformedElements = transformDataToElements(data); + setElements(transformedElements); + setLoading(false); + } + }, [data]); + + useEffect(() => { + if (elements.length > 0) { + const layoutOptions: FcoseLayoutOptions = { + name: 'fcose', + quality: 'proof', + nodeRepulsion: () => 10000, + idealEdgeLength: () => 300, + }; + + const cy: Core = cytoscape({ + container: document.getElementById('container'), + elements, + boxSelectionEnabled: false, + autounselectify: true, + minZoom: 0.25, + maxZoom: 4, + style: [ + { + selector: 'node', + style: { + label: 'data(label)', + 'background-color': '#95a5a6', + 'font-size': '12px', + color: 'black', + }, + }, + { + selector: '.router', + style: { + width: 1, + height: 1, + 'background-fit': 'contain', + 'background-position-x': '50%', + 'background-position-y': '50%', + 'background-clip': 'none', + }, + }, + { + selector: '.nokia', + style: { + 'background-image': 'url("/images/nokia_icon.png")', + }, + }, + { + selector: '.juniper', + style: { + 'background-image': 'url("/images/juniper_icon.png")', + }, + }, + { + selector: 'edge', + style: { + width: 3, + 'line-color': '#3498db', + 'target-arrow-color': '#3498db', + 'curve-style': 'bezier', + label: 'data(label)', + 'text-rotation': 'autorotate', + 'font-size': '10px', + color: '#2c3e50', + 'text-outline-color': '#ecf0f1', + 'text-outline-width': 3, + }, + }, + { + selector: 'edge[insync="true"]', + style: { + 'line-color': '#2ecc71', + }, + }, + { + selector: 'edge[insync="false"]', + style: { + 'line-color': '#e74c3c', + }, + }, + { + selector: '.site', + style: { + 'background-opacity': 0.1, + 'text-valign': 'top', + 'text-halign': 'left', + 'font-weight': 'bold', + 'font-size': '14px', + 'border-color': '#2980b9', + 'border-width': 1, + }, + }, + ], + layout: layoutOptions, + }); + + cyRef.current = cy; + + cy.on('zoom', function () { + updateElementStyles(cy); + }); + + cy.on('mouseover', 'node', function (event) { + const node = event.target; + const router = node.data('self'); + if (router) { + const nodeInfo = ( + <div style={{ whiteSpace: 'nowrap' }}> + <strong>subscription_id:</strong>{' '} + {router.subscription_instance_id || 'N/A'} <br /> + <strong>router_fqdn:</strong> {router.router_fqdn || 'N/A'} <br /> + <strong>router_access_via_ts:</strong>{' '} + {router.router_access_via_ts ? 'true' : 'false'} <br /> + <strong>router_lo_ipv4_address:</strong>{' '} + {router.router_lo_ipv4_address || 'N/A'} <br /> + <strong>router_lo_ipv6_address:</strong>{' '} + {router.router_lo_ipv6_address || 'N/A'} <br /> + <strong>router_lo_iso_address:</strong>{' '} + {router.router_lo_iso_address || 'N/A'} <br /> + <strong>router_role:</strong> {router.router_role || 'N/A'} <br /> + <strong>vendor:</strong> {router.vendor || 'N/A'} <br /> + </div> + ); + + setTooltipContent(nodeInfo); + setTooltipPosition({ + x: event.renderedPosition.x, + y: event.renderedPosition.y, + }); + } + }); + + cy.on('mouseout', 'node', function () { + setTooltipContent(null); + setTooltipPosition(null); + }); + } + }, [elements]); + + const handleLabelChange = (e: React.ChangeEvent<HTMLSelectElement>) => { + const selectedValue = e.target.value; + setLabelType(selectedValue); + const cy = cyRef.current; + if (cy) { + cy.edges().forEach((edge) => { + const iptrunk = edge.data('self'); + let label = ''; + switch (selectedValue) { + case 'iptrunkCapacity': + label = iptrunk.iptrunk_capacity; + break; + case 'iptrunkIsisMetric': + label = iptrunk.iptrunk_isis_metric; + break; + case 'iptrunkType': + label = iptrunk.iptrunk_type; + break; + case 'iptrunkIPV4Network': + label = iptrunk.iptrunk_ipv4_network; + break; + case 'iptrunkIPV6Network': + label = iptrunk.iptrunk_ipv6_network; + break; + } + edge.data('label', label); + }); + } + }; + + const transformDataToElements = ( + data: NetworkTopologyData, + ): ElementDefinition[] => { + const elements: ElementDefinition[] = []; + const sites = new Map<string, boolean>(); + + data.iptrunks.forEach((trunk) => { + trunk.iptrunk.iptrunk_sides.forEach((side) => { + const siteData = side.iptrunk_side_node.router_site; + const siteId = `site-${siteData.site_internal_id}`; + + if (!sites.has(siteId)) { + const { x, y } = latLongToXY( + siteData.site_latitude, + siteData.site_longitude, + ); + elements.push({ + data: { + id: siteId, + label: `${siteData.site_name} (${siteData.site_city}, ${siteData.site_country})`, + }, + position: { x, y }, + classes: ['site'], + }); + sites.set(siteId, true); + } + elements.push({ + data: { + id: side.iptrunk_side_node.subscription_instance_id, + parent: siteId, + label: side.iptrunk_side_node.router_fqdn, + self: side.iptrunk_side_node, + }, + classes: ['router', side.iptrunk_side_node.vendor.toLowerCase()], + }); + }); + + elements.push({ + data: { + id: trunk.iptrunk.subscription_instance_id, + source: + trunk.iptrunk.iptrunk_sides[0].iptrunk_side_node + .subscription_instance_id, + target: + trunk.iptrunk.iptrunk_sides[1].iptrunk_side_node + .subscription_instance_id, + label: trunk.iptrunk.iptrunk_isis_metric, + insync: trunk.insync.toString(), + self: trunk.iptrunk, + }, + }); + }); + + return elements; + }; + + const latLongToXY = ( + latitude: number, + longitude: number, + ): { x: number; y: number } => { + const x = (longitude + 180) * (1000 / 360); + const y = (90 - latitude) * (500 / 180); + return { x, y }; + }; + + const updateElementStyles = (cy: Core) => { + const zoom = cy.zoom(); + + const baseNodeSize = 10; + const baseFontSize = 5; + + const scaledNodeSize = baseNodeSize * zoom; + const scaledFontSize = baseFontSize * zoom; + + cy.style() + .selector('node') + .style({ + width: scaledNodeSize, + height: scaledNodeSize, + 'font-size': scaledFontSize, + }) + .update(); + }; + + return ( + <EuiPanel> + {loading ? ( + <EuiLoadingSpinner size="xl" /> + ) : ( + <div id="container" style={{ height: '80vh', marginTop: '20px' }}></div> + )} + {tooltipContent && tooltipPosition && ( + <div + style={{ + position: 'absolute', + left: tooltipPosition.x + 20, + top: tooltipPosition.y + 20, + pointerEvents: 'none', + zIndex: 9999, + }} + > + <EuiToolTip position="top" display="block"> + <div + style={{ + display: 'inline-block', + width: '450px', + backgroundColor: 'white', + border: '1px solid #ccc', + padding: '10px', + borderRadius: '5px', + zIndex: 9999, + boxShadow: '0 0 10px rgba(0,0,0,0.2)', + }} + > + {tooltipContent} + </div> + </EuiToolTip> + </div> + )} + <EuiFormRow fullWidth> + <EuiSelect + id="labelSelector" + options={[ + { value: 'iptrunkIsisMetric', text: 'Iptrunk ISIS Metric' }, + { value: 'iptrunkCapacity', text: 'Iptrunk Capacity' }, + { value: 'iptrunkType', text: 'Iptrunk Type' }, + { value: 'iptrunkIPV4Network', text: 'Iptrunk IPV4 Network' }, + { value: 'iptrunkIPV6Network', text: 'Iptrunk IPV6 Network' }, + ]} + value={labelType} + onChange={handleLabelChange} + /> + </EuiFormRow> + </EuiPanel> + ); +}; + +export default NetworkMap; diff --git a/package-lock.json b/package-lock.json index 57f2d3a6e5872c0cf2a9547a6b04fff99ff8d4ba..6272edce36340063cc55805a9a4cea3ed00a062d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,9 @@ "@open-policy-agent/opa-wasm": "^1.8.1", "@orchestrator-ui/orchestrator-ui-components": "1.20.0", "@reduxjs/toolkit": "^2.0.1", + "axios": "^1.7.2", + "cytoscape": "^3.29.2", + "cytoscape-fcose": "^2.2.0", "moment": "^2.29.4", "next": "^14.0.4", "next-auth": "^4.24.5", @@ -34,8 +37,10 @@ "@orchestrator-ui/jest-config": "*", "@orchestrator-ui/tsconfig": "*", "@testing-library/jest-dom": "^6.2.0", + "@types/cytoscape": "^3.21.2", + "@types/cytoscape-fcose": "^2.2.4", "@types/node": "^20.10.5", - "@types/react": "^18.2.45", + "@types/react": "^18.3.3", "@types/react-dom": "^18.2.18", "@types/react-no-ssr": "^1.1.7", "esbuild-jest": "^0.5.0", @@ -2365,6 +2370,126 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2913,6 +3038,21 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cytoscape": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.2.tgz", + "integrity": "sha512-eadSCH9hbRV9Ej93S8wact/Oe0DuQV4qlWfST/I6k4lBu6RgqI6mVFzvppcIGzWpzZRu7iyFjFhS2qW0w7KFRA==", + "dev": true + }, + "node_modules/@types/cytoscape-fcose": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@types/cytoscape-fcose/-/cytoscape-fcose-2.2.4.tgz", + "integrity": "sha512-QwWtnT8HI9h+DHhG5krGc1ZY0Ex+cn85MvX96ZNAjSxuXiZDnjIZW/ypVkvvubTjIY4rSdkJY1D/Nsn8NDpmAw==", + "dev": true, + "dependencies": { + "@types/cytoscape": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3076,9 +3216,9 @@ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { - "version": "18.3.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", - "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==", + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3794,9 +3934,9 @@ } }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -4481,6 +4621,14 @@ "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", "hasInstallScript": true }, + "node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "dependencies": { + "layout-base": "^2.0.0" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -4625,6 +4773,25 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/cytoscape": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.29.2.tgz", + "integrity": "sha512-2G1ycU28Nh7OHT9rkXRLpCDP30MKH1dXJORZuBhtEhEW7pKwgPi77ImqlCWinouyE1PNepIOGZBOrE84DG7LyQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -10388,6 +10555,11 @@ "node": ">=0.10" } }, + "node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -15078,126 +15250,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", - "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", - "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", - "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", - "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", - "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", - "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", - "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", - "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index 081486109837d99ba8a54a43149f2af18239c677..3ca3cd0a909d5e56d0b21b1cf15b2d3c7388f12b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "@open-policy-agent/opa-wasm": "^1.8.1", "@orchestrator-ui/orchestrator-ui-components": "1.20.0", "@reduxjs/toolkit": "^2.0.1", + "axios": "^1.7.2", + "cytoscape": "^3.29.2", + "cytoscape-fcose": "^2.2.0", "moment": "^2.29.4", "next": "^14.0.4", "next-auth": "^4.24.5", @@ -40,16 +43,18 @@ "@orchestrator-ui/jest-config": "*", "@orchestrator-ui/tsconfig": "*", "@testing-library/jest-dom": "^6.2.0", + "@types/cytoscape": "^3.21.2", + "@types/cytoscape-fcose": "^2.2.4", "@types/node": "^20.10.5", - "@types/react": "^18.2.45", + "@types/react": "^18.3.3", "@types/react-dom": "^18.2.18", "@types/react-no-ssr": "^1.1.7", "esbuild-jest": "^0.5.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^3.4.0", "husky": "^9.0.11", - "typescript": "^5.3.2", "prettier": "^2.3.2", - "eslint-plugin-prettier": "^3.4.0", - "eslint-config-prettier": "^8.3.0" + "typescript": "^5.3.2" }, "overrides": { "@elastic/eui": { diff --git a/pages/_app.tsx b/pages/_app.tsx index 13098704df7ecf805847be9136850b84873d16b5..ce0fbaef07bb661c9a0749f9fb937aeaf3f03a7c 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -9,6 +9,7 @@ import { TranslationsProvider } from '@/translations/translationsProvider'; import { EuiProvider, EuiThemeColorMode } from '@elastic/eui'; import '@elastic/eui/dist/eui_theme_dark.min.css'; import '@elastic/eui/dist/eui_theme_light.min.css'; +import { EuiSideNavItemType } from '@elastic/eui/src/components/side_nav/side_nav_types'; import { ApiClientContextProvider, ColorModes, @@ -20,11 +21,13 @@ import { WfoPageTemplate, WfoToastsList, defaultOrchestratorTheme, + WfoMenuItemLink, } from '@orchestrator-ui/orchestrator-ui-components'; import { SessionProvider } from 'next-auth/react'; import { NextAdapter } from 'next-query-params'; import App, { AppContext, AppInitialProps, AppProps } from 'next/app'; import Head from 'next/head'; +import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; import NoSSR from 'react-no-ssr'; import { QueryClient, QueryClientProvider } from 'react-query'; @@ -48,6 +51,8 @@ function CustomApp({ pageProps, orchestratorConfig, }: AppProps & AppOwnProps) { + const router = useRouter(); + const [queryClient] = useState(() => new QueryClient(queryClientConfig)); const [themeMode, setThemeMode] = useState<EuiThemeColorMode>( ColorModes.LIGHT, @@ -84,6 +89,25 @@ function CustomApp({ return null; } + const addMenuItems = ( + defaultMenuItems: EuiSideNavItemType<object>[], + ): EuiSideNavItemType<object>[] => [ + ...defaultMenuItems, + { + name: 'Maps', + id: '10', + isSelected: router.pathname === '/maps', + href: '/maps', + renderItem: () => ( + <WfoMenuItemLink + path={'/maps'} + translationString="Maps" + isSelected={router.pathname === '/maps'} + /> + ), + }, + ]; + return ( <WfoErrorBoundary> <OrchestratorConfigProvider @@ -113,6 +137,7 @@ function CustomApp({ <WfoPageTemplate getAppLogo={getAppLogo} onThemeSwitch={handleThemeSwitch} + overrideMenuItems={addMenuItems} > <QueryParamProvider adapter={NextAdapter} diff --git a/pages/maps/index.tsx b/pages/maps/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..49ed824481f60a2fb89a635ddbd1307f6102ee60 --- /dev/null +++ b/pages/maps/index.tsx @@ -0,0 +1,43 @@ +import NetworkMap, { + NetworkMapProps, + NetworkTopologyData, +} from '@/components/NetworkMap/NetworkMap'; +import axios from 'axios'; +import { NextPage, GetServerSideProps } from 'next'; +import Head from 'next/head'; +import React from 'react'; + +const Maps: NextPage<NetworkMapProps> = ({ data }) => { + return ( + <> + <Head> + <title>Maps</title> + </Head> + <div> + <NetworkMap data={data} /> + </div> + </> + ); +}; + +export const getServerSideProps: GetServerSideProps = async () => { + try { + const response = await axios.get(process.env.NETWORK_TOPOLOGY_API_URL!); + const networkTopologyData: NetworkTopologyData = response.data; + + return { + props: { + networkTopologyData, + }, + }; + } catch (error) { + console.error('Failed to fetch data', error); + return { + props: { + networkTopologyData: null, + }, + }; + } +}; + +export default Maps; diff --git a/public/images/juniper_icon.png b/public/images/juniper_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3cd1f608c7006c42c918b17808682b171f73e88e Binary files /dev/null and b/public/images/juniper_icon.png differ diff --git a/public/images/nokia_icon.png b/public/images/nokia_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7f4a5010e50e457983a3ac2a7b11ed0267477a04 Binary files /dev/null and b/public/images/nokia_icon.png differ