in source/frontend/src/components/Graph/Cytoscape.js [38:865]
export default () => {
const [{ graphResources, graphFilters }, dispatch] = useGraphState();
const [{ costPreferences }, costDispatch] = useCostsState();
const [selectedNodes, setSelectedNodes] = React.useState([]);
const compound = useRef();
const [error, setError] = useState(false);
const api = useRef();
const removeNodes = (id) => {
const nodeToDelete = compound.current.filter(function (element, i) {
return element.isNode() && element.data('id') === id;
});
const neighborhood = nodeToDelete.neighborhood();
compound.current.remove(nodeToDelete);
neighborhood.forEach((node) => {
if (node.neighborhood().size() === 0 && !node.data('selected')) {
compound.current.remove(node);
}
});
const elements = compound.current.json().elements;
if (elements) cleanUpGraph();
compound.current.remove(nodeToDelete);
return (
elements &&
elements.nodes &&
elements.nodes.concat(elements.edges).filter((item) => item !== undefined)
);
};
const isResource = R.equals('resource');
const isVPC = R.equals('vpc');
const isSubnet = R.equals('subnet');
const isAvailabiltyZone = R.equals('availabiltyZone');
const isSpecialType = R.anyPass([
isResource,
isVPC,
isSubnet,
isAvailabiltyZone,
]);
React.useEffect(() => {
dispatch({
type: 'updateCompound',
cy: compound.current,
});
}, [compound.current]);
const cleanUpGraph = () => {
var nodes = compound.current
.nodes()
.sort((a, b) => b.data('level') - a.data('level'));
nodes.forEach((node) => {
if (
!api.current.isExpandable(node) &&
node.isChildless() &&
!isSpecialType(node.data('type'))
) {
compound.current.remove(node);
} else {
node.lock();
}
});
};
const expandNode = async (nodes) => {
api.current && api.current.expandAll();
compound.current.nodes().map(function (ele) {
ele.removeClass('clicked');
});
compound.current.nodes().lock();
// compound.current.nodes('.selectable').removeListener('click');
compound.current.nodes('.hoverover').removeListener('mouseover');
compound.current.nodes().removeClass('selected');
sendGetRequests(
R.map(
(e) =>
wrapGetLinkedNodesHierachyRequest(
getLinkedNodesHierarchy,
{
id: e,
},
e,
costPreferences,
graphResources
)
.then((node) =>
handleSelectedResource(
processHierarchicalNodeData(
R.pathOr(
[],
['body', 'data', 'getLinkedNodesHierarchy'],
node
),
e,
costPreferences
),
e,
graphResources
)
)
.catch((err) => {
setError(err);
}),
R.is(Array, nodes)
? R.map((e) => e.data('id'), nodes)
: [nodes.data('clickedId')]
)
)
.then((e) => Promise.all(e))
.then(R.flatten)
.then((items) =>
R.concat(
filterEdges(R.filter((e) => e.edge, items)),
filterNodes(R.filter((e) => !e.edge, items))
)
)
.then((nodes) => {
dispatch({
type: 'updateGraphResources',
graphResources: nodes,
});
});
};
const uniqId = (a, b) => R.equals(a.data.id, b.data.id);
const filterNodes = (nodes) => {
return R.uniqWith(uniqId, nodes);
};
const filterEdges = (edges) => {
edges.forEach((edge, index) => {
if (edge.data.isSourceParent) {
edge.data.sourceMetadata.childIds.forEach((childId) => {
const edgeExists = findEdge(edges, childId, edge.data.target);
if (edgeExists >= 0) {
edges.splice(index, 1);
}
});
}
});
edges.forEach((edge, index) => {
const swappedEdge = findEdge(edges, edge.data.target, edge.data.source);
if (swappedEdge >= 0) {
edges.splice(index, 1);
}
});
return R.filter(
(e) =>
!R.or(
R.includes(e.data.sourceMetadata.type, graphFilters.typeFilters),
R.includes(e.data.targetMetadata.type, graphFilters.typeFilters)
),
edges
);
};
const findEdge = (edges, source, target) => {
return edges.findIndex(
(e) => e.data.source === source && e.data.target === target
);
};
const getContextMenu = () => {
return {
menuItems: [
{
id: 'showAll',
content: 'Show All',
tooltipText: 'Expands this box to show all the nodes within it',
selector: 'node.cy-expand-collapse-collapsed-node',
onClickFunction: function (event) {
var target = event.target || event.cyTarget;
if (target) {
compound.current.nodes().map(function (ele) {
ele.lock();
ele.removeClass('clicked');
ele.removeClass('highlight');
});
api.current.expandRecursively(target);
compound.current.nodes().map(function (ele) {
ele.unlock();
});
}
},
hasTrailingDivider: false,
},
{
id: 'focus',
content: 'Focus',
tooltipText: `Remove everything and bring in just this resource's relationships`,
selector: '.selectable',
onClickFunction: function (event) {
var target = event.target || event.cyTarget;
if (target.data('clickedId')) {
dispatch({
type: 'clearGraph',
});
sendGetRequests(
R.map(
(e) =>
wrapGetLinkedNodesHierachyRequest(
getLinkedNodesHierarchy,
{
id: e,
},
e,
costPreferences,
[]
)
.then((node) =>
handleSelectedResource(
processHierarchicalNodeData(
R.pathOr(
[],
['body', 'data', 'getLinkedNodesHierarchy'],
node
),
e,
costPreferences
),
e,
[]
)
)
.catch((err) => {
console.error(err);
setError(err);
}),
[target.data('clickedId')]
)
)
.then((e) => Promise.all(e))
.then(R.flatten)
.then((nodes) => {
dispatch({
type: 'updateGraphResources',
graphResources: nodes,
});
});
}
},
hasTrailingDivider: false,
},
{
id: 'currentNode',
content: 'Expand',
tooltipText: 'View relationships to this resources',
selector: '.selectable',
onClickFunction: function (event) {
compound.current.nodes().lock();
var target = event.target || event.cyTarget;
if (target.data('clickedId')) {
expandNode(target);
}
},
hasTrailingDivider: false,
show:
compound.current &&
compound.current.$(':selected').filter(function (element) {
return (
element.isNode() && R.equals(element.data('type'), 'resource')
);
}).length === 0,
},
{
id: 'expand',
content: 'Expand',
tooltipText: 'Fetch related resources',
selector: '.selectable',
hasTrailingDivider: true,
show:
compound.current &&
compound.current.$(':selected').filter(function (element) {
return (
element.isNode() && R.equals(element.data('type'), 'resource')
);
}).length > 0,
submenu: [
{
id: 'expandSelected',
content: 'Selected resources',
tooltipText: 'View relationships to the selected resources',
selector: '.selectable',
onClickFunction: () => {
expandNode(
compound.current.$(':selected').filter(function (element) {
return (
element.isNode() &&
R.equals(element.data('type'), 'resource')
);
})
);
},
hasTrailingDivider: true,
},
{
id: 'currentNode',
content: 'This resource',
tooltipText: 'View relationships to this resource',
selector: '.selectable',
onClickFunction: function (event) {
compound.current.nodes().lock();
var target = event.target || event.cyTarget;
if (target.data('clickedId')) {
expandNode(target);
}
},
hasTrailingDivider: false,
},
],
},
{
id: 'remove',
content: selectedNodes.length > 0 ? 'Remove Selected' : 'Remove',
tooltipText: selectedNodes.length > 0 ? 'Remove Selected' : 'Remove',
selector: '.selectable',
onClickFunction: function (event) {
var target = event.target || event.cyTarget;
if (target.data('clickedId')) {
dispatch({
type: 'updateGraphResources',
graphResources: removeNodes(target.data('clickedId')),
});
}
},
hasTrailingDivider: false,
},
{
id: 'collapse',
content: 'Collapse All',
tooltipText:
'Collapses everything in this box to hide all the nodes within it',
selector: 'node[type != "resource"] node:parent',
onClickFunction: function (event) {
var target = event.target || event.cyTarget;
if (target) {
compound.current.nodes().map(function (ele) {
ele.lock();
ele.removeClass('clicked');
ele.removeClass('highlight');
});
api.current.collapseRecursively(target);
compound.current.nodes().map(function (ele) {
ele.unlock();
});
}
},
hasTrailingDivider: false,
},
{
id: 'removeAll',
content: 'Remove All',
selector: '.removeAll',
onClickFunction: function (event) {
var target = event.target || event.cyTarget;
if (target) {
compound.current.remove(target.children());
cleanUpGraph();
compound.current.remove(target);
let resources = [];
const elements = compound.current.json().elements;
if (Object.keys(elements).length > 0) {
resources = elements.edges
? elements.nodes.concat(elements.edges)
: elements.nodes;
}
dispatch({
type: 'updateGraphResources',
graphResources: resources,
});
}
},
hasTrailingDivider: false,
},
{
id: 'details',
content: 'Show resource details',
tooltipText: 'View more details on this resource',
selector: '.selectable',
onClickFunction: function (event) {
var target = event.target || event.cyTarget;
if (target.data('clickedId')) {
const div = document.createElement('div');
ReactDOM.render(
<DetailsDialog selectedNode={event.target} />,
div
);
div.className = 'clickedNode';
document.body.appendChild(div);
}
},
hasTrailingDivider: false,
},
{
id: 'view',
content: 'Diagram',
coreAsWell: true,
onClickFunction: function (event) {
compound.current.edges().addClass('hidden');
},
hasTrailingDivider: true,
show: !R.isEmpty(graphResources),
selector: '.removeAll',
submenu: [
{
id: 'redraw',
content: 'Group resources',
coreAsWell: true,
onClickFunction: function () {
compound.current.nodes().layout(layout).run();
compound.current.edges().addClass('hidden');
},
hasTrailingDivider: true,
},
{
id: 'edges',
content: 'Edges',
coreAsWell: true,
onClickFunction: function (event) {
compound.current.edges().addClass('hidden');
},
show:
!R.isEmpty(graphResources) &&
!R.isEmpty(R.filter((e) => e.edge, graphResources)),
hasTrailingDivider: true,
submenu: [
{
id: 'showEdges',
content: 'Show',
coreAsWell: true,
onClickFunction: function (event) {
compound.current.edges().removeClass('hidden');
},
hasTrailingDivider: true,
},
{
id: 'hideEdges',
content: 'Hide',
coreAsWell: true,
onClickFunction: function (event) {
compound.current.edges().addClass('hidden');
},
hasTrailingDivider: true,
},
],
},
{
id: 'fit',
content: 'Fit',
coreAsWell: true,
onClickFunction: function () {
compound.current.fit(50);
compound.current.center();
},
hasTrailingDivider: true,
},
{
id: 'clear',
content: 'Clear',
selector: '.removeAll',
coreAsWell: true,
onClickFunction: function () {
dispatch({
type: 'clearGraph',
});
},
hasTrailingDivider: true,
},
],
},
{
id: 'costs',
content: 'Costs & usage',
coreAsWell: true,
hasTrailingDivider: true,
show: !R.isEmpty(graphResources) && costPreferences.processCosts,
submenu: [
{
id: 'costs',
content: 'Cost report',
selector: '.removeAll',
coreAsWell: true,
onClickFunction: function (event) {
const div = document.createElement('div');
ReactDOM.render(
<CostOverview
resources={graphResources}
costDispatch={costDispatch}
costPreferences={costPreferences}
/>,
div
);
div.className = 'clickedNode';
document.body.appendChild(div);
},
hasTrailingDivider: true,
show: !R.isEmpty(graphResources) && costPreferences.processCosts,
},
],
},
],
};
};
const setCompoundState = (cy) => {
cy.minZoom(0.25);
cy.maxZoom(2.5);
cy.gridGuide({
drawGrid: true,
snapToAlignmentLocationOnRelease: false,
parentSpacing: -1,
geometricGuideline: false,
parentPadding: true,
gridStackOrder: -1,
guidelinesStackOrder: -1,
resize: true,
snapToGridDuringDrag: false,
distributionGuidelines: false,
snapToGridCenter: false,
initPosAlignment: true,
lineWidth: 2.0,
});
cy.removeListener('cxttapstart');
cy.contextMenus(getContextMenu());
cy.removeListener('tap');
cy.selectionType('additive');
cy.filter(function (element) {
return element.isNode() && R.equals(element.data('type'), 'resource');
}).on('select', (event) => {
event.target.addClass('selected');
cy.contextMenus(getContextMenu());
compound.current = cy;
});
cy.filter(function (element) {
return element.isNode() && R.equals(element.data('type'), 'resource');
}).on('unselect', (event) => {
event.target.removeClass('selected');
cy.contextMenus(getContextMenu());
compound.current = cy;
});
cy.nodes('.hoverover').removeListener('mouseover');
cy.nodes('.hoverover').on('mouseover', function (event) {
let node = event.target;
let popper = node.popper({
content: () => {
const removeElements = (elms) => elms.forEach((el) => el.remove());
removeElements(document.querySelectorAll('.hoverOver'));
const div = document.createElement('div');
ReactDOM.render(<HoverDetails selectedNode={node} />, div);
div.className = 'hoverOver';
document.body.appendChild(div);
return div;
},
renderedPosition: (event) => {
return {
x:
event.renderedPosition('x') < window.innerWidth / 2
? document.getElementById('sidepanel-true')
? window.innerWidth - 280
: window.innerWidth
: document.getElementById('sidepanel-true')
? 180
: 0,
y: -5,
};
},
});
let destroy = () => {
const removeElements = (elms) => elms.forEach((el) => el.remove());
removeElements(document.querySelectorAll('.hoverOver'));
popper.destroy();
};
node.on('mouseout', destroy);
});
compound.current = cy;
};
const layout = {
name: 'fcose',
// 'draft', 'default' or 'proof'
// - "draft" only applies spectral layout
// - "default" improves the quality with incremental layout (fast cooling rate)
// - "proof" improves the quality with incremental layout (slow cooling rate)
quality: 'proof',
// Use random node positions at beginning of layout
// if this is set to false, then quality option must be "proof"
randomize: false,
// Whether or not to animate the layout
animate: true,
// Duration of animation in ms, if enabled
animationDuration: 500,
// Easing of animation, if enabled
animationEasing: undefined,
// Fit the viewport to the repositioned nodes
fit: true,
// Padding around layout
padding: 30,
// Whether to include labels in node dimensions. Valid in "proof" quality
nodeDimensionsIncludeLabels: true,
// Whether or not simple nodes (non-compound nodes) are of uniform dimensions
uniformNodeDimensions: true,
// Whether to pack disconnected components - valid only if randomize: true
packComponents: true,
// Layout step - all, transformed, enforced, cose - for debug purpose only
step: 'all',
/* spectral layout options */
// False for random, true for greedy sampling
samplingType: true,
// Sample size to construct distance matrix
sampleSize: 25,
// Separation amount between nodes
nodeSeparation: 200,
// Power iteration tolerance
piTol: 0.0000001,
/* incremental layout options */
// Node repulsion (non overlapping) multiplier
nodeRepulsion: (node) => 4500,
// Ideal edge (non nested) length
idealEdgeLength: (edge) => 50,
// Divisor to compute edge forces
edgeElasticity: (edge) => 0.45,
// Nesting factor (multiplier) to compute ideal edge length for nested edges
nestingFactor: 0.1,
// Maximum number of iterations to perform
numIter: 2500,
// For enabling tiling
tile: true,
// Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function)
tilingPaddingVertical: 10,
// Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function)
tilingPaddingHorizontal: 10,
// Gravity force (constant)
gravity: 0.25,
// Gravity range (constant) for compounds
gravityRangeCompound: 1.5,
// Gravity force (constant) for compounds
gravityCompound: 1.0,
// Gravity range (constant)
gravityRange: 3.8,
// Initial cooling factor for incremental layout
initialEnergyOnIncremental: 0.3,
/* constraint options */
// Fix desired nodes to predefined positions
// [{nodeId: 'n1', position: {x: 100, y: 200}}, {...}]
fixedNodeConstraint: undefined,
// Align desired nodes in vertical/horizontal direction
// {vertical: [['n1', 'n2'], [...]], horizontal: [['n2', 'n4'], [...]]}
alignmentConstraint: undefined,
// Place two nodes relatively in vertical/horizontal direction
// [{top: 'n1', bottom: 'n2', gap: 100}, {left: 'n3', right: 'n4', gap: 75}, {...}]
relativePlacementConstraint: undefined,
/* layout event callbacks */
ready: () => {
if (compound.current && !compound.current.expandCollapse('get')) {
api.current = compound.current.expandCollapse({
layoutBy: {
name: 'fcose',
// 'draft', 'default' or 'proof'
// - "draft" only applies spectral layout
// - "default" improves the quality with incremental layout (fast cooling rate)
// - "proof" improves the quality with incremental layout (slow cooling rate)
quality: 'proof',
// Use random node positions at beginning of layout
// if this is set to false, then quality option must be "proof"
randomize: false,
// Whether or not to animate the layout
animate: true,
// Duration of animation in ms, if enabled
animationDuration: 1500,
// Easing of animation, if enabled
animationEasing: undefined,
// Fit the viewport to the repositioned nodes
fit: true,
// Padding around layout
padding: 30,
// Whether to include labels in node dimensions. Valid in "proof" quality
nodeDimensionsIncludeLabels: true,
// Whether or not simple nodes (non-compound nodes) are of uniform dimensions
uniformNodeDimensions: true,
// Whether to pack disconnected components - valid only if randomize: true
packComponents: true,
// Layout step - all, transformed, enforced, cose - for debug purpose only
step: 'all',
/* spectral layout options */
// False for random, true for greedy sampling
samplingType: true,
// Sample size to construct distance matrix
sampleSize: 25,
// Separation amount between nodes
nodeSeparation: 200,
// Power iteration tolerance
piTol: 0.0000001,
/* incremental layout options */
// Node repulsion (non overlapping) multiplier
nodeRepulsion: (node) => 4500,
// Ideal edge (non nested) length
idealEdgeLength: (edge) => 50,
// Divisor to compute edge forces
edgeElasticity: (edge) => 0.45,
// Nesting factor (multiplier) to compute ideal edge length for nested edges
nestingFactor: 0.1,
// Maximum number of iterations to perform
numIter: 2500,
// For enabling tiling
tile: true,
// Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function)
tilingPaddingVertical: 10,
// Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function)
tilingPaddingHorizontal: 10,
// Gravity force (constant)
gravity: 0.25,
// Gravity range (constant) for compounds
gravityRangeCompound: 1.5,
// Gravity force (constant) for compounds
gravityCompound: 1.0,
// Gravity range (constant)
gravityRange: 3.8,
// Initial cooling factor for incremental layout
initialEnergyOnIncremental: 0.3,
/* constraint options */
// Fix desired nodes to predefined positions
// [{nodeId: 'n1', position: {x: 100, y: 200}}, {...}]
fixedNodeConstraint: undefined,
// Align desired nodes in vertical/horizontal direction
// {vertical: [['n1', 'n2'], [...]], horizontal: [['n2', 'n4'], [...]]}
alignmentConstraint: undefined,
// Place two nodes relatively in vertical/horizontal direction
// [{top: 'n1', bottom: 'n2', gap: 100}, {left: 'n3', right: 'n4', gap: 75}, {...}]
relativePlacementConstraint: undefined,
},
cueEnabled: false,
fisheye: false,
animate: true,
undoable: false,
expandCollapseCuePosition: 'top-left', // default cue position is top left you can specify a function per node too
expandCollapseCueSize: 12, // size of expand-collapse cue
expandCollapseCueLineSize: 8, // size of lines used for drawing plus-minus icons
});
}
}, // on layoutready
stop: () => {
graphResources.map((resource) => {
if (
!resource.edge &&
!resource.data.selected &&
resource.data.properties &&
graphFilters.typeFilters.indexOf(
resource.data.properties.resourceType
) >= 0
) {
removeNodes(resource.data.id);
}
const nodePosition = compound.current.filter(function (element, i) {
return element.isNode() && element.data('id') === resource.data.id;
});
resource.position = nodePosition.position();
});
compound.current && compound.current.center();
compound.current && compound.current.nodes().unlock();
const removeHighlight = setTimeout(() => {
compound.current &&
compound.current.nodes().map(function (ele) {
ele.removeClass('highlight');
});
compound.current &&
compound.current.edges().map(function (ele) {
ele.removeClass('highlight');
});
}, 5000);
return () => clearTimeout(removeHighlight);
}, // on layoutstop
};
const calculateCost = (resources) => {
return aggregateCostData(R.filter((resource) => !resource.edge, resources));
};
costPreferences.processCosts && calculateCost(graphResources);
compound.current && compound.current.nodes().lock();
return (
<div style={{ height: 'calc(100% - 64px)', width: '100%' }}>
<CytoscapeComponent
elements={graphResources}
layout={layout}
boxSelectionEnabled
stylesheet={graphStyle}
style={{
maxWidth: '100vw',
maxHeight: '100%',
width: '100vw',
height: '100%',
}}
cy={(cy) => {
setCompoundState(cy);
}}
/>
</div>
);
};