mirror of https://github.com/MISP/MISP
chg: [clusterRelations] Improved UI of relation_graph and
relation_viewerpull/6120/head
parent
1af22c1258
commit
2a6c6f439d
|
@ -831,9 +831,24 @@ class GalaxyClustersController extends AppController
|
|||
throw new NotFoundException('Invalid galaxy cluster');
|
||||
}
|
||||
$cluster = $cluster[0];
|
||||
$relations = $this->GalaxyCluster->GalaxyClusterRelation->getExistingRelationships();
|
||||
$relations = Hash::extract($relations, '{n}.ObjectRelationship.name');
|
||||
$existingRelations = $this->GalaxyCluster->GalaxyClusterRelation->getExistingRelationships();
|
||||
$cluster = $this->GalaxyCluster->attachClusterToRelations($this->Auth->user(), $cluster);
|
||||
$tree = array(array(
|
||||
'GalaxyCluster' => $cluster['GalaxyCluster'],
|
||||
'children' => array()
|
||||
));
|
||||
// add relation info between the two clusters
|
||||
foreach($cluster['GalaxyClusterRelation'] as $relation) {
|
||||
$tmp = array(
|
||||
'Relation' => array_diff_key($relation, array_flip(array('GalaxyCluster'))),
|
||||
'children' => array(
|
||||
array('GalaxyCluster' => $relation['GalaxyCluster']),
|
||||
)
|
||||
);
|
||||
$tree[0]['children'][] = $tmp;
|
||||
}
|
||||
$this->set('existingRelations', $existingRelations);
|
||||
$this->set('cluster', $cluster);
|
||||
$this->set('relations', $relations);
|
||||
$this->set('tree', $tree);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -613,7 +613,9 @@ class Galaxy extends AppModel
|
|||
// $cluster['GalaxyCluster']['name'] = $cluster['GalaxyCluster']['uuid'];
|
||||
// $referencedCluster['GalaxyCluster']['name'] = $referencedCluster['GalaxyCluster']['uuid'];
|
||||
$nodes[$referencedClusterId] = $referencedCluster['GalaxyCluster'];
|
||||
$nodes[$referencedClusterId]['group'] = $referencedCluster['GalaxyCluster']['type'];
|
||||
$nodes[$relation['galaxy_cluster_id']] = $cluster['GalaxyCluster'];
|
||||
$nodes[$relation['galaxy_cluster_id']]['group'] = $cluster['GalaxyCluster']['type'];
|
||||
if (true) {
|
||||
$links[] = array(
|
||||
'source' => $relation['galaxy_cluster_id'],
|
||||
|
|
|
@ -448,12 +448,12 @@ class GalaxyCluster extends AppModel
|
|||
if ($cluster['GalaxyCluster']['distribution'] != 4) {
|
||||
unset($clusters[$i]['SharingGroup']);
|
||||
}
|
||||
if ($cluster['GalaxyCluster']['org_id'] == 0) {
|
||||
unset($clusters[$i]['Org']);
|
||||
}
|
||||
if ($cluster['GalaxyCluster']['orgc_id'] == 0) {
|
||||
unset($clusters[$i]['Orgc']);
|
||||
}
|
||||
// if ($cluster['GalaxyCluster']['org_id'] == 0) {
|
||||
// unset($clusters[$i]['Org']);
|
||||
// }
|
||||
// if ($cluster['GalaxyCluster']['orgc_id'] == 0) {
|
||||
// unset($clusters[$i]['Orgc']);
|
||||
// }
|
||||
$clusters[$i] = $this->GalaxyClusterRelation->massageRelationTag($clusters[$i]);
|
||||
}
|
||||
return $clusters;
|
||||
|
@ -576,4 +576,18 @@ class GalaxyCluster extends AppModel
|
|||
}
|
||||
return array_values($clusterTags);
|
||||
}
|
||||
|
||||
public function attachClusterToRelations($user, $cluster)
|
||||
{
|
||||
if (isset($cluster['GalaxyClusterRelation'])) {
|
||||
foreach ($cluster['GalaxyClusterRelation'] as $k => $relation) {
|
||||
$conditions = array('conditions' => array('GalaxyCluster.id' => $relation['referenced_galaxy_cluster_id']));
|
||||
$relatedCluster = $this->fetchGalaxyClusters($user, $conditions, false);
|
||||
if (!empty($relatedCluster)) {
|
||||
$cluster['GalaxyClusterRelation'][$k]['GalaxyCluster'] = $relatedCluster[0]['GalaxyCluster'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $cluster;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,9 +33,10 @@ class GalaxyClusterRelation extends AppModel
|
|||
|
||||
public function getExistingRelationships()
|
||||
{
|
||||
$existingRelationships = $this->find('all', array(
|
||||
$existingRelationships = $this->find('list', array(
|
||||
'recursive' => -1,
|
||||
'fields' => array('referenced_galaxy_cluster_type')
|
||||
'fields' => array('referenced_galaxy_cluster_type'),
|
||||
'group' => array('referenced_galaxy_cluster_type')
|
||||
), false, false);
|
||||
return $existingRelationships;
|
||||
}
|
||||
|
|
|
@ -1,32 +1,47 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/assetLoader', array(
|
||||
'js' => array('d3')
|
||||
));
|
||||
?>
|
||||
<div style="display: flex; min-height: 600px;">
|
||||
<div style="flex: 1; padding: 5px;">
|
||||
|
||||
<div>
|
||||
<div style="padding: 5px; background-color: #f6f6f6; border-bottom: 1px solid #ccc; ">
|
||||
<div id="relationsQuickAddForm">
|
||||
<label for="RelationshipSource"><?= __('Source UUID') ?></label>
|
||||
<input id="RelationshipSource" type="text" value="<?= h($cluster['GalaxyCluster']['uuid']) ?>" disabled></input>
|
||||
<label for="RelationshipType"><?= __('Relationship type') ?></label>
|
||||
<select id="RelationshipType">
|
||||
<?php foreach ($relations as $relation): ?>
|
||||
<option value="<?= h($relation) ?>"><?= h($relation) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label for="RelationshipTarget"><?= __('Target UUID') ?></label>
|
||||
<select></select>
|
||||
<input id="RelationshipTarget" type="text"></input>
|
||||
<button id="buttonAddRelationship" type="button" class="btn btn-block btn-primary">
|
||||
<div class="input">
|
||||
<label for="RelationshipSource"><?= __('Source UUID') ?></label>
|
||||
<input id="RelationshipSource" type="text" value="<?= h($cluster['GalaxyCluster']['uuid']) ?>" disabled></input>
|
||||
</div>
|
||||
<div class="input">
|
||||
<label for="RelationshipType"><?= __('Relationship type') ?></label>
|
||||
<select id="RelationshipType">
|
||||
<?php foreach ($existingRelations as $relation): ?>
|
||||
<option value="<?= h($relation) ?>"><?= h($relation) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input">
|
||||
<label for="RelationshipTarget"><?= __('Target UUID') ?></label>
|
||||
<select></select>
|
||||
<input id="RelationshipTarget" type="text"></input>
|
||||
</div>
|
||||
<button id="buttonAddRelationship" type="button" class="btn btn-primary" style="margin-top: 20px">
|
||||
<i class="fas fa-plus"></i>
|
||||
Add relationship
|
||||
</button>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="flex: 4; padding: 5px; background-color: steelblue;">
|
||||
<?php debug($relations); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 5px; min-height: 600px;">
|
||||
<svg id="treeSVG" style="width: 100%; height: 100%; min-height: 500px;"></svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var treeData = <?= json_encode($tree) ?>;
|
||||
var margin = {top: 10, right: 10, bottom: 10, left: 20};
|
||||
var treeWidth, treeHeight;
|
||||
var colors = d3.scale.category10();
|
||||
$(document).ready(function() {
|
||||
// $('#relationsQuickAddForm select').chosen();
|
||||
})
|
||||
|
@ -60,4 +75,186 @@
|
|||
function toggleLoadingButton(loading) {
|
||||
|
||||
}
|
||||
|
||||
function buildTree() {
|
||||
var $tree = $('#treeSVG');
|
||||
treeWidth = $tree.width() - margin.right - margin.left;
|
||||
treeHeight = $tree.height() - margin.top - margin.bottom;
|
||||
|
||||
var tree = d3.layout.tree(treeData)
|
||||
.size([treeHeight, treeWidth]);
|
||||
|
||||
var diagonal = function link(d) {
|
||||
return "M" + d.source.y + "," + d.source.x
|
||||
+ "C" + (d.source.y + d.target.y) / 2 + "," + d.source.x
|
||||
+ " " + (d.source.y + d.target.y) / 2 + "," + d.target.x
|
||||
+ " " + d.target.y + "," + d.target.x;
|
||||
};
|
||||
|
||||
var svg = d3.select("#treeSVG")
|
||||
.attr("width", treeWidth + margin.right + margin.left)
|
||||
.attr("height", treeHeight + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
var root = treeData[0];
|
||||
root.isRoot = true;
|
||||
root.x0 = treeHeight / 2;
|
||||
root.y0 = 0;
|
||||
var nodes = tree.nodes(root).reverse();
|
||||
var links = tree.links(nodes);
|
||||
var maxDepth = 0;
|
||||
var leftMaxTextLength = 0;
|
||||
nodes.forEach(function(d) {
|
||||
maxDepth = maxDepth > d.depth ? maxDepth : d.depth;
|
||||
if (d.GalaxyCluster !== undefined) {
|
||||
var clusterLength = d.GalaxyCluster.type.length > d.GalaxyCluster.value.length ? d.GalaxyCluster.type.length : d.GalaxyCluster.value.length;
|
||||
leftMaxTextLength = leftMaxTextLength > clusterLength ? leftMaxTextLength : clusterLength;
|
||||
} else if (d.Relation !== undefined) {
|
||||
var tagLength = 0;
|
||||
if (d.Relation.Tag !== undefined) {
|
||||
tagLength = d.Relation.Tag.name / 2;
|
||||
}
|
||||
var relationLength = tagLength > d.Relation.referenced_galaxy_cluster_type.length ? tagLength : d.Relation.referenced_galaxy_cluster_type.length;
|
||||
leftMaxTextLength = leftMaxTextLength > relationLength ? leftMaxTextLength : relationLength;
|
||||
}
|
||||
})
|
||||
var offsetLeafLength = leftMaxTextLength * 6.7; // font-size of body is 12px
|
||||
var ratioFactor = (treeWidth - offsetLeafLength) / maxDepth;
|
||||
nodes.forEach(function(d) { d.y = d.depth * ratioFactor; });
|
||||
|
||||
var node = svg.selectAll("g.node")
|
||||
.data(nodes, function(d) { return getId(d, true) });
|
||||
|
||||
var nodeEnter = node.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
|
||||
.on("mouseover", nodeHover)
|
||||
.on("dblclick", nodeDbclick);
|
||||
|
||||
var gEnter = nodeEnter.append('g');
|
||||
drawEntities(gEnter);
|
||||
|
||||
var link = svg.selectAll("path.link")
|
||||
.data(links, function(d) { return getId(d.target); });
|
||||
|
||||
link.enter().insert("path", "g")
|
||||
.attr("class", "link")
|
||||
.style("fill", "none")
|
||||
.style("stroke", "#ccc")
|
||||
.style("stroke-width", "2px")
|
||||
.attr("d", function(d) {
|
||||
return diagonal(d);
|
||||
});
|
||||
}
|
||||
|
||||
function drawEntities(gEnter) {
|
||||
gEnter.filter(function(d) { return d.GalaxyCluster !== undefined }).call(drawCluster);
|
||||
gEnter.filter(function(d) { return d.Relation !== undefined }).call(drawRelation);
|
||||
}
|
||||
|
||||
function drawCluster(gEnter) {
|
||||
gEnter.append("circle")
|
||||
.attr("r", 5)
|
||||
.style("fill", function(d) { return colors(d.GalaxyCluster.type); })
|
||||
.style("stroke", "#000")
|
||||
.style("stroke-width", "2px");
|
||||
|
||||
drawLabel(gEnter, {
|
||||
text: [function(d) { return d.GalaxyCluster.value }, function(d) { return d.GalaxyCluster.type }],
|
||||
x: function(d) { return d.children ? "0em" : "1.5em"; },
|
||||
y: function(d) { return d.children ? "2em" : ""; },
|
||||
textAnchor: 'start',
|
||||
fontWeight: 'bold'
|
||||
});
|
||||
}
|
||||
|
||||
function drawRelation(gEnter) {
|
||||
var paddingX = 9;
|
||||
gEnter.append("foreignObject")
|
||||
.attr("height", 50)
|
||||
.attr("y", -24)
|
||||
.attr("width", function(d) { return getTextWidth(d.Relation.referenced_galaxy_cluster_type) + 2*paddingX + 'px'; })
|
||||
.append("xhtml:div")
|
||||
.append("div")
|
||||
.attr("class", "well well-small")
|
||||
// .attr("title", function(d) { return d.children ? "Version" : "<?= __('Latest version of the parent cluster') ?>" })
|
||||
.html(function(d) { return d.Relation.referenced_galaxy_cluster_type; })
|
||||
|
||||
paddingX = 6;
|
||||
gEnter.append("foreignObject")
|
||||
.attr("height", 18)
|
||||
.attr("y", 20)
|
||||
.attr("x", function(d) { return -(d.Relation.Tag !== undefined ? getTextWidth(d.Relation.Tag.name, {'white-space': 'nowrap', 'font-weight': 'bold'}) - 2*paddingX : 0)/2 + 'px'; })
|
||||
.attr("width", function(d) { return (d.Relation.Tag !== undefined ? getTextWidth(d.Relation.Tag.name, {'white-space': 'nowrap', 'font-weight': 'bold'}) + 2*paddingX : 0) + 'px'; })
|
||||
.append("xhtml:div")
|
||||
.append("span")
|
||||
.attr("class", "tag")
|
||||
.style('white-space', 'nowrap')
|
||||
.style('background-color', function(d) {return d.Relation.Tag !== undefined ? d.Relation.Tag.colour : '';})
|
||||
.style('color', function(d) {return d.Relation.Tag !== undefined ? getTextColour(d.Relation.Tag.colour) : 'white';})
|
||||
// .attr("title", function(d) { return d.children ? "Version" : "<?= __('Latest version of the parent cluster') ?>" })
|
||||
.html(function(d) { return d.Relation.Tag !== undefined ? d.Relation.Tag.name : ''; })
|
||||
}
|
||||
|
||||
function drawLabel(gEnter, options) {
|
||||
var defaultOptions = {
|
||||
text: '',
|
||||
x: '',
|
||||
dx: '',
|
||||
y: '',
|
||||
dy: '',
|
||||
textAnchor: 'start',
|
||||
fontWeight: ''
|
||||
}
|
||||
options = $.extend(defaultOptions, options);
|
||||
var svgText = gEnter.append("text")
|
||||
.attr("dy", options.dy)
|
||||
.attr("dx", options.dx)
|
||||
.attr("x", options.x)
|
||||
.attr("y", options.y)
|
||||
.attr("text-anchor", options.textAnchor)
|
||||
if (Array.isArray(options.text)) {
|
||||
options.text.forEach(function(text, i) {
|
||||
svgText.append('tspan')
|
||||
.attr('font-weight', i == 0 ? 'bold' : '')
|
||||
.attr('font-style', i != 0 ? 'italic' : '')
|
||||
.attr('x', options.x)
|
||||
.attr('dy', i != 0 ? 16 : 0)
|
||||
.text(text);
|
||||
})
|
||||
} else {
|
||||
svgText
|
||||
.attr("font-weight", options.fontWeight)
|
||||
.text(options.text);
|
||||
}
|
||||
}
|
||||
|
||||
function getTextWidth(text, additionalStyle) {
|
||||
var style = {visibility: 'hidden'};
|
||||
if (additionalStyle !== undefined) {
|
||||
style = $.extend(style, additionalStyle);
|
||||
}
|
||||
var tmp = $('<span></span>').text(text).css(style)
|
||||
$('body').append(tmp);
|
||||
var bcr = tmp[0].getBoundingClientRect()
|
||||
tmp.remove();
|
||||
return bcr.width;
|
||||
}
|
||||
|
||||
function nodeDbclick(d) {
|
||||
}
|
||||
|
||||
function nodeHover(d) {
|
||||
}
|
||||
|
||||
function getId(d) {
|
||||
var id = "";
|
||||
if (d.GalaxyCluster !== undefined) {
|
||||
id = d.GalaxyCluster.id;
|
||||
} else if (d.Relation !== undefined) {
|
||||
id = d.Relation.id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
</script>
|
|
@ -420,7 +420,7 @@ function init<?= $seed ?>() { // variables and functions have their own scope (n
|
|||
.attr("fill", "none")
|
||||
.attr("stroke-width", 2.5);
|
||||
series
|
||||
.style("stroke", function(d) { ;return colors(d.name); })
|
||||
.style("stroke", function(d) { return colors(d.name); })
|
||||
.attr("d", function(d) { return value_line(d.values); });
|
||||
series.exit().remove();
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ echo $this->element('genericElements/assetLoader', array(
|
|||
<?= __('There are no relations in this Galaxy'); ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="border: 1px solid #ddd; margin-bottom: 15px;">
|
||||
<div id="graphContainer" style="height: 70vh;"></div>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<div id="graphContainer" style="height: 70vh; border: 1px solid #ddd; "></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
@ -23,10 +23,12 @@ var graph = <?= json_encode($relations) ?>;
|
|||
var nodes, links;
|
||||
var width, height, margin;
|
||||
var vis, svg, plotting_area, force, container, zoom;
|
||||
var legendLabels, labels;
|
||||
var graphElementScale = 1;
|
||||
var graphElementTranslate = [0, 0];
|
||||
var nodeHeight = 20;
|
||||
var nodeWidth = 120;
|
||||
var colors = d3.scale.category10();
|
||||
|
||||
$(document).ready( function() {
|
||||
margin = {top: 5, right: 5, bottom: 5, left: 5},
|
||||
|
@ -39,23 +41,22 @@ $(document).ready( function() {
|
|||
|
||||
function initGraph() {
|
||||
var correctLink = [];
|
||||
var groupDomain = {};
|
||||
graph.links.forEach(function(link) {
|
||||
var tmpNode = graph.nodes.filter(function(node) {
|
||||
return node.id == link.source;
|
||||
})
|
||||
link.source = tmpNode[0]
|
||||
if (tmpNode[0] === undefined) {
|
||||
console.log(link);
|
||||
}
|
||||
tmpNode = graph.nodes.filter(function(node) {
|
||||
return node.id == link.target;
|
||||
})
|
||||
if (tmpNode[0] === undefined) {
|
||||
console.log(link);
|
||||
}
|
||||
link.target = tmpNode[0]
|
||||
groupDomain[link.source.group] = 1;
|
||||
groupDomain[link.target.group] = 1;
|
||||
correctLink.push(link)
|
||||
})
|
||||
groupDomain = Object.keys(groupDomain);
|
||||
colors.domain(groupDomain);
|
||||
graph.links = correctLink;
|
||||
force = d3.layout.force()
|
||||
.size([width, height])
|
||||
|
@ -75,6 +76,18 @@ function initGraph() {
|
|||
.on("zoom", zoomHandler);
|
||||
svg.call(zoom);
|
||||
|
||||
svg.append('g')
|
||||
.classed('legendContainer', true)
|
||||
.append('g')
|
||||
.classed('legend', true);
|
||||
legendLabels = groupDomain.map(function(domain) {
|
||||
return {
|
||||
text: domain,
|
||||
color: colors(domain)
|
||||
}
|
||||
})
|
||||
drawLabels();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
|
@ -124,9 +137,9 @@ function update() {
|
|||
// })
|
||||
nodesEnter.append("circle")
|
||||
.attr("r", 5)
|
||||
.style("fill", "lightsteelblue")
|
||||
.style("stroke", "steelblue")
|
||||
.style("stroke-width", "2px");
|
||||
.style("fill", function(d) { return colors(d.group); })
|
||||
.style("stroke", "black")
|
||||
.style("stroke-width", "1px");
|
||||
nodesEnter.append("text")
|
||||
.attr("dy", "20px")
|
||||
.attr("dx", "")
|
||||
|
@ -179,5 +192,54 @@ function drag(force) {
|
|||
.on("drag", dragmove)
|
||||
.on("dragend", dragend)
|
||||
}
|
||||
|
||||
function drawLabels() {
|
||||
labels = svg.select('.legend')
|
||||
.selectAll('.labels')
|
||||
.data(legendLabels);
|
||||
var label = labels.enter()
|
||||
.append('g')
|
||||
.attr('class', 'labels')
|
||||
label.append('circle')
|
||||
label.append('text')
|
||||
|
||||
labels.selectAll('circle')
|
||||
.style('fill', function(d, i){ return d.color })
|
||||
.style('stroke', function(d, i){ return d.color })
|
||||
.attr('r', 5);
|
||||
labels.selectAll('text')
|
||||
.text(function(d) { return d.text })
|
||||
.style('font-size', '16px')
|
||||
.style('text-decoration', function(d) { return d.disabled ? 'line-through' : '' })
|
||||
.attr('fill', function(d) { return d.disabled ? 'gray' : '' })
|
||||
.attr('text', 'start')
|
||||
.attr('dy', '.32em')
|
||||
.attr('dx', '8');
|
||||
labels.exit().remove();
|
||||
var ypos = 10, newxpos = 20, xpos;
|
||||
label
|
||||
.attr('transform', function(d, i) {
|
||||
var length = d3.select(this).select('text').node().getComputedTextLength() + 28;
|
||||
var xpos = newxpos;
|
||||
|
||||
if (width < (margin.left) + margin.right + xpos + length) {
|
||||
newxpos = xpos = 20;
|
||||
ypos += 20;
|
||||
}
|
||||
|
||||
newxpos += length;
|
||||
|
||||
return 'translate(' + xpos + ',' + ypos + ')';
|
||||
})
|
||||
var legendBB = svg.select('.legend').node().getBBox();
|
||||
var pad = 3;
|
||||
svg.select('.legendContainer').insert('rect', ':first-child')
|
||||
.style('fill', '#fff')
|
||||
.attr('x', legendBB.x - pad)
|
||||
.attr('y', legendBB.y - pad)
|
||||
.attr('width', legendBB.width + pad)
|
||||
.attr('height', legendBB.height + pad)
|
||||
.style('stroke', '#eee');
|
||||
}
|
||||
</script>
|
||||
<?php endif; ?>
|
|
@ -1,6 +1,19 @@
|
|||
<button class="btn btn-inverse" onclick="$('#references_div').toggle('blind', 300);"><span class="fa fa-eye-slash"> <?php echo __('Toggle Cluster relationships'); ?></span></button>
|
||||
<button class="btn btn-inverse" onclick="toggleClusterRelations()"><span class="fa fa-eye-slash"> <?php echo __('Toggle Cluster relationships'); ?></span></button>
|
||||
<div id="references_div" style="position: relative; border: solid 1px;" class="statistics_attack_matrix hidden">
|
||||
<?php
|
||||
echo $this->element('GalaxyClusters/view_relations');
|
||||
?>
|
||||
</div>
|
||||
<script>
|
||||
function toggleClusterRelations() {
|
||||
$('#references_div').toggle({
|
||||
effect: 'blind',
|
||||
duration: 300,
|
||||
complete: function() {
|
||||
if (window.buildTree !== undefined) {
|
||||
buildTree();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue