Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
import expandCollapse from 'cytoscape-expand-collapse';
import { Sidebar } from '../sidebar/Sidebar.js';
import { ZoomContext } from '../zoom-context.provider.js';
import {
CalmInterfaceTypeSchema,
CalmHostPortInterfaceSchema,
CalmHostnameInterfaceSchema,
CalmPathInterfaceSchema,
CalmOAuth2AudienceInterfaceSchema,
CalmURLInterfaceSchema,
CalmRateLimitInterfaceSchema,
CalmContainerImageInterfaceSchema,
CalmPortInterfaceSchema,
} from '../../../../../shared/src/types/interface-types.js';
import { CalmControlsSchema } from '../../../../../shared/src/types/control-types.js';

// Initialize Cytoscape plugins
nodeEdgeHtmlLabel(cytoscape);
Expand All @@ -23,7 +35,7 @@
};

// Types for nodes and edges
export type Node = {
export type CalmNode = {
classes?: string;
data: {
description: string;
Expand All @@ -32,7 +44,19 @@
id: string;
_displayPlaceholderWithDesc: string;
_displayPlaceholderWithoutDesc: string;
[idx: string]: string;
parent?: string;
interfaces?: (
| CalmInterfaceTypeSchema
| CalmHostPortInterfaceSchema
| CalmHostnameInterfaceSchema
| CalmPathInterfaceSchema
| CalmOAuth2AudienceInterfaceSchema
| CalmURLInterfaceSchema
| CalmRateLimitInterfaceSchema
| CalmContainerImageInterfaceSchema
| CalmPortInterfaceSchema
)[];
controls?: CalmControlsSchema;
};
};

Expand All @@ -50,7 +74,7 @@
title?: string;
isNodeDescActive: boolean;
isConDescActive: boolean;
nodes: Node[];
nodes: CalmNode[];
edges: Edge[];
}

Expand All @@ -64,13 +88,12 @@
const cyRef = useRef<HTMLDivElement>(null);
const [cy, setCy] = useState<Core | null>(null);
const { zoomLevel, updateZoom } = useContext(ZoomContext);
const [selectedNode, setSelectedNode] = useState<Node['data'] | null>(null);
const [selectedEdge, setSelectedEdge] = useState<Edge['data'] | null>(null);
const [selectedItem, setSelectedItem] = useState<CalmNode['data'] | Edge['data'] | null>(null);

// Generate node label templates
const getNodeLabelTemplateGenerator =
(selected = false) =>
(data: Node['data']) => `
(data: CalmNode['data']) => `
<div class="node element ${selected ? 'selected-node' : ''}">
<p class="title">${data.label}</p>
<p class="type">${data.type}</p>
Expand Down Expand Up @@ -108,12 +131,14 @@
{
selector: 'node',
style: {
label: isNodeDescActive ? 'data(_displayPlaceholderWithDesc)' : 'data(_displayPlaceholderWithoutDesc)',
label: isNodeDescActive
? 'data(_displayPlaceholderWithDesc)'
: 'data(_displayPlaceholderWithoutDesc)',
'text-valign': 'center',
'text-halign': 'center',
'text-wrap': 'wrap',
'text-max-width': '180px',
"font-family": 'Arial',
'font-family': 'Arial',
width: '200px',
height: 'label',
shape: 'rectangle',
Expand All @@ -139,14 +164,12 @@
// Add event listeners
updatedCy.on('tap', 'node', (e) => {
const node = e.target as NodeSingular;
setSelectedEdge(null);
setSelectedNode(node?.data());
setSelectedItem(node?.data());
});

updatedCy.on('tap', 'edge', (e) => {
const edge = e.target as EdgeSingular;
setSelectedNode(null);
setSelectedEdge(edge?.data());
setSelectedItem(edge?.data());
});

updatedCy.on('zoom', () => updateZoom(updatedCy.zoom()));
Expand Down Expand Up @@ -174,7 +197,7 @@
return () => {
updatedCy.destroy(); // Clean up Cytoscape instance
};
}, [nodes, edges, isConDescActive, isNodeDescActive, updateZoom]);

Check warning on line 200 in calm-hub-ui/src/visualizer/components/cytoscape-renderer/CytoscapeRenderer.tsx

View workflow job for this annotation

GitHub Actions / Build, Test, and Lint Shared Module

React Hook useEffect has missing dependencies: 'cy' and 'getNodeLabelTemplateGenerator'. Either include them or remove the dependency array

Check warning on line 200 in calm-hub-ui/src/visualizer/components/cytoscape-renderer/CytoscapeRenderer.tsx

View workflow job for this annotation

GitHub Actions / Build, Test, and Lint Shared Module

React Hook useEffect has missing dependencies: 'cy' and 'getNodeLabelTemplateGenerator'. Either include them or remove the dependency array

// Synchronize zoom level with context
useEffect(() => {
Expand All @@ -192,11 +215,8 @@
</div>
)}
<div ref={cyRef} className="flex-1 bg-white visualizer" style={{ height: '100vh' }} />
{selectedNode && (
<Sidebar selectedData={selectedNode} closeSidebar={() => setSelectedNode(null)} />
)}
{selectedEdge && (
<Sidebar selectedData={selectedEdge} closeSidebar={() => setSelectedEdge(null)} />
{selectedItem && (
<Sidebar selectedData={selectedItem} closeSidebar={() => setSelectedItem(null)} />
)}
</div>
);
Expand Down
91 changes: 49 additions & 42 deletions calm-hub-ui/src/visualizer/components/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,80 @@
import { Sidebar } from '../sidebar/Sidebar.js';
import { useState } from 'react';
import { CytoscapeRenderer, Node, Edge } from '../cytoscape-renderer/CytoscapeRenderer.js';
import { CytoscapeRenderer, CalmNode, Edge } from '../cytoscape-renderer/CytoscapeRenderer.js';
import {
CalmArchitectureSchema,
CalmRelationshipSchema,
} from '../../../../../shared/src/types/core-types.js';
import {
CALMDeployedInRelationship,
CALMComposedOfRelationship,
CALMConnectsRelationship,
CALMDeployedInRelationship,
CALMInteractsRelationship,
CALMRelationship,
CALMArchitecture,
} from '../../../../../shared/src/types.js';

interface DrawerProps {
calmInstance?: CALMArchitecture;
calmInstance?: CalmArchitectureSchema;
title?: string;
isNodeDescActive: boolean;
isConDescActive: boolean;
}

function isComposedOf(relationship: CALMRelationship): relationship is CALMComposedOfRelationship {
function isComposedOf(
relationship: CalmRelationshipSchema
): relationship is CALMComposedOfRelationship {
return 'composed-of' in relationship['relationship-type'];
}

function isDeployedIn(relationship: CALMRelationship): relationship is CALMDeployedInRelationship {
function isDeployedIn(
relationship: CalmRelationshipSchema
): relationship is CALMDeployedInRelationship {
return 'deployed-in' in relationship['relationship-type'];
}

function isInteracts(relationship: CALMRelationship): relationship is CALMInteractsRelationship {
function isInteracts(
relationship: CalmRelationshipSchema
): relationship is CALMInteractsRelationship {
return 'interacts' in relationship['relationship-type'];
}

function isConnects(relationship: CALMRelationship): relationship is CALMConnectsRelationship {
function isConnects(
relationship: CalmRelationshipSchema
): relationship is CALMConnectsRelationship {
return 'connects' in relationship['relationship-type'];
}

function getComposedOfRelationships(calmInstance: CALMArchitecture) {
function getComposedOfRelationships(calmInstance: CalmArchitectureSchema) {
const composedOfRelationships: {
[idx: string]: {
type: 'parent' | 'child';
parent?: string;
};
} = {};

calmInstance.relationships.forEach((relationship) => {
calmInstance.relationships?.forEach((relationship) => {
if (isComposedOf(relationship)) {
const rel = relationship['relationship-type']['composed-of'];
composedOfRelationships[rel['container']] = { type: 'parent' };
rel['nodes'].forEach((node) => {
composedOfRelationships[rel!['container']] = { type: 'parent' };
rel!['nodes'].forEach((node) => {
composedOfRelationships[node] = {
type: 'child',
parent: rel['container'],
parent: rel!['container'],
};
});
}
});

return composedOfRelationships;
}
function getDeployedInRelationships(calmInstance: CALMArchitecture) {

function getDeployedInRelationships(calmInstance: CalmArchitectureSchema) {
const deployedInRelationships: {
[idx: string]: {
type: 'parent' | 'child';
parent?: string;
};
} = {};
calmInstance.relationships.forEach((relationship) => {
calmInstance.relationships?.forEach((relationship) => {
if (isDeployedIn(relationship)) {
const rel = relationship['relationship-type']['deployed-in'];
deployedInRelationships[rel['container']] = { type: 'parent' };
Expand All @@ -80,63 +91,60 @@ function getDeployedInRelationships(calmInstance: CALMArchitecture) {
}

export function Drawer({ calmInstance, title, isConDescActive, isNodeDescActive }: DrawerProps) {
const [selectedNode, setSelectedNode] = useState(null);
const [selectedNode, setSelectedNode] = useState<CalmNode | null>(null);

function closeSidebar() {
setSelectedNode(null);
}

function getNodes(): Node[] {
function getNodes(): CalmNode[] {
if (!calmInstance || !calmInstance.relationships) return [];

const composedOfRelationships = getComposedOfRelationships(calmInstance);
const deployedInRelationships = getDeployedInRelationships(calmInstance);
const nodes = calmInstance.nodes.map((node) => {
const newData: Node = {

return (calmInstance.nodes ?? []).map((node) => {
const newData: CalmNode = {
classes: 'node',
data: {
label: node.name,
description: node.description,
type: node['node-type'],
id: node['unique-id'],
//Used to make the size of the node scale dynamically
_displayPlaceholderWithDesc: `${node.name}\n\n\n${node['node-type']}\n\n\n${node.description}\n`,
_displayPlaceholderWithoutDesc: `${node.name}\n\n\n${node['node-type']}`,
},
};

if (composedOfRelationships[node['unique-id']]?.type === 'parent') {
newData.classes = 'group';
if (node.interfaces) {
newData.data.interfaces = node.interfaces;
}

if (
composedOfRelationships[node['unique-id']]?.type === 'child' &&
composedOfRelationships[node['unique-id']]['parent']
) {
newData.data.parent = composedOfRelationships[node['unique-id']].parent!;
}
const composedOfRel = composedOfRelationships[node['unique-id']];
const deployedInRel = deployedInRelationships[node['unique-id']];

if (deployedInRelationships[node['unique-id']]?.type === 'parent') {
if (composedOfRel?.type === 'parent' || deployedInRel?.type === 'parent') {
newData.classes = 'group';
}

if (
deployedInRelationships[node['unique-id']]?.type === 'child' &&
deployedInRelationships[node['unique-id']]['parent'] &&
!newData.data.parent
) {
newData.data.parent = deployedInRelationships[node['unique-id']].parent!;
const parentId =
composedOfRel?.type === 'child' && composedOfRel.parent
? composedOfRel.parent
: deployedInRel?.type === 'child' && deployedInRel.parent
? deployedInRel.parent
: undefined;

if (parentId) {
newData.data.parent = parentId;
}
return newData;
});

return nodes;
}

function getEdges(): Edge[] {
if (!calmInstance || !calmInstance.relationships) return [];

const edges = calmInstance.relationships
return calmInstance.relationships
.filter((relationship) => !isComposedOf(relationship) && !isDeployedIn(relationship))
.map((relationship) => {
if (isInteracts(relationship)) {
Expand All @@ -162,8 +170,7 @@ export function Drawer({ calmInstance, title, isConDescActive, isNodeDescActive
};
}
})
.filter((edge) => edge !== undefined);
return edges;
.filter((edge): edge is Edge => edge !== undefined);
}

const edges = getEdges();
Expand Down Expand Up @@ -195,7 +202,7 @@ export function Drawer({ calmInstance, title, isConDescActive, isNodeDescActive
)}
</div>
{selectedNode && (
<Sidebar selectedData={selectedNode} closeSidebar={closeSidebar} />
<Sidebar selectedData={selectedNode['data']} closeSidebar={closeSidebar} />
)}
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions calm-hub-ui/src/visualizer/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function Menu({
name="connection-description"
aria-label="connection-description"
checked={isConDescActive}
onClick={toggleConnectionDesc}
onChange={toggleConnectionDesc}
/>
</label>
<label className="label cursor-pointer">
Expand All @@ -67,7 +67,7 @@ export function Menu({
className="toggle"
aria-label="node-description"
checked={isNodeDescActive}
onClick={toggleNodeDesc}
onChange={toggleNodeDesc}
/>
</label>
</>
Expand Down
Loading
Loading