Skip to content

Commit 07117ec

Browse files
committed
Enhance the dependency tree view in a more dynamic rendering
Signed-off-by: tdruez <[email protected]>
1 parent 61bc390 commit 07117ec

File tree

12 files changed

+340
-71
lines changed

12 files changed

+340
-71
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ v35.2.0 (unreleased)
1818
``policies.yml`` file.
1919
https://github.com/aboutcode-org/scancode.io/issues/1348
2020

21+
- Enhance the dependency tree view in a more dynamic rendering.
22+
Vulnerabilities and compliance alert are displayed along the dependency entries.
23+
2124
v35.1.0 (2025-07-02)
2225
--------------------
2326

Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
.tree{
2+
--spacing : 1.5rem;
3+
--radius : 10px;
4+
}
5+
6+
.tree li{
7+
display : block;
8+
position : relative;
9+
padding-left : calc(2 * var(--spacing) - var(--radius) - 2px);
10+
}
11+
12+
.tree ul{
13+
margin-left : calc(var(--radius) - var(--spacing));
14+
padding-left : 0;
15+
}
16+
17+
.tree ul li{
18+
border-left : 2px solid #ddd;
19+
}
20+
21+
.tree ul li:last-child{
22+
border-color : transparent;
23+
}
24+
25+
.tree ul li::before{
26+
content : '';
27+
display : block;
28+
position : absolute;
29+
top : calc(var(--spacing) / -2);
30+
left : -2px;
31+
width : calc(var(--spacing) + 2px);
32+
height : calc(var(--spacing) + 1px);
33+
border : solid #ddd;
34+
border-width : 0 0 2px 2px;
35+
}
36+
37+
.tree summary{
38+
display : block;
39+
cursor : pointer;
40+
}
41+
42+
.tree summary::marker,
43+
.tree summary::-webkit-details-marker{
44+
display : none;
45+
}
46+
47+
.tree summary:focus{
48+
outline : none;
49+
}
50+
51+
.tree summary:focus-visible{
52+
outline : 1px dotted #000;
53+
}
54+
55+
.tree li::after,
56+
.tree summary::before{
57+
content : '';
58+
display : block;
59+
position : absolute;
60+
top : calc(var(--spacing) / 2 - var(--radius));
61+
left : calc(var(--spacing) - var(--radius) - 1px);
62+
width : calc(2 * var(--radius));
63+
height : calc(2 * var(--radius));
64+
border-radius : 50%;
65+
background : #ddd;
66+
}
67+
68+
.tree summary::before{
69+
z-index : 1;
70+
background : #696 url('expand-collapse.svg') 0 0;
71+
}
72+
73+
.tree details[open] > summary::before{
74+
background-position : calc(-2 * var(--radius)) 0;
75+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
about_resource: tree.css
2+
name: css-tree-views
3+
homepage_url: https://iamkate.com/code/tree-views/
4+
description: A tree view (collapsible list) can be created using only html and css, without
5+
the need for JavaScript. Accessibility software will see the tree view as lists nested inside
6+
disclosure widgets, and the standard keyboard interaction is supported automatically.
7+
license_expression: cc0-1.0
8+
licenses:
9+
- key: cc0-1.0
10+
name: cc0-1.0

scanpipe/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2568,6 +2568,13 @@ def from_db(cls, db, field_names, values):
25682568

25692569
return new
25702570

2571+
@property
2572+
def has_compliance_issue(self):
2573+
"""Return True if the compliance status is not OK or not set."""
2574+
if not self.compliance_alert or self.compliance_alert == self.Compliance.OK:
2575+
return False
2576+
return True
2577+
25712578
@property
25722579
def license_policy_index(self):
25732580
return self.project.license_policy_index

scanpipe/templates/scanpipe/includes/project_list_table.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
<a href="{% url 'project_dependencies' project.slug %}">
4040
{{ project.discovereddependencies_count|intcomma }}
4141
</a>
42+
<a href="{% url 'project_dependency_tree' project.slug %}">
43+
<span class="icon">
44+
<i class="fa-solid fa-sitemap fa-sm"></i>
45+
</span>
46+
</a>
4247
{% else %}
4348
<span>0</span>
4449
{% endif %}

scanpipe/templates/scanpipe/includes/project_summary_level.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
{{ project.vulnerable_dependency_count|intcomma }}
4545
</a>
4646
{% endif %}
47+
<a href="{% url 'project_dependency_tree' project.slug %}" class="ml-2">
48+
<span class="icon">
49+
<i class="fa-solid fa-sitemap is-size-6"></i>
50+
</span>
51+
</a>
4752
{% else %}
4853
<span class="has-text-grey">0</span>
4954
{% endif %}
Lines changed: 135 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
{% extends "scanpipe/base.html" %}
2+
{% load static %}
23

34
{% block title %}ScanCode.io: {{ project.name }} - Dependency tree{% endblock %}
45

6+
{% block extrahead %}
7+
<link rel="stylesheet" href="{% static 'iamkate-tree-views/tree.css' %}" crossorigin="anonymous">
8+
<style>
9+
.tree {
10+
line-height: 1.8rem;
11+
--spacing : 2rem;
12+
}
13+
.tree summary {
14+
display: inline-block;
15+
}
16+
.tree summary::before{
17+
background-color: rgb(72, 199, 142);
18+
background-image: url('{% static "iamkate-tree-views/expand-collapse.svg" %}');
19+
}
20+
</style>
21+
{% endblock %}
22+
523
{% block content %}
624
<div id="content-header" class="container is-max-widescreen mb-3">
725
{% include 'scanpipe/includes/navbar_header.html' %}
@@ -13,64 +31,127 @@
1331
</div>
1432

1533
<div class="container is-max-widescreen mb-3">
16-
{% if recursion_error %}
17-
<article class="message is-danger">
18-
<div class="message-body">
19-
The dependency tree cannot be rendered as it contains circular references.
20-
{{ message|linebreaksbr }}
21-
</div>
22-
</article>
23-
{% endif %}
24-
<div id="tree"></div>
34+
<section class="mx-5">
35+
{% if recursion_error %}
36+
<article class="message is-danger">
37+
<div class="message-body">
38+
The dependency tree cannot be rendered as it contains circular references.
39+
{{ message|linebreaksbr }}
40+
</div>
41+
</article>
42+
{% endif %}
43+
44+
<div class="mb-4">
45+
<button id="collapseAll" class="button is-small">
46+
<span>Collapse All</span>
47+
<span class="icon is-small">
48+
<i class="fas fa-minus"></i>
49+
</span>
50+
</button>
51+
<button id="expendAll" class="button is-small">
52+
<span>Expend All</span>
53+
<span class="icon is-small">
54+
<i class="fas fa-plus"></i>
55+
</span>
56+
</button>
57+
<button id="showVulnerableOnlyButton" class="button is-small">
58+
<span>Show Vulnerable only</span>
59+
<span class="icon is-small">
60+
<i class="fa-solid fa-bug"></i>
61+
</span>
62+
</button>
63+
<button id="showComplianceAlertOnlyButton" class="button is-small">
64+
<span>Show Compliance Alert only</span>
65+
<span class="icon is-small ">
66+
<i class="fa-solid fa-scale-balanced"></i>
67+
</span>
68+
</button>
69+
</div>
70+
71+
<ul id="tree" class="tree">
72+
<li>
73+
<details open>
74+
<summary class="has-text-weight-semibold">
75+
{{ dependency_tree.name }}
76+
</summary>
77+
{% include 'scanpipe/tree/dependency_children.html' with children=dependency_tree.children %}
78+
</details>
79+
</li>
80+
</ul>
81+
</section>
2582
</div>
2683
{% endblock %}
2784

2885
{% block scripts %}
29-
<script src="https://d3js.org/d3.v7.min.js"></script>
30-
<script src="https://cdn.jsdelivr.net/npm/@observablehq/[email protected]"></script>
31-
{{ dependency_tree|json_script:"dependency_tree" }}
32-
{{ row_count|json_script:"row_count" }}
33-
{{ max_depth|json_script:"max_depth" }}
34-
<script>
35-
const data = JSON.parse(document.getElementById("dependency_tree").textContent);
36-
const hierarchyData = d3.hierarchy(data);
37-
const columnWidth = 110;
38-
const rowWidth = 25;
39-
const columnCount = hierarchyData.height;
40-
const rowCount = hierarchyData.links().length;
41-
const width = columnWidth * (columnCount + 1);
42-
const height = rowWidth * (rowCount + 1);
43-
44-
function indent() {
45-
return (root) => {
46-
root.eachBefore((node, i) => {
47-
node.y = node.depth;
48-
node.x = i;
49-
});
50-
};
86+
<script>
87+
document.addEventListener('DOMContentLoaded', () => {
88+
const treeContainer = document.getElementById('tree');
89+
const collapseAllButton = document.getElementById('collapseAll');
90+
const expendAllButton = document.getElementById('expendAll');
91+
92+
function collapseAllDetails() {
93+
document.querySelectorAll('details').forEach(details => {
94+
details.removeAttribute('open');
95+
});
96+
showAllListItems();
97+
}
98+
99+
function expendAllDetails() {
100+
document.querySelectorAll('details').forEach(details => {
101+
details.setAttribute('open', ''); // Adding 'open' attribute to open the details
102+
});
103+
showAllListItems();
104+
}
105+
106+
collapseAllButton.addEventListener('click', collapseAllDetails);
107+
expendAllButton.addEventListener('click', expendAllDetails);
108+
109+
// Following function are use to limit the display to specific elements.
110+
111+
function expandAncestors(detailsElement) {
112+
let parent = detailsElement.parentElement.closest('details');
113+
while (parent) {
114+
parent.setAttribute('open', '');
115+
parent.parentElement.style.display = '';
116+
parent = parent.parentElement.closest('details');
117+
}
118+
}
119+
120+
function showAllListItems() {
121+
const listItems = treeContainer.querySelectorAll('li');
122+
listItems.forEach(item => {
123+
item.style.display = '';
124+
});
125+
}
126+
127+
function hideAllListItems() {
128+
const listItems = treeContainer.querySelectorAll('li');
129+
listItems.forEach(item => {
130+
item.style.display = 'none';
131+
});
132+
}
133+
134+
function handleItems(attribute, value) {
135+
collapseAllDetails();
136+
hideAllListItems();
137+
138+
const items = document.querySelectorAll(`li[${attribute}="${value}"]`);
139+
items.forEach(item => {
140+
item.style.display = 'block';
141+
expandAncestors(item);
142+
});
143+
}
144+
145+
function handleVulnerableItems() {
146+
handleItems('data-is-vulnerable', 'true');
147+
}
148+
149+
function handleComplianceAlertItems() {
150+
handleItems('data-compliance-alert', 'true');
51151
}
52152

53-
// https://observablehq.com/plot/marks/tree
54-
const plot = Plot.plot({
55-
axis: null,
56-
margin: 10,
57-
marginLeft: 40,
58-
marginRight: 160,
59-
width: width,
60-
height: height,
61-
marks: [
62-
Plot.tree(hierarchyData.leaves(), {
63-
path: (node) => node.ancestors().reverse().map(({ data: { name } }) => name).join("|"),
64-
delimiter: "|",
65-
treeLayout: indent,
66-
strokeWidth: 1,
67-
curve: "step-before",
68-
fontSize: 14,
69-
textStroke: "none"
70-
})
71-
]
72-
});
73-
74-
document.getElementById("tree").appendChild(plot);
75-
</script>
153+
showVulnerableOnlyButton.addEventListener('click', handleVulnerableItems);
154+
showComplianceAlertOnlyButton.addEventListener('click', handleComplianceAlertItems);
155+
});
156+
</script>
76157
{% endblock %}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<ul>
2+
{% for node in children %}
3+
<li
4+
{% if node.is_vulnerable %} data-is-vulnerable="true"{% endif %}
5+
{% if node.has_compliance_issue %} data-compliance-alert="true"{% endif %}
6+
>
7+
{% if node.children %}
8+
<details class="my-1" open>
9+
<summary>
10+
{% include 'scanpipe/tree/dependency_node.html' with node=node only %}
11+
</summary>
12+
{% include 'scanpipe/tree/dependency_children.html' with children=node.children only %}
13+
</details>
14+
{% else %}
15+
{% include 'scanpipe/tree/dependency_node.html' with node=node only %}
16+
{% endif %}
17+
</li>
18+
{% endfor %}
19+
</ul>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{% if node.name %}
2+
{{ node.name }}
3+
{% else %}
4+
Missing data
5+
{% endif %}
6+
7+
<span class="ml-1">
8+
{% if node.url %}
9+
<a href="{{ node.url }}" target="_blank">
10+
<i class="fa-solid fa-square-arrow-up-right fa-sm" title="View details"></i>
11+
</a>
12+
{% endif %}
13+
{% if node.has_compliance_issue %}
14+
<a href="{{ node.url }}#terms" target="_blank" class="{% if node.compliance_alert == 'error' %}has-text-danger{% else %}has-text-warning{% endif %}">
15+
<i class="fa-solid fa-scale-balanced fa-sm" title="Compliance alert: {{ node.compliance_alert }}"></i>
16+
</a>
17+
{% endif %}
18+
{% if node.is_vulnerable %}
19+
<a href="{{ node.url }}#vulnerabilities" target="_blank">
20+
<i class="fa-solid fa-bug fa-sm has-text-danger" title="Vulnerabilities"></i>
21+
</a>
22+
{% endif %}
23+
</span>

0 commit comments

Comments
 (0)