@@ -2,257 +2,84 @@ import cytoscape from "cytoscape";
22import logger from "../utils/logger" ;
33import { AllContainerData , ContainerData } from "./../typings/dockerConfig" ;
44import { atomicWrite } from "../utils/atomicWrite" ;
5- import { rateLimitedReadFile } from "../utils/rateLimitFS" ;
65
76const CACHE_DIR_JSON = "./src/data/graph.json" ;
8- const CACHE_DIR_HTML = "./src/data/graph.html" ;
9- const _assets = "./src/utils/assets" ;
10- const serverSvg = `${ _assets } /server-icon.svg` ;
11- const containerSvg = `${ _assets } /container-icon.svg` ;
12- const pngPath = "./src/data/graph.png" ;
137
14- async function getPathData ( path : string ) {
15- try {
16- return await rateLimitedReadFile ( path ) ;
17-
18- } catch ( error : unknown ) {
19- const errorMsg = error instanceof Error ? error . message : String ( error ) ;
20- logger . error ( errorMsg ) ;
21- return false ;
22- }
23- }
24-
25- async function renderGraphToImage (
26- htmlContent : string ,
27- outputImagePath : string ,
28- ) : Promise < void > {
29- let puppeteer ;
30- try {
31- puppeteer = await import ( "puppeteer" ) ;
32- } catch ( error ) {
33- logger . error ( "Puppeteer is not installed. Please install it to generate images." ) ;
34- throw new Error ( `Puppeteer is not installed (${ error } )` ) ;
35- }
36-
37- let browser ;
38- try {
39- browser = await puppeteer . default . launch ( {
40- headless : "shell" ,
41- args : [ "--disable-setuid-sandbox" , "--no-sandbox" ] ,
42- executablePath : process . env . PUPPETEER_EXECUTABLE_PATH ,
43- } ) ;
44-
45- const page = await browser . newPage ( ) ;
46- await page . setContent ( htmlContent , { waitUntil : "networkidle0" } ) ;
47- await page . waitForSelector ( "#cy" , { visible : true , timeout : 15000 } ) ;
48-
49- await page . waitForFunction (
50- ( ) => {
51- const cyElement = document . querySelector ( "#cy" ) ;
52- return cyElement ? cyElement . children . length > 0 : false ;
53- } ,
54- { timeout : 10000 }
55- ) ;
56-
57- await page . screenshot ( {
58- path : outputImagePath ,
59- type : outputImagePath . endsWith ( ".jpg" ) ? "jpeg" : "png" ,
60- fullPage : true ,
61- captureBeyondViewport : true ,
62- } ) ;
63- } catch ( error : unknown ) {
64- let errorMessage = "Unknown error occurred during browser operation" ;
65-
66- if ( error instanceof Error ) {
67- errorMessage = error . message ;
68-
69- // Detect common dependency errors
70- if ( errorMessage . includes ( "libnss3" ) || errorMessage . includes ( "libxcb" ) ) {
71- errorMessage = `❗ Missing system dependencies (libnss3)` ;
72- }
73-
74- // Detect Chrome not found errors
75- if ( errorMessage . includes ( "Failed to launch" ) ) {
76- errorMessage = `❗ Chrome not found!` ;
77- }
78- }
79-
80- throw new Error ( `Graph rendering failed - ${ errorMessage } ` ) ;
81- } finally {
82- if ( browser ) {
83- await browser . close ( ) . catch ( ( ) => { } ) ;
84- }
85- }
86-
87- logger . info ( `Graph rendered and image saved to - ${ outputImagePath } ` ) ;
88- }
89-
90- async function generateGraphFiles (
8+ async function generateGraphJSON (
919 allContainerData : AllContainerData ,
9210) : Promise < boolean > {
93- if ( process . env . CI === "true" ) {
94- logger . warn ( "Running inside a CI/CD Action, wont generated graphs" ) ;
95- return false ;
96- } else {
97- try {
98- logger . info ( "generateGraphFiles >>> Starting generation" ) ;
99- const graphElements : cytoscape . ElementDefinition [ ] = [ ] ;
100-
101- for ( const [ hostName , containers ] of Object . entries ( allContainerData ) ) {
102- if ( "error" in containers ) {
103- // TODO: make error'ed hosts better
104- // Issue URL: https://github.com/Its4Nik/DockStatAPI/issues/32
105- graphElements . push ( {
11+ try {
12+ logger . info ( "generateGraphJSON >>> Starting generation" ) ;
13+
14+ // Define the new JSON structure
15+ const graphData = {
16+ nodes : [ ] as cytoscape . ElementDefinition [ ] ,
17+ edges : [ ] as cytoscape . ElementDefinition [ ] ,
18+ } ;
19+
20+ for ( const [ hostName , containers ] of Object . entries ( allContainerData ) ) {
21+ if ( "error" in containers ) {
22+ graphData . nodes . push ( {
23+ data : {
24+ id : hostName ,
25+ label : `Host: ${ hostName } Error: ${ containers . error } ` ,
26+ type : "server" ,
27+ error : true ,
28+ } ,
29+ } ) ;
30+ } else {
31+ const containerList = containers as ContainerData [ ] ;
32+
33+ // Host node with container count and metadata
34+ graphData . nodes . push ( {
35+ data : {
36+ id : hostName ,
37+ label : `${ hostName } \n${ containerList . length } Containers` ,
38+ type : "server" ,
39+ hostName,
40+ containerCount : containerList . length ,
41+ } ,
42+ } ) ;
43+
44+ for ( const container of containerList ) {
45+ const { id, ...otherContainerProps } = container ;
46+
47+ graphData . nodes . push ( {
10648 data : {
107- id : hostName ,
108- label : `Host: ${ hostName } Error: ${ containers . error } ` ,
109- type : "server" ,
49+ id : id ,
50+ label : `${ container . name } \n${ container . state . toUpperCase ( ) } ` ,
51+ type : "container" ,
52+ parent : hostName ,
53+ ...otherContainerProps ,
11054 } ,
11155 } ) ;
112- } else {
113- const containerList = containers as ContainerData [ ] ;
11456
115- // host node with container count
116- graphElements . push ( {
57+ // Edge between host and container
58+ graphData . edges . push ( {
11759 data : {
118- id : hostName ,
119- label : `${ hostName } - ${ containerList . length } Containers` ,
120- type : "server" ,
60+ id : `${ hostName } -${ container . id } ` ,
61+ source : hostName ,
62+ target : container . id ,
63+ connectionType : "host-container" ,
12164 } ,
12265 } ) ;
123-
124- for ( const container of containerList ) {
125- // container node
126- graphElements . push ( {
127- data : {
128- id : container . id ,
129- label : `${ container . name } (${ container . state } )` ,
130- type : "container" ,
131- } ,
132- } ) ;
133-
134- // edge between host and container
135- graphElements . push ( {
136- data : {
137- source : hostName ,
138- target : container . id ,
139- } ,
140- } ) ;
141- }
14266 }
14367 }
144-
145- atomicWrite ( CACHE_DIR_JSON , JSON . stringify ( graphElements , null , 2 ) ) ;
146-
147- const htmlContent = `
148- <!DOCTYPE html>
149- <html lang="en">
150- <head>
151- <meta charset="UTF-8">
152- <meta name="viewport" content="width=device-width, initial-scale=1.0">
153- <title>Cytoscape Graph</title>
154- <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.24.0/cytoscape.min.js"></script>
155- <style>
156- #cy {
157- width: 100%;
158- height: 100vh;
159- display: block;
160- }
161- </style>
162- </head>
163- <body>
164- <div id="cy"></div>
165- <script>
166- const graphElements = ${ JSON . stringify ( graphElements ) } ;
167-
168- const options = {
169- container: document.getElementById("cy"),
170- elements: graphElements,
171- autoungrabify: false,
172- };
173- const cy = cytoscape(options);
174-
175- cy.style()
176- .selector("node")
177- .style({
178- label: "data(label)",
179- "background-color": "#0074D9",
180- "text-valign": "bottom", // Vertical alignment to the center
181- "text-halign": "center", // Horizontal alignment to the center
182- "color": "#000000",
183- "text-margin-y": 10,
184- "background-image": (ele) => {
185- let iconData = "";
186- switch (ele.data("type")) {
187- case "server":
188- iconData = encodeURIComponent(\`${ await getPathData ( serverSvg ) } \`);
189- break;
190- default:
191- iconData = encodeURIComponent(\`${ await getPathData ( containerSvg ) } \`);
192- break;
193- }
194- return \`url("data:image/svg+xml,\${iconData}")\`; // Return the SVG as a data URL
195- },
196- "background-fit": "contain",
197- "background-clip": "none",
198- "background-opacity": 0,
199- width: 70, // Adjust the width for more room
200- height: 70, // Adjust the height for more room
201- "font-size": 15, // Adjust font size to avoid cutting off text
202- })
203- .update();
204-
205- cy.style()
206- .selector("edge")
207- .style({
208- width: 1,
209- "line-color": "#A9A9A9",
210- "curve-style": "bezier",
211- })
212- .update();
213-
214- cy.layout({
215- name: "concentric",
216- evelWidth: function (nodes) {
217- return 2;
218- },
219- fit: true,
220- padding: 40,
221- avoidOverlap: true,
222- nodeDimensionsIncludeLabels: true,
223- animate: false,
224- spacingFactor: 0.8,
225- }).run();
226-
227- cy.on("tap", "node", (event) => {
228- const node = event.target;
229- const nodeId = node.id();
230- const nodeLabel = node.data("label");
231- alert(\`Clicked on: \${nodeLabel}\`);
232- });
233-
234- </script>
235- </body>
236- </html>
237- ` ;
238-
239- atomicWrite ( CACHE_DIR_HTML , htmlContent ) ;
240- await renderGraphToImage ( htmlContent , pngPath )
241- . then ( ( ) => logger . debug ( "HTML converted to image successfully!" ) )
242- . catch ( ( err ) => logger . error ( "Error:" , err ) ) ;
243-
244- logger . info ( "generateGraphFiles <<< Files generated successfully" ) ;
245- return true ;
246- } catch ( error : unknown ) {
247- const errorMsg = error instanceof Error ? error . message : String ( error ) ;
248- logger . error ( errorMsg ) ;
249- return false ;
25068 }
69+
70+ // Write the new structured JSON to file
71+ atomicWrite ( CACHE_DIR_JSON , JSON . stringify ( graphData , null , 2 ) ) ;
72+ logger . info ( "generateGraphJSON <<< JSON file generated successfully" ) ;
73+ return true ;
74+ } catch ( error : unknown ) {
75+ const errorMsg = error instanceof Error ? error . message : String ( error ) ;
76+ logger . error ( errorMsg ) ;
77+ return false ;
25178 }
25279}
25380
254- function getGraphFilePaths ( ) {
255- return { json : CACHE_DIR_JSON , html : CACHE_DIR_HTML } ;
81+ function getGraphFilePath ( ) {
82+ return { json : CACHE_DIR_JSON } ;
25683}
25784
258- export { generateGraphFiles , getGraphFilePaths } ;
85+ export { generateGraphJSON , getGraphFilePath } ;
0 commit comments