diff --git a/package-lock.json b/package-lock.json index 0fc567a..d92c9af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-policy-topology", - "version": "0.1.0", + "version": "0.1.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-policy-topology", - "version": "0.1.0", + "version": "0.1.10", "dependencies": { "@patternfly/patternfly": "^4.224.5", "@patternfly/react-core": "^4.278.1", @@ -21,12 +21,14 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "ws": "^8.18.0" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@puppeteer/browsers": "^2.3.1", "chai": "^5.1.1", + "concurrently": "^7.0.0", "mocha": "^10.7.3", "puppeteer": "^23.1.1", "wait-on": "^8.0.0", @@ -6747,6 +6749,126 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concurrently": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz", + "integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "date-fns": "^2.29.1", + "lodash": "^4.17.21", + "rxjs": "^7.0.0", + "shell-quote": "^1.7.3", + "spawn-command": "^0.0.2-1", + "supports-color": "^8.1.0", + "tree-kill": "^1.2.2", + "yargs": "^17.3.1" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/concurrently/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -7704,6 +7826,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -12826,6 +12965,27 @@ } } }, + "node_modules/jsdom/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -15919,27 +16079,6 @@ "node": ">=18" } }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -18392,6 +18531,12 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -19355,6 +19500,16 @@ "node": ">=8" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -20009,26 +20164,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-manifest-plugin": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", @@ -20666,15 +20801,16 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { diff --git a/package.json b/package.json index e1faf80..e8d7a9e 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,11 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "ws": "^8.18.0" }, "scripts": { - "start": "react-scripts start", + "start": "concurrently \"react-scripts start\" \"node watch-configmap.js\"", "build": "react-scripts build", "test": "mocha tests --timeout 10000", "test:ci": "npm start & wait-on http://localhost:3000 && mocha tests --timeout 10000", @@ -49,6 +50,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@puppeteer/browsers": "^2.3.1", "chai": "^5.1.1", + "concurrently": "^7.0.0", "mocha": "^10.7.3", "puppeteer": "^23.1.1", "wait-on": "^8.0.0", diff --git a/public/index.html b/public/index.html index aa069f2..b2bcea8 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Policy Machinery diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..bbb4bb5 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "Policy Machinery", + "name": "Policy Machinery", "icons": [ { "src": "favicon.ico", diff --git a/src/App.css b/src/App.css index 4ac8ad7..1a98c83 100644 --- a/src/App.css +++ b/src/App.css @@ -14,14 +14,12 @@ } .App-header { - background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); - color: white; } .App .policy-topology-container svg text { @@ -35,6 +33,10 @@ color: #61dafb; } +/* .App .policy-topology-container { + width: 400px; +} */ + .App .pf-c-dropdown__toggle { background-color: #fff; margin-bottom: 1em; diff --git a/src/App.js b/src/App.js index 8c2650d..5c95828 100644 --- a/src/App.js +++ b/src/App.js @@ -1,134 +1,48 @@ -import React, { useState, useEffect } from 'react'; -import PolicyTopology from './PolicyTopology.js'; -import PickResource from './PickResource.js'; -import * as dot from 'graphlib-dot'; // still needed for parsing dotString -import './App.css'; +import React, { useState, useEffect } from "react"; +import PolicyTopology from "./PolicyTopology.js"; +import PickResource from "./PickResource.js"; +import * as dot from "graphlib-dot"; // Needed to parse dotString +import "./App.css"; function App() { - const initialDotString = ` - strict digraph "" { - graph [bb="0,0,440.51,352"]; - node [fillcolor=lightgrey, - label="", - shape=ellipse - ]; - "gateway.gateway.networking.k8s.io:default/prod-web" [fillcolor="#e5e5e5", - height=0.57778, - label="Gateway\ndefault/prod-web", - pos="280.92,253.6", - shape=box, - style=filled, - width=1.5612]; - "gateway.gateway.networking.k8s.io:default/prod-web#http" [fillcolor="#e5e5e5", - height=0.57778, - label="Listener\ndefault/prod-web#http", - pos="369.92,176", - shape=box, - style=filled, - width=1.9609]; - "gateway.gateway.networking.k8s.io:default/prod-web" -> "gateway.gateway.networking.k8s.io:default/prod-web#http" [key="Gateway -> Listener", - pos="e,346.01,196.85 304.77,232.8 315.05,223.85 327.21,213.24 338.23,203.63"]; - "gateway.gateway.networking.k8s.io:default/prod-web#https" [fillcolor="#e5e5e5", - height=0.57778, - label="Listener\ndefault/prod-web#https", - pos="74.922,176", - shape=box, - style=filled, - width=2.0365]; - "gateway.gateway.networking.k8s.io:default/prod-web" -> "gateway.gateway.networking.k8s.io:default/prod-web#https" [key="Gateway -> Listener", - pos="e,129.7,196.64 225.99,232.91 199.33,222.87 167.14,210.74 139.34,200.27"]; - "httproute.gateway.networking.k8s.io:default/my-app" [fillcolor="#e5e5e5", - height=0.57778, - label="HTTPRoute\ndefault/my-app", - pos="223.92,98.4", - shape=box, - style=filled, - width=1.4101]; - "httproute.gateway.networking.k8s.io:default/my-app#rule-1" [fillcolor="#e5e5e5", - height=0.57778, - label="HTTPRouteRule\ndefault/my-app#rule-1", - pos="72.922,20.8", - shape=box, - style=filled, - width=1.9716]; - "httproute.gateway.networking.k8s.io:default/my-app" -> "httproute.gateway.networking.k8s.io:default/my-app#rule-1" [key="HTTPRoute -> HTTPRouteRule", - pos="e,113.24,41.518 183.46,77.605 164.59,67.911 141.98,56.29 122.14,46.092"]; - "httproute.gateway.networking.k8s.io:default/my-app#rule-2" [fillcolor="#e5e5e5", - height=0.57778, - label="HTTPRouteRule\ndefault/my-app#rule-2", - pos="232.92,20.8", - shape=box, - style=filled, - width=1.9716]; - "httproute.gateway.networking.k8s.io:default/my-app" -> "httproute.gateway.networking.k8s.io:default/my-app#rule-2" [key="HTTPRoute -> HTTPRouteRule", - pos="e,230.5,41.653 226.33,77.605 227.25,69.689 228.32,60.489 229.32,51.828"]; - "gateway.gateway.networking.k8s.io:default/prod-web#http" -> "httproute.gateway.networking.k8s.io:default/my-app" [key="Listener -> HTTPRoute", - pos="e,262.9,119.12 330.8,155.2 312.64,145.55 290.89,133.99 271.77,123.83"]; - "gateway.gateway.networking.k8s.io:default/prod-web#https" -> "httproute.gateway.networking.k8s.io:default/my-app" [key="Listener -> HTTPRoute", - pos="e,184.14,119.12 114.85,155.2 133.38,145.55 155.58,133.99 175.09,123.83"]; - "dnspolicy.kuadrant.io:default/geo" [height=0.57778, - label="DNSPolicy\ndefault/geo", - pos="215.92,331.2", - shape=note, - style=dashed, - width=1.108]; - "dnspolicy.kuadrant.io:default/geo" -> "gateway.gateway.networking.k8s.io:default/prod-web" [key="Policy -> Target", - pos="e,263.45,274.45 233.34,310.4 240.55,301.79 249.04,291.66 256.83,282.36", - style=dashed]; - "tlspolicy.kuadrant.io:default/https" [height=0.57778, - label="TLSPolicy\ndefault/https", - pos="74.922,253.6", - shape=note, - style=dashed, - width=1.1943]; - "tlspolicy.kuadrant.io:default/https" -> "gateway.gateway.networking.k8s.io:default/prod-web#https" [key="Policy -> Target", - pos="e,74.922,196.85 74.922,232.8 74.922,224.89 74.922,215.69 74.922,207.03", - style=dashed]; - "authpolicy.kuadrant.io:default/api-key-admins" [height=0.57778, - label="AuthPolicy\ndefault/api-key-admins", - pos="72.922,98.4", - shape=note, - style=dashed, - width=2.0256]; - "authpolicy.kuadrant.io:default/api-key-admins" -> "httproute.gateway.networking.k8s.io:default/my-app#rule-1" [key="Policy -> Target", - pos="e,72.922,41.653 72.922,77.605 72.922,69.689 72.922,60.489 72.922,51.828", - style=dashed]; - "authpolicy.kuadrant.io:default/business-hours" [height=0.57778, - label="AuthPolicy\ndefault/business-hours", - pos="344.92,331.2", - shape=note, - style=dashed, - width=1.9718]; - "authpolicy.kuadrant.io:default/business-hours" -> "gateway.gateway.networking.k8s.io:default/prod-web" [key="Policy -> Target", - pos="e,298.12,274.45 327.77,310.4 320.67,301.79 312.31,291.66 304.64,282.36", - style=dashed]; - "ratelimitpolicy.kuadrant.io:default/my-app-rl" [height=0.57778, - label="RateLimitPolicy\ndefault/my-app-rl", - pos="223.92,176", - shape=note, - style=dashed, - width=1.5936]; - "ratelimitpolicy.kuadrant.io:default/my-app-rl" -> "httproute.gateway.networking.k8s.io:default/my-app" [key="Policy -> Target", - pos="e,223.92,119.25 223.92,155.2 223.92,147.29 223.92,138.09 223.92,129.43", - style=dashed]; - } - `; - + const [dotString, setDotString] = useState(""); const [graph, setGraph] = useState(null); useEffect(() => { - const g = dot.read(initialDotString); - setGraph(g); - }, [initialDotString]); + let ws; + const connectWebSocket = () => { + ws = new WebSocket("ws://localhost:4000/ws"); + + ws.onmessage = (event) => { + try { + const { dotString: updatedDotString } = JSON.parse(event.data); + console.log("WebSocket message received:", updatedDotString); + setDotString(updatedDotString); + + const parsedGraph = dot.read(updatedDotString); + setGraph(parsedGraph); + } catch (error) { + console.error("Error processing WebSocket message:", error); + } + }; + + ws.onclose = () => { + console.warn("WebSocket closed. Attempting to reconnect..."); + setTimeout(connectWebSocket, 3000); // Retry after 3 seconds + }; + + ws.onerror = (error) => console.error("WebSocket error:", error); + }; + + connectWebSocket(); + return () => ws && ws.close(); + }, []); return (
-

Policy Topology Example

-
- {}} /> -
- +

Policy Machinery

+
); diff --git a/src/PickResource.js b/src/PickResource.js index 594d326..074539e 100644 --- a/src/PickResource.js +++ b/src/PickResource.js @@ -1,30 +1,48 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { Dropdown, DropdownToggle, DropdownItem } from '@patternfly/react-core'; +import React, { useState, useEffect, useCallback } from "react"; +import { Dropdown, DropdownToggle, DropdownItem } from "@patternfly/react-core"; const PickResource = ({ graph, onResourceSelect }) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [dropdownItems, setDropdownItems] = useState([]); - const [selectedLabel, setSelectedLabel] = useState('Select a resource'); + const [selectedLabel, setSelectedLabel] = useState("Select a resource"); - const handleSelection = useCallback((nodeId) => { - setSelectedLabel(nodeId ? graph.node(nodeId).label : 'Select a resource'); - onResourceSelect(nodeId); - setIsDropdownOpen(false); - }, [graph, onResourceSelect]); + const handleSelection = useCallback( + (nodeId) => { + if (graph && typeof graph.node === "function") { + setSelectedLabel( + nodeId ? graph.node(nodeId).label : "Select a resource" + ); + } + onResourceSelect(nodeId); + setIsDropdownOpen(false); + }, + [graph, onResourceSelect] + ); useEffect(() => { - if (graph) { + if (graph && typeof graph.nodes === "function") { const items = [ - handleSelection(null)}> + handleSelection(null)} + > - , - ...graph.nodes().map(node => ( - handleSelection(node)}> - {graph.node(node).label} + ...graph.nodes().map((node) => ( + handleSelection(node)} + > + {graph.node(node)?.label || node} )), ]; setDropdownItems(items); + } else { + console.warn("Invalid graph object:", graph); + setDropdownItems([]); } }, [graph, handleSelection]); @@ -35,7 +53,9 @@ const PickResource = ({ graph, onResourceSelect }) => { return ( setIsDropdownOpen(false)} - toggle={{selectedLabel}} + toggle={ + {selectedLabel} + } isOpen={isDropdownOpen} dropdownItems={dropdownItems} /> diff --git a/src/PolicyTopology.js b/src/PolicyTopology.js index f2a7e02..b76ee37 100644 --- a/src/PolicyTopology.js +++ b/src/PolicyTopology.js @@ -1,10 +1,10 @@ -import React, { useEffect, useRef, useState, useCallback } from 'react'; -import * as d3 from 'd3'; -import { graphviz } from 'd3-graphviz'; // eslint-disable-line no-unused-vars -import { Button } from '@patternfly/react-core'; -import graphlib from 'graphlib'; -import * as dot from 'graphlib-dot'; -import './PolicyTopology.css'; +import React, { useEffect, useRef, useState, useCallback } from "react"; +import * as d3 from "d3"; +import { graphviz } from "d3-graphviz"; // eslint-disable-line no-unused-vars +import { Button } from "@patternfly/react-core"; +import graphlib from "graphlib"; +import * as dot from "graphlib-dot"; +import "./PolicyTopology.css"; const PolicyTopology = ({ initialDotString }) => { const containerRef = useRef(null); @@ -12,78 +12,111 @@ const PolicyTopology = ({ initialDotString }) => { const [graph, setGraph] = useState(null); // Function to update graph when a node is selected - const handleNodeSelection = useCallback((nodeId) => { - if (!graph) return; + const handleNodeSelection = useCallback( + (nodeId) => { + if (!graph) return; - const filteredGraph = new graphlib.Graph(); - const nodesToInclude = new Set(); + const filteredGraph = new graphlib.Graph(); + const nodesToInclude = new Set(); - const addPredecessors = (node) => { - if (!nodesToInclude.has(node)) { - nodesToInclude.add(node); - const predecessors = graph.predecessors(node) || []; - predecessors.forEach(addPredecessors); - } - }; + const addPredecessors = (node) => { + if (!nodesToInclude.has(node)) { + nodesToInclude.add(node); + const predecessors = graph.predecessors(node) || []; + predecessors.forEach(addPredecessors); + } + }; - const addSuccessors = (node) => { - const successors = graph.successors(node) || []; - successors.forEach(successor => { - nodesToInclude.add(successor); - }); - }; + const addSuccessors = (node) => { + const successors = graph.successors(node) || []; + successors.forEach((successor) => { + nodesToInclude.add(successor); + }); + }; - addPredecessors(nodeId); - addSuccessors(nodeId); + addPredecessors(nodeId); + addSuccessors(nodeId); - nodesToInclude.forEach(node => { - filteredGraph.setNode(node, graph.node(node)); - }); + nodesToInclude.forEach((node) => { + filteredGraph.setNode(node, graph.node(node)); + }); - graph.edges().forEach(edge => { - if (nodesToInclude.has(edge.v) && nodesToInclude.has(edge.w)) { - filteredGraph.setEdge(edge.v, edge.w, graph.edge(edge.v, edge.w)); - } - }); + graph.edges().forEach((edge) => { + if (nodesToInclude.has(edge.v) && nodesToInclude.has(edge.w)) { + filteredGraph.setEdge(edge.v, edge.w, graph.edge(edge.v, edge.w)); + } + }); + + const filteredDotString = dot.write(filteredGraph); + setDotString(filteredDotString); // Update the dotString state + }, + [graph] + ); - const filteredDotString = dot.write(filteredGraph); - setDotString(filteredDotString); // Update the dotString state - }, [graph]); + // Parse the DOT string into a graph object when dotString or initialDotString changes + useEffect(() => { + if (initialDotString !== dotString) { + setDotString(initialDotString); + } + }, [initialDotString]); - // Parse the DOT string into a graph object when dotString changes useEffect(() => { if (dotString) { - const g = dot.read(dotString); - setGraph(g); + try { + const g = dot.read(dotString); + console.log("Parsed graph in PolicyTopology:", g); + console.log("Graph nodes in PolicyTopology:", g.nodes()); + setGraph(g); + } catch (error) { + console.error("Error parsing DOT string in PolicyTopology:", error); + } + } else { + console.warn("Empty dotString received in PolicyTopology"); } }, [dotString]); // Render the graph with updates using d3-graphviz useEffect(() => { - if (containerRef.current && graph) { - const renderGraph = () => { - d3.select(containerRef.current) + if (containerRef.current && dotString) { + try { + console.log("Rendering dotString with d3-graphviz:", dotString); + + // Calculate dimensions dynamically based on viewport + const viewportWidth = window.innerWidth * 1.2; // Use 95% of the viewport width + const viewportHeight = window.innerHeight * 0.85; // Use 85% of the viewport height + + const graphvizInstance = d3 + .select(containerRef.current) .graphviz() - .height(containerRef.current.clientHeight) // Set SVG height to container height - .fit(true) - .zoom(false) - .transition(() => d3.transition().duration(750)) // Animate transitions + .width(viewportWidth) // Set width to fill most of the viewport + .height(viewportHeight) // Set height to fill most of the viewport + .fit(true) // Ensure it scales proportionally + .zoom(false); // Disable zoom for now + + // Render the graph with animation + graphvizInstance + .transition(() => d3.transition().duration(750)) // Animation duration .renderDot(dotString) - .on('end', () => { - const nodes = containerRef.current.querySelectorAll('g.node'); - nodes.forEach(node => { - node.addEventListener('click', (event) => { - const nodeElement = event.target.closest('g.node'); - const nodeId = nodeElement.querySelector('title').textContent; + .on("end", () => { + console.log("Graph rendered successfully"); + // Add click event listeners to nodes + const nodes = containerRef.current.querySelectorAll("g.node"); + nodes.forEach((node) => { + node.addEventListener("click", (event) => { + const nodeElement = event.target.closest("g.node"); + const nodeId = nodeElement.querySelector("title").textContent; handleNodeSelection(nodeId); }); }); }); - }; - - renderGraph(); + } catch (error) { + console.error("Error rendering dotString with d3-graphviz:", error); + } + } else if (!dotString) { + console.warn("No dotString available for rendering"); } - }, [graph, dotString, handleNodeSelection]); + }, [dotString, handleNodeSelection]); + // Function to reset the graph to its initial state const resetGraph = useCallback(() => { @@ -93,7 +126,11 @@ const PolicyTopology = ({ initialDotString }) => { return (
-
diff --git a/watch-configmap.js b/watch-configmap.js new file mode 100644 index 0000000..7d69414 --- /dev/null +++ b/watch-configmap.js @@ -0,0 +1,58 @@ +import { WebSocketServer } from 'ws'; +import { spawn } from 'child_process'; + +const wss = new WebSocketServer({ port: 4000 }); + +console.log('WebSocket server listening on ws://localhost:4000'); + +wss.on('connection', (ws) => { + console.log('Client connected to WebSocket'); + + const kubectl = spawn('kubectl', [ + 'get', + 'configmap', + 'topology', + '-n', + 'kuadrant-system', + '-o', + 'json', + '--watch', + ]); + + let buffer = ''; + + kubectl.stdout.on('data', (chunk) => { + buffer += chunk.toString(); // Accumulate chunks in the buffer + + try { + // Attempt to parse the buffer + const configMap = JSON.parse(buffer); + const dotString = configMap.data.topology; + + // Send the updated dotString to the client + console.log('Sending dotString to WebSocket:', dotString); + ws.send(JSON.stringify({ dotString })); + + // Clear the buffer after successful parsing + buffer = ''; + } catch (error) { + if (error.name !== 'SyntaxError') { + console.error('Unexpected error:', error); + buffer = ''; // Clear buffer for unexpected errors + } + } + }); + + kubectl.stderr.on('data', (data) => { + console.error('kubectl error:', data.toString()); + }); + + kubectl.on('close', (code) => { + console.log(`kubectl process exited with code ${code}`); + ws.close(); + }); + + ws.on('close', () => { + console.log('WebSocket connection closed'); + }); +});