Skip to content

Commit

Permalink
Add visual indicator when an object also have tree in hierarchy view #70
Browse files Browse the repository at this point in the history
 (#126)

Signed-off-by: tdruez <[email protected]>
  • Loading branch information
tdruez committed May 30, 2024
1 parent ae1e2d4 commit b74d174
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 157 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ Release notes

### Version 5.1.1-dev

- Add visual indicator in hierarchy views, when an object on the far left or far right
also belong or have a hierarchy (relathionship tree).
https://github.com/nexB/dejacode/issues/70

### Version 5.1.0

- Upgrade Python version to 3.12 and Django to 5.0.x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@

{% block javascripts %}
{{ block.super }}
<script src="{% static "js/jquery.jsPlumb-1.7.2-min.js" %}" integrity="sha384-ITD4LUuh8ImLrJ5g55OIlG2QoiYVUuXLN9CStlO1e2SQZm0SyGfNkMiwPboMOv8D" crossorigin="anonymous"></script>
<script src="{% static 'js/jquery.jsPlumb-1.7.2-min.js' %}" integrity="sha384-ITD4LUuh8ImLrJ5g55OIlG2QoiYVUuXLN9CStlO1e2SQZm0SyGfNkMiwPboMOv8D" crossorigin="anonymous"></script>
{% include 'component_catalog/includes/component_hierarchy.js.html' with related_parents=tabsets.Hierarchy.fields.0.1.related_parents related_children=tabsets.Hierarchy.fields.0.1.related_children productcomponents=tabsets.Hierarchy.fields.0.1.productcomponents %}
{% if tabsets.Owner.extra %}
{% include 'organization/includes/owner_hierarchy.js.html' with current_owner=object.owner parents=tabsets.Owner.extra.context.owner_parents children=tabsets.Owner.extra.context.owner_children tab_name="tab_owner" %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,84 +1,66 @@
<script>
function isTabActive(id) {
const activeTab = document.querySelector('.tab-pane.active');
if (activeTab) {
return activeTab.id === id;
}
return false;
}
{% extends "hierarchy_base.js.html" %}

jsPlumb.ready(function() {
var jsPlumbComponentHierarchy = jsPlumb.getInstance({
Connector: ['Straight'],
PaintStyle: {strokeStyle: 'gray', lineWidth: 1},
EndpointStyle: {radius: 3, fillStyle: 'gray'},
Anchors: ['LeftMiddle', 'RightMiddle'],
Container: $("#tab_hierarchy")
});
{% block herarchy_js_content %}
// Connect related_parents
{% for related_parent in related_parents %}
var source_id = 'component_{{ object.pk }}';
var target_id = 'component_{{ related_parent.parent.pk }}';
var connectionOptions = {
source: source_id,
target: target_id,
paintStyle: {strokeStyle: 'gray', lineWidth: 2}
};
{% if object.dataspace.show_usage_policy_in_user_views and related_parent.usage_policy %}
var fill_color = '{{ related_parent.get_usage_policy_color }}';
connectionOptions['endpointStyle'] = {fillStyle: fill_color, radius: 4};
connectionOptions['paintStyle'] = {strokeStyle: fill_color, lineWidth: 2};
{% endif %}
{% if not related_parent.is_deployed %}
connectionOptions['paintStyle']['dashstyle'] = '2 2';
{% endif %}
connectNodes(jsPlumbHierarchy, connectionOptions);

// Do not draw right away as the tab may be hidden
jsPlumbComponentHierarchy.setSuspendDrawing(true);
{% for related_parent in related_parents %}
var connection = {
source: 'component_{{ object.pk }}',
target: 'component_{{ related_parent.parent.pk }}',
paintStyle: {strokeStyle: 'gray', lineWidth: 2}
};
{% if object.dataspace.show_usage_policy_in_user_views and related_parent.usage_policy %}
var fill_color = '{{ related_parent.get_usage_policy_color }}';
connection['endpointStyle'] = {fillStyle: fill_color, radius: 4};
connection['paintStyle'] = {strokeStyle: fill_color, lineWidth: 2};
{% endif %}
{% if not related_parent.is_deployed %}
connection['paintStyle']['dashstyle'] = '2 2';
{% endif %}
jsPlumbComponentHierarchy.connect(connection);
{% endfor %}
{% if related_parent.parent_count > 0 %}
var linkUrl = '{{ related_parent.parent.get_absolute_url }}#hierarchy';
addEndpointWithLink(jsPlumbHierarchy, target_id, 'LeftMiddle', linkUrl);
{% endif %}
{% endfor %}

{% for productcomponent in productcomponents %}
var connection = {
source: 'component_{{ object.pk }}',
target: 'product_{{ productcomponent.product.pk }}',
paintStyle: {strokeStyle: 'grey', lineWidth: 2}
};
{% if not productcomponent.is_deployed %}
connection['paintStyle']['dashstyle'] = '2 2';
{% endif %}
jsPlumbComponentHierarchy.connect(connection);
{% endfor %}
// Connect products (productcomponents)
{% for productcomponent in productcomponents %}
var connectionOptions = {
source: 'component_{{ object.pk }}',
target: 'product_{{ productcomponent.product.pk }}',
paintStyle: {strokeStyle: 'grey', lineWidth: 2}
};
{% if not productcomponent.is_deployed %}
connectionOptions['paintStyle']['dashstyle'] = '2 2';
{% endif %}
connectNodes(jsPlumbHierarchy, connectionOptions);
{% endfor %}

{% for related_child in related_children %}
var connection = {
source: 'component_{{ related_child.child.pk }}',
target: 'component_{{ object.pk }}',
paintStyle: {strokeStyle: 'gray', lineWidth: 2}
};
{% if object.dataspace.show_usage_policy_in_user_views and related_child.usage_policy %}
var fill_color = '{{ related_child.get_usage_policy_color }}';
connection['endpointStyle'] = {fillStyle: fill_color, radius: 4};
connection['paintStyle'] = {strokeStyle: fill_color, lineWidth: 2};
{% endif %}
{% if not related_parent.is_deployed %}
connection['paintStyle']['dashstyle'] = '2 2';
{% endif %}
jsPlumbComponentHierarchy.connect(connection);
{% endfor %}
// Connect related_children
{% for related_child in related_children %}
var source_id = 'component_{{ related_child.child.pk }}';
var target_id = 'component_{{ object.pk }}';
var connectionOptions = {
source: source_id,
target: target_id,
paintStyle: {strokeStyle: 'gray', lineWidth: 2}
};
{% if object.dataspace.show_usage_policy_in_user_views and related_child.usage_policy %}
var fill_color = '{{ related_child.get_usage_policy_color }}';
connectionOptions['endpointStyle'] = {fillStyle: fill_color, radius: 4};
connectionOptions['paintStyle'] = {strokeStyle: fill_color, lineWidth: 2};
{% endif %}
{% if not related_child.is_deployed %}
connectionOptions['paintStyle']['dashstyle'] = '2 2';
{% endif %}
connectNodes(jsPlumbHierarchy, connectionOptions);

// Draw if the related tab is active
if (isTabActive("tab_hierarchy"))
jsPlumbComponentHierarchy.setSuspendDrawing(false, true);

// Repaint on opening the tab, as when the tab content is hidden
// the connectors are not painted properly
$('button[data-bs-target="#tab_hierarchy"]').on('shown.bs.tab', function (e) {
// Second argument instructs jsPlumb to perform a full repaint.
jsPlumbComponentHierarchy.setSuspendDrawing(false, true);
});

// Repaint on resizing the browser window if the related tab is active
$(window).resize(function(){
if (isTabActive("tab_hierarchy"))
jsPlumbComponentHierarchy.repaintEverything();
});
});
</script>
{% if related_child.child_count > 0 %}
var linkUrl = '{{ related_child.child.get_absolute_url }}#hierarchy';
addEndpointWithLink(jsPlumbHierarchy, source_id, 'RightMiddle', linkUrl);
{% endif %}
{% endfor %}
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
{% endif %}
</td>
<td>{{ data.child.version }}</td>
<td>{{ data.child.owner }}</td>
<td>{{ data.child.owner|default_if_none:"" }}</td>
<td>{{ data.subcomponent.purpose }}</td>
<td>
{% if data.subcomponent.license_expression %}
Expand All @@ -50,7 +50,7 @@
<td class="text-center">{{ data.subcomponent.is_modified|as_icon }}</td>
<td>
{% if component.is_active %}
<ul class="list-inline">
<ul class="list-inline mb-0">
<li class="list-inline-item">
<a href="{{ data.child.get_absolute_url }}#hierarchy" target="_blank" title="{% trans 'Hierarchy' %}"><i class="fas fa-sitemap"></i></a>
</li>
Expand Down
6 changes: 2 additions & 4 deletions component_catalog/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,8 @@ def test_component_catalog_hierarchy_tab(self):

expected1 = f'<div id="component_{self.component1.id}" class="card bg-body-tertiary mb-2">'
expected2 = f'<div id="component_{self.component2.id}" class="card bg-body-tertiary mb-2">'
expected3 = f"source: 'component_{self.component1.id}'"
expected4 = f"target: 'component_{self.component2.id}'"
expected3 = f"var source_id = 'component_{self.component1.id}'"
expected4 = f"var target_id = 'component_{self.component2.id}'"

self.assertContains(response, expected1)
self.assertContains(response, expected2)
Expand Down Expand Up @@ -598,7 +598,6 @@ def test_component_catalog_detail_view_owner_tab_hierarchy_availability(self):
self.client.login(username="nexb_user", password="t3st")
url = self.component1.get_absolute_url()
response = self.client.get(url)
self.assertNotContains(response, "jsPlumbOwnerHierarchy")
self.assertNotContains(response, "Selected Owner")
self.assertNotContains(response, "Child Owners")

Expand All @@ -608,7 +607,6 @@ def test_component_catalog_detail_view_owner_tab_hierarchy_availability(self):
)

response = self.client.get(url)
self.assertContains(response, "jsPlumb")
self.assertContains(response, "Selected Owner")
self.assertContains(response, "Child Owners")
self.assertContains(response, child_owner.name)
Expand Down
41 changes: 31 additions & 10 deletions component_catalog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import signing
from django.core.validators import EMPTY_VALUES
from django.db.models import Count
from django.db.models import Prefetch
from django.http import FileResponse
from django.http import Http404
Expand Down Expand Up @@ -630,18 +631,38 @@ def get_queryset(self):
"licenses",
)

related_children_qs = Subcomponent.objects.select_related(
"usage_policy",
).prefetch_related(
"licenses",
Prefetch("child", queryset=component_prefetch_qs),
related_children_qs = (
Subcomponent.objects.select_related(
"usage_policy",
)
.prefetch_related(
"licenses",
Prefetch("child", queryset=component_prefetch_qs),
)
.annotate(
child_count=Count("child__children"),
)
.order_by(
"child__name",
"child__version",
)
)

related_parents_qs = Subcomponent.objects.select_related(
"usage_policy",
).prefetch_related(
"licenses",
Prefetch("parent", queryset=component_prefetch_qs),
related_parents_qs = (
Subcomponent.objects.select_related(
"usage_policy",
)
.prefetch_related(
"licenses",
Prefetch("parent", queryset=component_prefetch_qs),
)
.annotate(
parent_count=Count("parent__related_parents"),
)
.order_by(
"parent__name",
"parent__version",
)
)

return (
Expand Down
58 changes: 58 additions & 0 deletions dje/templates/hierarchy_base.js.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script>
const tabId = "{{ tab_name|default:"tab_hierarchy" }}"

function isTabActive(id) {
const activeTab = document.querySelector('.tab-pane.active');
return activeTab ? activeTab.id === id : false;
}

function addEndpointWithLink(jsPlumbInstance, elementId, position, linkUrl) {
const endpointOptions = {
paintStyle: {fillStyle: '#2e73d0', radius: 5},
anchors: [position]
}

const endpoint = jsPlumbInstance.addEndpoint(elementId, endpointOptions);
wrapEndpointInLink(endpoint.canvas, linkUrl);
return endpoint;
}

function wrapEndpointInLink(svgElement, linkUrl) {
const linkElement = document.createElement('a');
linkElement.href = linkUrl;
svgElement.parentNode.insertBefore(linkElement, svgElement);
linkElement.appendChild(svgElement);
}

function connectNodes(jsPlumbHierarchy, connectionOptions) {
jsPlumbHierarchy.connect(connectionOptions);
}

jsPlumb.ready(function() {
const jsPlumbHierarchy = jsPlumb.getInstance({
Connector: ['Straight'],
PaintStyle: {strokeStyle: 'gray', lineWidth: 1},
EndpointStyle: {radius: 3, fillStyle: 'gray'},
Anchors: ['LeftMiddle', 'RightMiddle'],
Container: document.querySelector("#tab_hierarchy")
});

// Do not draw right away as the tab may be hidden
jsPlumbHierarchy.setSuspendDrawing(true);

// Draw if the hierarchy tab is active
if (isTabActive(tabId)) jsPlumbHierarchy.setSuspendDrawing(false, true);

document.querySelector('button[data-bs-target="#tab_hierarchy"]').addEventListener('shown.bs.tab', function (e) {
// Second argument instructs jsPlumb to perform a full repaint.
jsPlumbHierarchy.setSuspendDrawing(false, true);
});

// Repaint on resizing the browser window if the related tab is active
window.addEventListener('resize', function(){
if (isTabActive(tabId)) jsPlumbHierarchy.repaintEverything();
});

{% block herarchy_js_content %}{% endblock %}
});
</script>
2 changes: 1 addition & 1 deletion license_library/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def test_license_library_detail_view_owner_tab_hierarchy_availability(self):
)

response = self.client.get(url)
self.assertContains(response, "jsPlumbOwnerHierarchy")
self.assertContains(response, "jsPlumbHierarchy")
self.assertContains(response, "Selected Owner")
self.assertContains(response, "Child Owners")
self.assertContains(response, child_owner.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
}

jsPlumb.ready(function() {
var jsPlumbOwnerHierarchy = jsPlumb.getInstance({
var jsPlumbHierarchy = jsPlumb.getInstance({
Connector: ['Straight'],
PaintStyle: {strokeStyle: 'gray', lineWidth: 1},
EndpointStyle: {radius: 3, fillStyle: 'gray'},
Expand All @@ -14,29 +14,30 @@
});

// Do not draw right away as the tab may be hidden
jsPlumbOwnerHierarchy.setSuspendDrawing(true);
jsPlumbHierarchy.setSuspendDrawing(true);

{% for parent in parents %}
jsPlumbOwnerHierarchy.connect({source: 'owner_{{ current_owner.pk }}', target: 'owner_{{ parent.pk }}'});
jsPlumbHierarchy.connect({source: 'owner_{{ current_owner.pk }}', target: 'owner_{{ parent.pk }}'});
{% endfor %}
{% for child in children %}
jsPlumbOwnerHierarchy.connect({source: 'owner_{{ child.pk }}', target: 'owner_{{ current_owner.pk }}'});
jsPlumbHierarchy.connect({source: 'owner_{{ child.pk }}', target: 'owner_{{ current_owner.pk }}'});
{% endfor %}

// Draw if the related tab is active
if (is_active_tab("{{ tab_name }}"))
jsPlumbOwnerHierarchy.setSuspendDrawing(false, true);
jsPlumbHierarchy.setSuspendDrawing(false, true);

// Repaint on opening the tab, as when the tab content is hidden
// the connectors are not painted properly
$('button[data-bs-target="#{{ tab_name }}"]').on('shown.bs.tab', function (e) {
// Second argument instructs jsPlumb to perform a full repaint.
jsPlumbOwnerHierarchy.setSuspendDrawing(false, true);
jsPlumbHierarchy.setSuspendDrawing(false, true);
});

// Repaint on resizing the browser window if the related tab is active
$(window).resize(function(){
if (is_active_tab("{{ tab_name }}"))
jsPlumbOwnerHierarchy.repaintEverything();
jsPlumbHierarchy.repaintEverything();
});
});
</script>
2 changes: 1 addition & 1 deletion organization/templates/organization/owner_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% load static %}
{% block javascripts %}
{{ block.super }}
<script src="{% static "js/jquery.jsPlumb-1.7.2-min.js" %}" integrity="sha384-ITD4LUuh8ImLrJ5g55OIlG2QoiYVUuXLN9CStlO1e2SQZm0SyGfNkMiwPboMOv8D" crossorigin="anonymous"></script>
<script src="{% static 'js/jquery.jsPlumb-1.7.2-min.js' %}" integrity="sha384-ITD4LUuh8ImLrJ5g55OIlG2QoiYVUuXLN9CStlO1e2SQZm0SyGfNkMiwPboMOv8D" crossorigin="anonymous"></script>
{% if tabsets.Hierarchy.extra %}
{% include 'organization/includes/owner_hierarchy.js.html' with current_owner=tabsets.Hierarchy.extra.context.owner parents=tabsets.Hierarchy.extra.context.owner_parents children=tabsets.Hierarchy.extra.context.owner_children tab_name="tab_hierarchy" %}
{% endif %}
Expand Down
Loading

0 comments on commit b74d174

Please sign in to comment.