Skip to content
This repository was archived by the owner on Aug 2, 2025. It is now read-only.

Commit 63c396e

Browse files
committed
Change: Graph generation logic changed!
1 parent 915cc33 commit 63c396e

3 files changed

Lines changed: 108 additions & 261 deletions

File tree

src/handlers/graph.ts

Lines changed: 61 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -2,257 +2,84 @@ import cytoscape from "cytoscape";
22
import logger from "../utils/logger";
33
import { AllContainerData, ContainerData } from "./../typings/dockerConfig";
44
import { atomicWrite } from "../utils/atomicWrite";
5-
import { rateLimitedReadFile } from "../utils/rateLimitFS";
65

76
const 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 };

src/routes/graphs/routes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Request, Response, Router } from "express";
22
import { createResponseHandler } from "../../handlers/response";
33
import path from "path";
4+
import { rateLimitedReadFile } from "../../utils/rateLimitFS";
45
const router = Router();
56

67
router.get("/", async (req: Request, res: Response) => {
@@ -28,4 +29,17 @@ router.get("/image", async (req: Request, res: Response) => {
2829
}
2930
});
3031

32+
router.get("/json", async (req: Request, res: Response) => {
33+
const ResponseHandler = createResponseHandler(res);
34+
try {
35+
const data = await rateLimitedReadFile(
36+
path.join(__dirname, "/../../.." + "/src/data/graph.json"),
37+
);
38+
return ResponseHandler.rawData(data, "Graph JSON fetched");
39+
} catch (error: unknown) {
40+
const errorMsg = error instanceof Error ? error.message : String(error);
41+
return ResponseHandler.critical(errorMsg);
42+
}
43+
});
44+
3145
export default router;

0 commit comments

Comments
 (0)