Skip to content

Commit

Permalink
Feat: Update to Graph display
Browse files Browse the repository at this point in the history
  • Loading branch information
Its4Nik committed Feb 12, 2025
1 parent 479106d commit 6af6ad4
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 142 deletions.
73 changes: 22 additions & 51 deletions app/components/cytoscape-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import cytoscape from "cytoscape";
import { cn } from "~/lib/utils";
import { CytoscapeJsonStructure, nodeData } from "~/lib/typings/cytoscape";
import { Dialog, DialogContent, DialogTrigger } from "~/components/ui/dialog";
import { getCytoscapeConfig } from "~/config/cytoscape";
import { Separator } from "./ui/separator";

interface GraphProps {
data: CytoscapeJsonStructure;
onNodeClick?: (nodeData: nodeData) => void;
onNodeClick: (nodeData: nodeData) => void;
}

const CytoscapeGraph: React.FC<GraphProps> = ({ data, onNodeClick }) => {
Expand All @@ -17,55 +19,14 @@ const CytoscapeGraph: React.FC<GraphProps> = ({ data, onNodeClick }) => {
useEffect(() => {
if (!cyRef.current) return;

const cy = cytoscape({
container: cyRef.current,
elements: data,
style: [
{
selector: "node",
style: {
"background-image": (node: nodeData) => {
const isContainer = node.data.type === "container";

return isContainer === true
? "/assets/_graph-icons/container.svg"
: "/assets/_graph-icons/server.svg";
},
"background-width": "60%",
"background-height": "60%",
label: "data(name)",
"text-valign": "center",
"text-halign": "center",
"font-size": "12px",
shape: "ellipse",
"background-color": "transparent",
"text-outline-width": 0,
},
},
{
selector: "edge",
style: {
width: 2,
"line-color": "#A78BFA",
"target-arrow-color": "#A78BFA",
"target-arrow-shape": "triangle",
"curve-style": "bezier",
},
},
],
layout: {
name: "concentric",
animate: false,
padding: 10,
avoidOverlap: true,
},
});
const config = getCytoscapeConfig(cyRef.current, data);
const cy = cytoscape(config);

cy.on("tap", "node", (event) => {
const nodeData = event.target.data();
setSelectedNode(nodeData);
onNodeClick(nodeData);
setSelectedNode({ data: nodeData });
setIsDialogOpen(true);
onNodeClick?.(nodeData);
});

return () => {
Expand All @@ -77,7 +38,7 @@ const CytoscapeGraph: React.FC<GraphProps> = ({ data, onNodeClick }) => {
<>
<div
className={cn(
"w-full h-[1000px] border border-gray-300 rounded-md shadow-md"
"w-full h-[1000px] border border-gray-300 rounded-md shadow-md bg-muted"
)}
ref={cyRef}
/>
Expand All @@ -89,8 +50,12 @@ const CytoscapeGraph: React.FC<GraphProps> = ({ data, onNodeClick }) => {
</DialogTrigger>
<DialogContent className="p-4">
{selectedNode ? (
<div>
<h3 className="font-bold text-lg">{selectedNode.data.name}</h3>
<div className="p-4 font-normal text-xl">
<h3 className="font-bold text-accent text-4xl mb-4">
{selectedNode?.data?.name || selectedNode?.data?.hostName}
</h3>
<Separator className="my-4" />

<p>Type: {selectedNode.data.type}</p>
{selectedNode.data.hostName && (
<p>Host: {selectedNode.data.hostName}</p>
Expand All @@ -102,10 +67,16 @@ const CytoscapeGraph: React.FC<GraphProps> = ({ data, onNodeClick }) => {
<p>State: {selectedNode.data.state}</p>
)}
{selectedNode.data.cpu_usage !== undefined && (
<p>CPU Usage: {selectedNode.data.cpu_usage}%</p>
<p>CPU Usage: {selectedNode.data.cpu_usage.toFixed(2)}%</p>
)}
{selectedNode.data.mem_usage !== undefined && (
<p>Memory Usage: {selectedNode.data.mem_usage}MB</p>
<p>
Memory Usage:{" "}
{(selectedNode.data.mem_usage / 1024 / 1024 / 1024).toFixed(
2
)}{" "}
GB
</p>
)}
</div>
) : (
Expand Down
66 changes: 0 additions & 66 deletions app/components/node-details-modal.tsx

This file was deleted.

71 changes: 71 additions & 0 deletions app/config/cytoscape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import cytoscape, { CytoscapeOptions } from "cytoscape";
import { CytoscapeJsonStructure } from "~/lib/typings/cytoscape";

export const getCytoscapeConfig = (
container: HTMLElement | null,
data: CytoscapeJsonStructure
): CytoscapeOptions => {
const theme = document.documentElement.getAttribute("data-theme") || "light";
const isDark = theme === "dark";

const nodeBackgroundColor = isDark ? "#555" : "#0074D9";
const nodeTextColor = isDark ? "#EEE" : "#A9A9A9";
const edgeColor = isDark ? "#888" : "#A9A9A9";

return {
container,
elements: data,
layout: {
name: "concentric",
animate: false,
padding: 10,
avoidOverlap: true,
fit: true,
nodeDimensionsIncludeLabels: true,
spacingFactor: 0.6,
minNodeSpacing: 1,
},
style: [
{
selector: "node",
style: {
label: (node: cytoscape.NodeSingular) => {
const type = node.data("type");
return type === "container"
? node.data("name")
: node.data("hostName");
},
"background-image": (node: cytoscape.NodeSingular) => {
const type = node.data("type");
if (type === "container") {
return isDark
? "/assets/_graph-icons/container-dark.svg"
: "/assets/_graph-icons/container-light.svg";
} else {
return isDark
? "/assets/_graph-icons/server-dark.svg"
: "/assets/_graph-icons/server-light.svg";
}
},
"background-color": nodeBackgroundColor,
"background-fit": "contain",
"background-clip": "none",
"background-opacity": 0,
color: nodeTextColor,
"text-valign": "bottom",
"text-halign": "center",
width: "40",
height: "40",
},
},
{
selector: "edge",
style: {
width: 1,
"line-color": edgeColor,
"curve-style": "bezier",
},
},
],
};
};
23 changes: 20 additions & 3 deletions app/lib/typings/cytoscape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ interface nodeData {
data: {
id: string;
label: string;
type: string;
type: "host" | "container";
hostName: string;
containerCount?: number;
parent?: string;
name?: string;
state?: string;
cpu_usage?: number;
Expand All @@ -19,6 +18,24 @@ interface nodeData {
};
}

interface singleNodeData {
id: string;
label: string;
type: "host" | "container";
hostName: string;
containerCount?: number;
name?: string;
state?: string;
cpu_usage?: number;
mem_usage?: number;
mem_limit?: number;
net_rx?: number;
net_tx?: number;
current_net_rx?: number;
current_net_tx?: number;
network_mode?: string;
}

interface edgeData {
data: {
id: string;
Expand All @@ -41,4 +58,4 @@ interface CytoscapeJsonStructure {
edges: edgeData[];
}

export type { CytoscapeJsonStructure, nodeData, nodes, edges };
export type { CytoscapeJsonStructure, nodeData, nodes, edges, singleNodeData };
66 changes: 44 additions & 22 deletions app/routes/views.graph.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,59 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import CytoscapeGraph from "~/components/cytoscape-graph";
import NodeDetailsModal from "~/components/node-details-modal";
import { getCytoscapeJson } from "~/lib/dataFetcher";

const elements = await getCytoscapeJson();
import { singleNodeData } from "~/lib/typings/cytoscape";
import { Loader2 } from "lucide-react";

export default function GraphPage() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedNodeData, setSelectedNodeData] = useState<any>(null);
const [elements, setElements] = useState<unknown>(null);
const [isLoading, setIsLoading] = useState(true);
const [, setIsModalOpen] = useState(false);
const [, setSelectedNodeData] = useState<singleNodeData | null>(null);

useEffect(() => {
const loadData = async () => {
try {
const data = await getCytoscapeJson();
setElements(JSON.parse(data));
} catch (error) {
console.error("Error loading graph data:", error);
} finally {
setIsLoading(false);
}
};

loadData();
}, []);

const handleNodeClick = (nodeData: any) => {
const handleNodeClick = (nodeData: singleNodeData) => {
if (!nodeData?.id) {
console.error("Invalid node data:", nodeData);
return;
}
setSelectedNodeData(nodeData);
setIsModalOpen(true);
};

return (
<>
<div className="p-8 h-full">
<h1 className="text-2xl font-bold mb-4">Cytoscape Graph</h1>
<CytoscapeGraph data={JSON.parse(elements)} />
<NodeDetailsModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
nodeData={selectedNodeData}
/>
<div className="relative h-full p-8">
<div
className={`absolute inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm transition-opacity duration-300 ${
isLoading ? "opacity-100" : "opacity-0 pointer-events-none"
}`}
>
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>

<div>
<h1 className="text-2xl font-bold mb-4">Raw Cytoscape Data</h1>
<pre>
<code>{elements}</code>
</pre>
<div
className={`h-full transition-opacity duration-300 ${
isLoading ? "opacity-0" : "opacity-100"
}`}
>
<h1 className="text-4xl font-bold mb-4">Container Graph</h1>
{elements && (
<CytoscapeGraph data={elements} onNodeClick={handleNodeClick} />
)}
</div>
</>
</div>
);
}
1 change: 1 addition & 0 deletions public/assets/_graph-icons/container-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
1 change: 1 addition & 0 deletions public/assets/_graph-icons/server-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes

0 comments on commit 6af6ad4

Please sign in to comment.