mirror of
https://github.com/pandorafms/pandorafms.git
synced 2025-07-28 16:24:54 +02:00
minor fixes networkmapclass
Former-commit-id: d8eaa2ab328d803647d365a023c3ec08efb086d3
This commit is contained in:
parent
6990951dc7
commit
4947d2117a
@ -55,6 +55,47 @@ define('MAP_Y_CORRECTION', 150);
|
|||||||
* - JS controllers.
|
* - JS controllers.
|
||||||
* - Data translated to JSON format.
|
* - Data translated to JSON format.
|
||||||
* - Interface layer.
|
* - Interface layer.
|
||||||
|
*
|
||||||
|
* Basic parameters for NetworkMap class constructor:
|
||||||
|
*
|
||||||
|
* nodes => is really 'rawNodes' here you put an array like follows:
|
||||||
|
* nodes => [
|
||||||
|
* 'some_id' => [
|
||||||
|
* 'type' => NODE_GENERIC/NODE_PANDORA/NODE_MODULE/NODE_AGENT,
|
||||||
|
* behaviour and fields to be used are different depending
|
||||||
|
* on the type.
|
||||||
|
* 'id_agente' => 'some_id',
|
||||||
|
* 'status' => 'agent status',
|
||||||
|
* 'id_parent' => 'target parent to match in map, its no need to use the
|
||||||
|
* real parent but must match some_id'
|
||||||
|
* 'id_node' => incremental id (0,1,2,3...),
|
||||||
|
* 'image' => relative path to image to use,
|
||||||
|
* 'label' => label to use (in NODE_GENERIC)
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* Tooltipster support: Keep in mind, using tooltipster behaviour
|
||||||
|
* of map changes.
|
||||||
|
*
|
||||||
|
* Sample usage:
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
*
|
||||||
|
* $map_manager = new NetworkMap(
|
||||||
|
* [
|
||||||
|
* 'nodes' => vmware_get_nodes($show_vms, $show_ds, $show_esx),
|
||||||
|
* 'pure' => 1,
|
||||||
|
* 'use_tooltipster' => 1,
|
||||||
|
* 'tooltip_params' => [
|
||||||
|
* 'page' => 'operation/agentes/ver_agente',
|
||||||
|
* 'get_agent_json' => 1,
|
||||||
|
* ],
|
||||||
|
* ]
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* $map_manager->printMap();
|
||||||
|
*
|
||||||
|
* </code>
|
||||||
*/
|
*/
|
||||||
class NetworkMap
|
class NetworkMap
|
||||||
{
|
{
|
||||||
@ -151,12 +192,49 @@ class NetworkMap
|
|||||||
public $relations;
|
public $relations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mode simple or advanced.
|
* Include a Pandora (or vendor) node or not.
|
||||||
* Not being used yet.
|
|
||||||
*
|
*
|
||||||
* @var integer
|
* @var integer
|
||||||
*/
|
*/
|
||||||
public $mode;
|
public $noPandoraNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use tooltipster interface instead of standard.
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
public $useTooltipster;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options used in AJAX call while using tooltipster.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $tooltipParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a custom method to parse Graphviz output and generate Graph.
|
||||||
|
* Function pointer.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $customParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines arguments to be passed to $customParser.
|
||||||
|
* If is not defined, default arguments will be used.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $customParserArgs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If using a custom parser, fallback to default parser while
|
||||||
|
* found exceptions.
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
public $fallbackDefaultParser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of map options. Because how is built, the structure matches
|
* Array of map options. Because how is built, the structure matches
|
||||||
@ -287,6 +365,41 @@ class NetworkMap
|
|||||||
$this->mapOptions['pure'] = $options['pure'];
|
$this->mapOptions['pure'] = $options['pure'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($options['no_pandora_node'])) {
|
||||||
|
$this->noPandoraNode = $options['no_pandora_node'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a custom parser.
|
||||||
|
if (isset($options['custom_parser'])) {
|
||||||
|
$this->customParser = $options['custom_parser'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom parser arguments.
|
||||||
|
if (isset($options['custom_parser_args'])) {
|
||||||
|
if (is_array($options['custom_parser_args'])) {
|
||||||
|
foreach ($options['custom_parser_args'] as $k) {
|
||||||
|
$this->customParserArgs[] = $k;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->customParserArgs = $options['custom_parser_args'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default parser.
|
||||||
|
if (isset($options['fallback_to_default_parser'])) {
|
||||||
|
$this->fallbackDefaultParser = $options['fallback_to_default_parser'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['use_tooltipster'])) {
|
||||||
|
$this->useTooltipster = $options['use_tooltipster'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($options['tooltip_params'])) {
|
||||||
|
foreach ($options['tooltip_params'] as $k => $v) {
|
||||||
|
$this->tooltipParams[$k] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Map options, check default values above.
|
// Map options, check default values above.
|
||||||
// This is only used while generating new maps using
|
// This is only used while generating new maps using
|
||||||
// (generateDotGraph).
|
// (generateDotGraph).
|
||||||
@ -931,7 +1044,12 @@ class NetworkMap
|
|||||||
$nooverlap = $this->mapOptions['nooverlap'];
|
$nooverlap = $this->mapOptions['nooverlap'];
|
||||||
$zoom = $this->mapOptions['zoom'];
|
$zoom = $this->mapOptions['zoom'];
|
||||||
|
|
||||||
if (isset($config['networkmap_max_width'])) {
|
if (isset($this->mapOptions['width'])
|
||||||
|
&& isset($this->mapOptions['height'])
|
||||||
|
) {
|
||||||
|
$size_x = ($this->mapOptions['width'] / 100);
|
||||||
|
$size_y = ($this->mapOptions['height'] / 100);
|
||||||
|
} else if (isset($config['networkmap_max_width'])) {
|
||||||
$size_x = ($config['networkmap_max_width'] / 100);
|
$size_x = ($config['networkmap_max_width'] / 100);
|
||||||
$size_y = ($size_x * 0.8);
|
$size_y = ($size_x * 0.8);
|
||||||
} else {
|
} else {
|
||||||
@ -1106,6 +1224,7 @@ class NetworkMap
|
|||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
if (is_array($relations)) {
|
||||||
foreach ($relations as $index => $rel) {
|
foreach ($relations as $index => $rel) {
|
||||||
/*
|
/*
|
||||||
* AA, AM and MM links management
|
* AA, AM and MM links management
|
||||||
@ -1216,6 +1335,9 @@ class NetworkMap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->graph['relations'] = $cleaned;
|
$this->graph['relations'] = $cleaned;
|
||||||
}
|
}
|
||||||
@ -1506,6 +1628,16 @@ class NetworkMap
|
|||||||
$item['networkmap_id'] = $node['style']['id_networkmap'];
|
$item['networkmap_id'] = $node['style']['id_networkmap'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX: Compatibility with Tooltipster - Simple map controller.
|
||||||
|
if ($this->useTooltipster) {
|
||||||
|
$item['label'] = $item['text'];
|
||||||
|
$item['image'] = $item['image_url'];
|
||||||
|
$item['image_height'] = 52;
|
||||||
|
$item['image_width'] = 52;
|
||||||
|
$item['width'] = $this->mapOptions['map_filter']['node_radius'];
|
||||||
|
$item['height'] = $this->mapOptions['map_filter']['node_radius'];
|
||||||
|
}
|
||||||
|
|
||||||
$return[] = $item;
|
$return[] = $item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1645,6 +1777,12 @@ class NetworkMap
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// XXX: Compatibility with Tooltipster - Simple map controller.
|
||||||
|
if ($this->useTooltipster) {
|
||||||
|
$item['orig'] = $rel['id_parent'];
|
||||||
|
$item['dest'] = $rel['id_child'];
|
||||||
|
}
|
||||||
|
|
||||||
// Set direct values.
|
// Set direct values.
|
||||||
$item['id'] = $i++;
|
$item['id'] = $i++;
|
||||||
|
|
||||||
@ -1721,7 +1859,7 @@ class NetworkMap
|
|||||||
$graph = '';
|
$graph = '';
|
||||||
|
|
||||||
if ($nodes === false) {
|
if ($nodes === false) {
|
||||||
if ($this->rawNodes) {
|
if (isset($this->rawNodes)) {
|
||||||
$nodes = $this->rawNodes;
|
$nodes = $this->rawNodes;
|
||||||
} else {
|
} else {
|
||||||
// Search for nodes.
|
// Search for nodes.
|
||||||
@ -1734,6 +1872,7 @@ class NetworkMap
|
|||||||
// Open Graph.
|
// Open Graph.
|
||||||
$graph = $this->openDotFile();
|
$graph = $this->openDotFile();
|
||||||
|
|
||||||
|
if (!$this->noPandoraNode) {
|
||||||
// Create empty pandora node to link orphans.
|
// Create empty pandora node to link orphans.
|
||||||
$this->nodes[0] = [
|
$this->nodes[0] = [
|
||||||
'label' => get_product_name(),
|
'label' => get_product_name(),
|
||||||
@ -1748,9 +1887,12 @@ class NetworkMap
|
|||||||
$graph .= $this->createDotNode(
|
$graph .= $this->createDotNode(
|
||||||
$this->nodes[0]
|
$this->nodes[0]
|
||||||
);
|
);
|
||||||
|
$i = 1;
|
||||||
|
} else {
|
||||||
|
$i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Create dot nodes.
|
// Create dot nodes.
|
||||||
$i = 1;
|
|
||||||
$orphans = [];
|
$orphans = [];
|
||||||
foreach ($nodes as $k => $node) {
|
foreach ($nodes as $k => $node) {
|
||||||
if (isset($node['type']) && $node['type'] == NODE_AGENTE
|
if (isset($node['type']) && $node['type'] == NODE_AGENTE
|
||||||
@ -2027,9 +2169,41 @@ class NetworkMap
|
|||||||
|
|
||||||
unlink($filename_dot);
|
unlink($filename_dot);
|
||||||
|
|
||||||
|
if (function_exists($this->customParser)) {
|
||||||
|
try {
|
||||||
|
if (empty($this->customParserArgs)) {
|
||||||
|
$graph = call_user_func(
|
||||||
|
$this->customParser,
|
||||||
|
$filename_plain,
|
||||||
|
$this->dotGraph
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$graph = call_user_func(
|
||||||
|
$this->customParser,
|
||||||
|
$filename_plain,
|
||||||
|
$this->dotGraph,
|
||||||
|
$this->customParserArgs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If developer is using a custom method to parse graphviz
|
||||||
|
// results, but want to handle using default parser
|
||||||
|
// or custom based on data, it is possible to launch
|
||||||
|
// exceptions to control internal flow.
|
||||||
|
if ($this->fallbackDefaultParser === true) {
|
||||||
$graph = $this->parseGraphvizMapFile(
|
$graph = $this->parseGraphvizMapFile(
|
||||||
$filename_plain
|
$filename_plain
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
ui_print_error_message($e->getMessage());
|
||||||
|
$graph = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$graph = $this->parseGraphvizMapFile(
|
||||||
|
$filename_plain
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
unlink($filename_plain);
|
unlink($filename_plain);
|
||||||
|
|
||||||
@ -2302,6 +2476,13 @@ class NetworkMap
|
|||||||
} else {
|
} else {
|
||||||
$this->map['center_x'] = $node_center['x'];
|
$this->map['center_x'] = $node_center['x'];
|
||||||
$this->map['center_y'] = $node_center['y'];
|
$this->map['center_y'] = $node_center['y'];
|
||||||
|
|
||||||
|
if (!isset($this->map['center_x'])
|
||||||
|
&& !isset($this->map['center_y'])
|
||||||
|
) {
|
||||||
|
$this->map['center_x'] = ($nodes[0]['x'] - MAP_X_CORRECTION);
|
||||||
|
$this->map['center_y'] = ($nodes[0]['y'] - MAP_Y_CORRECTION);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->graph = $graph;
|
$this->graph = $graph;
|
||||||
@ -2365,7 +2546,7 @@ class NetworkMap
|
|||||||
$output .= 'var y_offs ='.$networkmap['filter']['y_offs'].";\n";
|
$output .= 'var y_offs ='.$networkmap['filter']['y_offs'].";\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($networkmap['filter']['y_offs'])) {
|
if (empty($networkmap['filter']['z_dash'])) {
|
||||||
$output .= "var z_dash =null;\n";
|
$output .= "var z_dash =null;\n";
|
||||||
} else {
|
} else {
|
||||||
$output .= 'var z_dash = '.$networkmap['filter']['z_dash'].";\n";
|
$output .= 'var z_dash = '.$networkmap['filter']['z_dash'].";\n";
|
||||||
@ -2926,6 +3107,24 @@ class NetworkMap
|
|||||||
{
|
{
|
||||||
$output = '';
|
$output = '';
|
||||||
|
|
||||||
|
if (enterprise_installed()
|
||||||
|
&& $this->useTooltipster
|
||||||
|
) {
|
||||||
|
$output .= '<script type="text/javascript">
|
||||||
|
var nodes = networkmap.nodes;
|
||||||
|
var arrows = networkmap.links;
|
||||||
|
var width = networkmap_dimensions[0];
|
||||||
|
var height = networkmap_dimensions[1];
|
||||||
|
var font_size = 14;
|
||||||
|
var custom_params = '.json_encode($this->tooltipParams).';
|
||||||
|
var controller = null;
|
||||||
|
var homedir = "'.ui_get_full_url(false).'"
|
||||||
|
$(function() {
|
||||||
|
controller = new SimpleMapController("#simple_map");
|
||||||
|
controller.init_map();
|
||||||
|
});
|
||||||
|
</script>';
|
||||||
|
} else {
|
||||||
// Generate JS for advanced controller.
|
// Generate JS for advanced controller.
|
||||||
$output .= '
|
$output .= '
|
||||||
|
|
||||||
@ -2960,6 +3159,7 @@ class NetworkMap
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
</script>';
|
</script>';
|
||||||
|
}
|
||||||
|
|
||||||
if ($return === false) {
|
if ($return === false) {
|
||||||
echo $output;
|
echo $output;
|
||||||
@ -2979,6 +3179,35 @@ class NetworkMap
|
|||||||
{
|
{
|
||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
|
if (enterprise_installed()
|
||||||
|
&& isset($this->useTooltipster)
|
||||||
|
&& $this->useTooltipster == true
|
||||||
|
) {
|
||||||
|
$output .= '<script type="text/javascript" src="'.ui_get_full_url(
|
||||||
|
'include/javascript/d3.3.5.14.js'
|
||||||
|
).'" charset="utf-8"></script>';
|
||||||
|
$output .= '<script type="text/javascript" src="'.ui_get_full_url(
|
||||||
|
'enterprise/include/javascript/SimpleMapController.js'
|
||||||
|
).'"></script>';
|
||||||
|
$output .= '<script type="text/javascript" src="'.ui_get_full_url(
|
||||||
|
'enterprise/include/javascript/tooltipster.bundle.min.js'
|
||||||
|
).'"></script>';
|
||||||
|
$output .= '<script type="text/javascript" src="'.ui_get_full_url(
|
||||||
|
'include/javascript/jquery.svg.js'
|
||||||
|
).'"></script>';
|
||||||
|
$output .= '<script type="text/javascript" src="'.ui_get_full_url(
|
||||||
|
'include/javascript/jquery.svgdom.js'
|
||||||
|
).'"></script>';
|
||||||
|
$output .= '<link rel="stylesheet" type="text/css" href="'.ui_get_full_url(
|
||||||
|
'/enterprise/include/styles/tooltipster.bundle.min.css'
|
||||||
|
).'" />'."\n";
|
||||||
|
|
||||||
|
$output .= '<div id="simple_map" data-id="'.$this->idMap.'" style="border: 1px #dddddd solid;">';
|
||||||
|
$output .= '<svg id="svg'.$this->idMap.'" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" pointer-events="all" width="'.$this->mapOptions['width'].'" height="'.$this->mapOptions['height'].'px">';
|
||||||
|
$output .= '</svg>';
|
||||||
|
$output .= '</div>';
|
||||||
|
} else {
|
||||||
|
// Load default interface.
|
||||||
ui_require_css_file('networkmap');
|
ui_require_css_file('networkmap');
|
||||||
ui_require_css_file('jquery.contextMenu', 'include/styles/js/');
|
ui_require_css_file('jquery.contextMenu', 'include/styles/js/');
|
||||||
|
|
||||||
@ -2996,18 +3225,21 @@ class NetworkMap
|
|||||||
$networkmap['filter']['l2_network_interfaces'] = 1;
|
$networkmap['filter']['l2_network_interfaces'] = 1;
|
||||||
|
|
||||||
$output .= '<script type="text/javascript" src="'.$config['homeurl'].'include/javascript/d3.3.5.14.js" charset="utf-8"></script>';
|
$output .= '<script type="text/javascript" src="'.$config['homeurl'].'include/javascript/d3.3.5.14.js" charset="utf-8"></script>';
|
||||||
|
if (isset($this->map['__simulated']) === false) {
|
||||||
|
// Load context menu if manageable networkmap.
|
||||||
$output .= '<script type="text/javascript" src="'.$config['homeurl'].'include/javascript/jquery.contextMenu.js"></script>';
|
$output .= '<script type="text/javascript" src="'.$config['homeurl'].'include/javascript/jquery.contextMenu.js"></script>';
|
||||||
|
}
|
||||||
|
|
||||||
$output .= '<script type="text/javascript" src="'.$config['homeurl'].'include/javascript/functions_pandora_networkmap.js"></script>';
|
$output .= '<script type="text/javascript" src="'.$config['homeurl'].'include/javascript/functions_pandora_networkmap.js"></script>';
|
||||||
|
|
||||||
// Open networkconsole_id div.
|
// Open networkconsole_id div.
|
||||||
$output .= '<div id="networkconsole_'.$networkmap['id'].'"';
|
$output .= '<div id="networkconsole_'.$networkmap['id'].'"';
|
||||||
$output .= ' style="position: relative; overflow: hidden; background: #FAFAFA">';
|
$output .= ' style="width: '.$this->mapOptions['width'].'; height: '.$this->mapOptions['height'].';position: relative; overflow: hidden; background: #FAFAFA">';
|
||||||
|
|
||||||
$output .= '<div style="display: '.$minimap_display.';">';
|
$output .= '<div style="display: '.$minimap_display.';">';
|
||||||
$output .= '<canvas id="minimap_'.$networkmap['id'].'"';
|
$output .= '<canvas id="minimap_'.$networkmap['id'].'"';
|
||||||
$output .= ' style="position: absolute; left: 0px; top: 0px; border: 1px solid #bbbbbb;">';
|
$output .= ' style="position: absolute; left: 0px; top: 0px; border: 1px solid #bbbbbb;">';
|
||||||
$output .= '</canvas>';
|
$output .= '</canvas>';
|
||||||
|
|
||||||
$output .= '<div id="arrow_minimap_'.$networkmap['id'].'"';
|
$output .= '<div id="arrow_minimap_'.$networkmap['id'].'"';
|
||||||
$output .= ' style="position: absolute; left: 0px; top: 0px;">';
|
$output .= ' style="position: absolute; left: 0px; top: 0px;">';
|
||||||
$output .= '<a title="'.__('Open Minimap').'" href="javascript: toggle_minimap();">';
|
$output .= '<a title="'.__('Open Minimap').'" href="javascript: toggle_minimap();">';
|
||||||
@ -3028,6 +3260,7 @@ class NetworkMap
|
|||||||
|
|
||||||
// Close networkconsole_id div.
|
// Close networkconsole_id div.
|
||||||
$output .= "</div>\n";
|
$output .= "</div>\n";
|
||||||
|
}
|
||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
@ -3125,6 +3358,7 @@ class NetworkMap
|
|||||||
onchange({type: document[hidden] ? "blur" : "focus"});
|
onchange({type: document[hidden] ? "blur" : "focus"});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
';
|
';
|
||||||
if ($return === false) {
|
if ($return === false) {
|
||||||
echo $output;
|
echo $output;
|
||||||
|
@ -2178,6 +2178,7 @@ function show_menu(item, data) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof $.contextMenu == "function") {
|
||||||
$.contextMenu("destroy");
|
$.contextMenu("destroy");
|
||||||
$.contextMenu({
|
$.contextMenu({
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@ -2185,15 +2186,18 @@ function show_menu(item, data) {
|
|||||||
// define the elements of the menu
|
// define the elements of the menu
|
||||||
items: items_list
|
items: items_list
|
||||||
});
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Force to show in the mouse position
|
//Force to show in the mouse position
|
||||||
|
if (typeof $("#networkconsole_" + networkmap_id).contextMenu == "function") {
|
||||||
$("#networkconsole_" + networkmap_id).contextMenu({
|
$("#networkconsole_" + networkmap_id).contextMenu({
|
||||||
x: mouse[0],
|
x: mouse[0],
|
||||||
y: mouse[1]
|
y: mouse[1]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function add_interface_link(data_parent) {
|
function add_interface_link(data_parent) {
|
||||||
var selection = d3.selectAll(".node_children");
|
var selection = d3.selectAll(".node_children");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user