Skip to content

Commit c90b5ed

Browse files
committed
add left-pane file tree view and related templates
Signed-off-by: Aayush Kumar <aayush214.kumar@gmail.com>
1 parent 17d338f commit c90b5ed

File tree

5 files changed

+180
-0
lines changed

5 files changed

+180
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<ul class="pl-2">
2+
{% for node in children %}
3+
<li class="mb-1">
4+
{% if node.is_dir %}
5+
<div
6+
class="tree-node is-flex is-align-items-center has-text-weight-semibold is-clickable px-1"
7+
data-folder
8+
{% if node.has_children %}
9+
data-target="{{ node.path|slugify }}"
10+
data-url="{% url 'file_tree' slug=project.slug %}?path={{ node.path }}"
11+
{% endif %}
12+
>
13+
<span class="icon is-small chevron mr-1"
14+
data-chevron
15+
{% if not node.has_children %}
16+
style="visibility: hidden;"
17+
{% endif %}
18+
>
19+
<i class="fas fa-chevron-right"></i>
20+
</span>
21+
22+
<span
23+
class="is-flex is-align-items-center folder-meta">
24+
<span class="icon is-small mr-1"><i class="fas fa-folder"></i></span>
25+
<span>{{ node.name }}</span>
26+
</span>
27+
</div>
28+
29+
{% if node.has_children %}
30+
<div id="dir-{{ node.path|slugify }}" class="ml-4 hidden" data-loaded="false"></div>
31+
{% endif %}
32+
33+
{% else %}
34+
<div
35+
class="is-flex is-align-items-center ml-5" style="cursor: pointer;">
36+
<span class="icon is-small mr-1"><i class="fas fa-file"></i></span>
37+
<span>{{ node.name }}</span>
38+
</div>
39+
{% endif %}
40+
</li>
41+
{% endfor %}
42+
</ul>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{% extends "scanpipe/base.html" %}
2+
{% load static %}
3+
{% block title %}ScanCode.io: {{ project.name }} - Resource{% endblock %}
4+
5+
{% block extrahead %}
6+
<style>
7+
.hidden {
8+
display: none;
9+
}
10+
11+
.chevron {
12+
transition: transform 0.2s ease;
13+
display: inline-block;
14+
}
15+
16+
.chevron.rotated {
17+
transform: rotate(90deg);
18+
}
19+
</style>
20+
{% endblock %}
21+
22+
{% block content %}
23+
<div class="columns is-gapless is-mobile" style="height: 100vh; margin: 0;">
24+
<div class="column is-one-third p-4 has-background-white" style="border-right: 1px solid #ccc; overflow-y: auto;">
25+
<div id="file-tree">
26+
{% include "scanpipe/panels/file_tree_panel.html" with children=children path=path %}
27+
</div>
28+
</div>
29+
30+
<div class="column p-4 has-background-white" style="overflow-y: auto;">
31+
<div id="right-pane">
32+
</div>
33+
</div>
34+
</div>
35+
{% endblock %}
36+
37+
{% block scripts %}
38+
<script>
39+
document.addEventListener("click", async function (e) {
40+
const chevron = e.target.closest("[data-chevron]");
41+
if (chevron) {
42+
const folderNode = chevron.closest("[data-folder]");
43+
const targetId = folderNode.dataset.target;
44+
const url = folderNode.dataset.url;
45+
const icon = chevron.querySelector("i");
46+
const target = document.getElementById("dir-" + targetId);
47+
48+
if (target.dataset.loaded === "true") {
49+
target.classList.toggle("hidden");
50+
51+
} else {
52+
target.classList.remove("hidden");
53+
const response = await fetch(url + "&tree=true");
54+
target.innerHTML = await response.text();
55+
target.dataset.loaded = "true";
56+
57+
htmx.process(target);
58+
}
59+
60+
chevron.classList.toggle("rotated");
61+
62+
e.stopPropagation();
63+
return;
64+
}
65+
});
66+
</script>
67+
{% endblock %}

scanpipe/tests/test_views.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,3 +1569,35 @@ def test_project_codebase_resources_export_json(self):
15691569

15701570
for field in expected_fields:
15711571
self.assertIn(field, json_data[0])
1572+
1573+
def test_file_tree_base_url_lists_top_level_nodes(self):
1574+
make_resource_file(self.project1, path="child1.txt")
1575+
make_resource_file(self.project1, path="dir1")
1576+
1577+
url = reverse("file_tree", kwargs={"slug": self.project1.slug})
1578+
response = self.client.get(url)
1579+
children = response.context[-1]["children"]
1580+
1581+
child1 = children[0]
1582+
dir1 = children[1]
1583+
1584+
self.assertEqual(child1.path, "child1.txt")
1585+
self.assertEqual(dir1.path, "dir1")
1586+
1587+
def test_file_tree_nested_url_lists_only_children_of_given_path(self):
1588+
make_resource_file(self.project1, path="parent/child1.txt")
1589+
make_resource_file(self.project1, path="parent/dir1")
1590+
make_resource_file(self.project1, path="parent/dir1/child2.txt")
1591+
1592+
url = reverse("file_tree", kwargs={"slug": self.project1.slug})
1593+
response = self.client.get(url + "?path=parent&tree=true")
1594+
children = response.context["children"]
1595+
1596+
child1 = children[0]
1597+
dir1 = children[1]
1598+
1599+
self.assertEqual(child1.path, "parent/child1.txt")
1600+
self.assertEqual(dir1.path, "parent/dir1")
1601+
1602+
self.assertFalse(child1.has_children)
1603+
self.assertTrue(dir1.has_children)

scanpipe/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,5 +241,10 @@
241241
views.LicenseListView.as_view(),
242242
name="license_list",
243243
),
244+
path(
245+
"project/<slug:slug>/codebase_tree/",
246+
views.CodebaseResourceTreeView.as_view(),
247+
name="file_tree",
248+
),
244249
path("monitor/", include("django_rq.urls")),
245250
]

scanpipe/views.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
from django.core.exceptions import ValidationError
3838
from django.core.files.storage.filesystem import FileSystemStorage
3939
from django.core.serializers.json import DjangoJSONEncoder
40+
from django.db.models import Exists
41+
from django.db.models import OuterRef
4042
from django.db.models import Prefetch
4143
from django.db.models.manager import Manager
4244
from django.http import FileResponse
@@ -2566,3 +2568,35 @@ def get_node(self, package):
25662568
if children:
25672569
node["children"] = children
25682570
return node
2571+
2572+
2573+
class CodebaseResourceTreeView(ConditionalLoginRequired, generic.DetailView):
2574+
template_name = "scanpipe/resource_tree.html"
2575+
2576+
def get(self, request, *args, **kwargs):
2577+
slug = self.kwargs.get("slug")
2578+
project = get_object_or_404(Project, slug=slug)
2579+
path = request.GET.get("path", None)
2580+
2581+
base_qs = (
2582+
CodebaseResource.objects.filter(project=project, parent_path=path)
2583+
.only("path", "name", "type")
2584+
.order_by("path")
2585+
)
2586+
2587+
subdirs = CodebaseResource.objects.filter(
2588+
project=project,
2589+
parent_path=OuterRef("path"),
2590+
)
2591+
2592+
children = base_qs.annotate(has_children=Exists(subdirs))
2593+
2594+
context = {
2595+
"project": project,
2596+
"path": path,
2597+
"children": children,
2598+
}
2599+
2600+
if request.GET.get("tree") == "true":
2601+
return render(request, "scanpipe/panels/file_tree_panel.html", context)
2602+
return render(request, self.template_name, context)

0 commit comments

Comments
 (0)