-
-
Notifications
You must be signed in to change notification settings - Fork 189
Add left-pane file tree view and related templates #1704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
tdruez
merged 23 commits into
aboutcode-org:main
from
aayushkdev:1682-left-pane-file-tree
Sep 3, 2025
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
4d0b4d5
add left-pane file tree view and related templates
aayushkdev 8e5dd0c
temporarily include parent_path field from previous pr for tests
aayushkdev 606c2b6
Some formatting changes
aayushkdev 8a5b576
bump migration up to resolve failing tests
aayushkdev 69ad5ac
remove conflicting migration
aayushkdev 2c1dd0f
implement suggested changes
aayushkdev 28f7e05
fix code format
aayushkdev 79e2743
use instead of None to follow code format
aayushkdev 054d87f
update save function so tests pass
aayushkdev adbe5b4
add suggested improvements
aayushkdev 3fec6ab
refactor tests
aayushkdev da1596e
fix code format
aayushkdev 8640fef
add the right resource table panel
aayushkdev 7c133bd
add tests
aayushkdev 633e5df
small ui change in table
aayushkdev fe0f33b
add resizable panel
aayushkdev d7dfce4
Merge remote-tracking branch 'origin/main' into 1682-left-pane-file-tree
aayushkdev e665feb
Revert filtering support and add reviewed changes
aayushkdev 08727c0
fix icon alignment
aayushkdev 23b0c0a
update icon+subpath to open caret path on left side
aayushkdev da5be03
update 'path' link to open the corresponding directory in the right pane
aayushkdev 1e39667
Set a minimum width of about 200px for the left panel
aayushkdev 3fa79ec
add a css style for currently selected item
aayushkdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
scanpipe/templates/scanpipe/panels/codebase_tree_panel.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <ul> | ||
| {% for node in children %} | ||
| <li class="mb-1"> | ||
| {% if node.is_dir %} | ||
| <div class="tree-node is-flex is-align-items-center has-text-weight-semibold px-1" data-folder data-path="{{ node.path }}"{% if node.has_children %} data-target="{{ node.path|slugify }}" data-url="{% url 'codebase_resource_tree' slug=project.slug %}?path={{ node.path }}"{% endif %}> | ||
| <span class="icon is-small chevron mr-1{% if not node.has_children %} is-invisible{% endif %} is-clickable is-flex is-align-items-center" data-chevron> | ||
| <i class="fas fa-chevron-right"></i> | ||
| </span> | ||
| <span class="is-flex is-align-items-center folder-meta is-clickable" data-folder-click hx-get="{% url 'codebase_resource_table' project.slug %}?path={{ node.path }}" hx-target="#right-pane"> | ||
| <span class="icon is-small mr-1"> | ||
| <i class="fas fa-folder"></i> | ||
| </span> | ||
| <span>{{ node.name }}</span> | ||
| </span> | ||
| </div> | ||
| {% if node.has_children %} | ||
| <div id="dir-{{ node.path|slugify }}" class="ml-4 is-hidden" data-loaded="false"></div> | ||
| {% endif %} | ||
| {% else %} | ||
| <div class="is-flex is-align-items-center ml-5 is-clickable is-file" data-file data-path="{{ node.path }}" hx-get="{% url 'codebase_resource_table' project.slug %}?path={{ node.path }}" hx-target="#right-pane"> | ||
| <span class="icon is-small mr-1"> | ||
| <i class="far fa-file"></i> | ||
| </span> | ||
| <span>{{ node.name }}</span> | ||
| </div> | ||
| {% endif %} | ||
| </li> | ||
| {% endfor %} | ||
| </ul> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
scanpipe/templates/scanpipe/panels/resource_table_panel.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| {% load humanize %} | ||
|
|
||
| {% if resources %} | ||
| <table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth"> | ||
| <thead class="is-sticky"> | ||
| <tr> | ||
| <th>Path</th> | ||
| <th>Status</th> | ||
| <th>Type</th> | ||
| <th>Size</th> | ||
| <th>Name</th> | ||
| <th>Extension</th> | ||
| <th>Language</th> | ||
| <th>MIME Type</th> | ||
| <th>Tag</th> | ||
| <th>License</th> | ||
| <th>Alert</th> | ||
| <th>Packages</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {% for resource in resources %} | ||
| <tr> | ||
| <td class="break-all" style="min-width: 200px;"> | ||
| {% if resource.is_dir %} | ||
| <a href="#" class="expand-in-tree" data-path="{{ resource.path }}" hx-get="{% url 'codebase_resource_table' project.slug %}?path={{ resource.path }}" hx-target="#right-pane">{{ resource.path }}</a> | ||
| {% else %} | ||
| <a href="{% url 'resource_detail' project.slug resource.path %}">{{ resource.path }}</a> | ||
| {% endif %} | ||
| </td> | ||
| <td> | ||
| {{ resource.status }} | ||
| </td> | ||
| <td> | ||
| {{ resource.type }} | ||
| </td> | ||
| <td> | ||
| {% if resource.is_file %} | ||
| {{ resource.size|filesizeformat|default_if_none:"" }} | ||
| {% endif %} | ||
| </td> | ||
| <td class="break-all" style="min-width: 100px;"> | ||
| {{ resource.name }} | ||
| </td> | ||
| <td> | ||
| {{ resource.extension }} | ||
| </td> | ||
| <td class="break-all"> | ||
| {{ resource.programming_language }} | ||
| </td> | ||
| <td class="break-all"> | ||
| {{ resource.mime_type }} | ||
| </td> | ||
| <td> | ||
| {{ resource.tag }} | ||
| </td> | ||
| <td> | ||
| {{ resource.detected_license_expression }} | ||
| </td> | ||
| <td> | ||
| {{ resource.compliance_alert }} | ||
| </td> | ||
| <td> | ||
| {% if resource.discovered_packages.all %} | ||
| {% for package in resource.discovered_packages.all|slice:":3" %} | ||
| <a href="{% url 'project_packages' project.slug %}?purl={{ package.package_url }}">{{ package }}</a>{% if not forloop.last %}, {% endif %} | ||
| {% endfor %} | ||
| {% if resource.discovered_packages.all|length > 3 %} | ||
| +{{ resource.discovered_packages.all|length|add:"-3" }} more | ||
| {% endif %} | ||
| {% endif %} | ||
| </td> | ||
| </tr> | ||
| {% endfor %} | ||
| </tbody> | ||
| </table> | ||
|
|
||
| {% if is_paginated %} | ||
| <nav class="pagination is-centered mt-4" role="navigation"> | ||
| {% if page_obj.has_previous %} | ||
| <a class="pagination-previous" hx-get="{% url 'codebase_resource_table' project.slug %}?path={{ path }}&page={{ page_obj.previous_page_number }}" hx-target="#right-pane">Previous</a> | ||
| {% endif %} | ||
| {% if page_obj.has_next %} | ||
| <a class="pagination-next" hx-get="{% url 'codebase_resource_table' project.slug %}?path={{ path }}&page={{ page_obj.next_page_number }}" hx-target="#right-pane">Next page</a> | ||
| {% endif %} | ||
| <ul class="pagination-list"> | ||
| <li><span class="pagination-ellipsis">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span></li> | ||
| </ul> | ||
| </nav> | ||
| {% endif %} | ||
| {% else %} | ||
| <div class="has-text-centered p-6"> | ||
| <div class="icon is-large has-text-grey-light mb-3"> | ||
| <i class="fas fa-folder-open fa-3x"></i> | ||
| </div> | ||
| <p class="has-text-grey"> | ||
| {% if path %} | ||
| No resources found in this directory. | ||
| {% else %} | ||
| Select a file or folder from the tree to view its contents. | ||
| {% endif %} | ||
| </p> | ||
| </div> | ||
| {% endif %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| {% extends "scanpipe/base.html" %} | ||
| {% load static humanize %} | ||
| {% block title %}ScanCode.io: {{ project.name }} - Resource Tree{% endblock %} | ||
|
|
||
| {% block extrahead %} | ||
| <style> | ||
| .is-current { | ||
| background: rgba(128,128,128,0.10); | ||
| border-radius: 6px; | ||
| } | ||
| .chevron { | ||
| transition: transform 0.2s ease; | ||
| display: inline-block; | ||
| } | ||
| .chevron.rotated { | ||
| transform: rotate(90deg); | ||
| } | ||
|
|
||
| .resizable-container { | ||
| display: flex; | ||
| height: calc(100vh - 140px); | ||
| margin: 0; | ||
| } | ||
|
|
||
| .left-pane { | ||
| min-width: 0; | ||
| max-width: 100%; | ||
| border-right: 1px solid #ccc; | ||
| overflow-y: auto; | ||
| overflow-x: hidden; | ||
| flex-basis: 25%; | ||
| transition: opacity 0.2s ease; | ||
| } | ||
|
|
||
| .left-pane.collapsed { | ||
| opacity: 0; | ||
| pointer-events: none; | ||
| } | ||
|
|
||
| .resizer { | ||
| width: 5px; | ||
| background: #ddd; | ||
| cursor: col-resize; | ||
| position: relative; | ||
| flex-shrink: 0; | ||
| } | ||
|
|
||
| .resizer:hover { | ||
| background: #bbb; | ||
| } | ||
|
|
||
| .resizer::before { | ||
| content: ''; | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 1px; | ||
| transform: translateY(-50%); | ||
| width: 3px; | ||
| height: 30px; | ||
| background: #999; | ||
| border-radius: 2px; | ||
| } | ||
|
|
||
| .right-pane { | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| overflow-x: hidden; | ||
| min-width: 0; | ||
| transition: opacity 0.2s ease; | ||
| } | ||
|
|
||
| .right-pane.collapsed { | ||
| opacity: 0; | ||
| pointer-events: none; | ||
| } | ||
| </style> | ||
| {% endblock %} | ||
|
|
||
| {% block content %} | ||
| <div id="content-header" class="container is-max-widescreen mb-3"> | ||
| {% include 'scanpipe/includes/navbar_header.html' %} | ||
| <section class="mx-5"> | ||
| <div class="is-flex is-justify-content-space-between"> | ||
| {% include 'scanpipe/includes/breadcrumb.html' with linked_project=True current="Resource Tree" %} | ||
| </div> | ||
| </section> | ||
| </div> | ||
|
|
||
| <div class="resizable-container"> | ||
| <div class="left-pane" id="left-pane"> | ||
| <div id="resource-tree" class="p-4"> | ||
| {% include "scanpipe/panels/codebase_tree_panel.html" with children=children path=path %} | ||
| </div> | ||
| </div> | ||
| <div class="resizer" id="resizer"></div> | ||
| <div class="right-pane" id="right-pane"> | ||
| <div class="p-4"> | ||
| {% include "scanpipe/panels/resource_table_panel.html" %} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {% endblock %} | ||
|
|
||
| {% block scripts %} | ||
| <script> | ||
| // Tree functionality | ||
| document.addEventListener("click", async function (e) { | ||
| const chevron = e.target.closest("[data-chevron]"); | ||
| if (chevron) { | ||
| const folderNode = chevron.closest("[data-folder]"); | ||
| const targetId = folderNode.dataset.target; | ||
| const url = folderNode.dataset.url; | ||
| const target = document.getElementById("dir-" + targetId); | ||
|
|
||
| if (target.dataset.loaded === "true") { | ||
| target.classList.toggle("is-hidden"); | ||
| } else { | ||
| target.classList.remove("is-hidden"); | ||
| const response = await fetch(url + "&tree_panel=true"); | ||
| target.innerHTML = await response.text(); | ||
| target.dataset.loaded = "true"; | ||
| htmx.process(target); | ||
| } | ||
|
|
||
| chevron.classList.toggle("rotated"); | ||
| e.stopPropagation(); | ||
| return; | ||
| } | ||
|
|
||
| const folderMeta = e.target.closest(".folder-meta"); | ||
| if (folderMeta) { | ||
| const folderNode = folderMeta.closest("[data-folder]"); | ||
| if (folderNode && folderNode.dataset.target) { | ||
| document.querySelectorAll('.tree-node.is-current, .is-file.is-current').forEach(el => el.classList.remove('is-current')); | ||
| folderNode.classList.add('is-current'); | ||
| const chevron = folderNode.querySelector("[data-chevron]"); | ||
| const target = document.getElementById("dir-" + folderNode.dataset.target); | ||
|
|
||
| if (target.classList.contains("is-hidden")) { | ||
| target.classList.remove("is-hidden"); | ||
| chevron.classList.add("rotated"); | ||
| if (target.dataset.loaded !== "true") { | ||
| const response = await fetch(folderNode.dataset.url + "&tree_panel=true"); | ||
| target.innerHTML = await response.text(); | ||
| target.dataset.loaded = "true"; | ||
| htmx.process(target); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const fileNode = e.target.closest(".is-file[data-file]"); | ||
| if (fileNode) { | ||
| document.querySelectorAll('.tree-node.is-current, .is-file.is-current').forEach(el => el.classList.remove('is-current')); | ||
| fileNode.classList.add('is-current'); | ||
| } | ||
|
|
||
| const expandLink = e.target.closest(".expand-in-tree"); | ||
| if (expandLink) { | ||
| e.preventDefault(); | ||
| const path = expandLink.getAttribute("data-path"); | ||
| const leftPane = document.getElementById("left-pane"); | ||
| if (!leftPane) return; | ||
| let node = leftPane.querySelector(`[data-folder][data-path="${path}"], .is-file[data-file][data-path="${path}"]`); | ||
| if (node) { | ||
| document.querySelectorAll('.tree-node.is-current, .is-file.is-current').forEach(el => el.classList.remove('is-current')); | ||
| node.classList.add('is-current'); | ||
| const chevron = node.querySelector("[data-chevron]"); | ||
| if (chevron && !chevron.classList.contains("rotated")) chevron.click(); | ||
| node.scrollIntoView({behavior: "smooth", block: "center"}); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| document.addEventListener("DOMContentLoaded", function() { | ||
| const resizer = document.getElementById('resizer'); | ||
| const leftPane = document.getElementById('left-pane'); | ||
| const rightPane = document.getElementById('right-pane'); | ||
| let isResizing = false; | ||
|
|
||
| resizer.addEventListener('mousedown', function(e) { | ||
| isResizing = true; | ||
| document.body.style.cursor = 'col-resize'; | ||
| document.body.style.userSelect = 'none'; | ||
| e.preventDefault(); | ||
| }); | ||
|
|
||
| document.addEventListener('mousemove', function(e) { | ||
| if (!isResizing) return; | ||
|
|
||
| const container = document.querySelector('.resizable-container'); | ||
| const containerRect = container.getBoundingClientRect(); | ||
| let newLeftWidth = e.clientX - containerRect.left; | ||
| const containerWidth = containerRect.width; | ||
| const resizerWidth = 5; | ||
| const minLeftWidth = 200; | ||
| if (newLeftWidth < minLeftWidth) newLeftWidth = minLeftWidth; | ||
| if (newLeftWidth > containerWidth - resizerWidth) newLeftWidth = containerWidth - resizerWidth; | ||
|
|
||
| const leftPercent = (newLeftWidth / containerWidth) * 100; | ||
| const rightPercent = ((containerWidth - newLeftWidth - resizerWidth) / containerWidth) * 100; | ||
|
|
||
| leftPane.style.flexBasis = leftPercent + '%'; | ||
| rightPane.style.flexBasis = rightPercent + '%'; | ||
| }); | ||
|
|
||
| document.addEventListener('mouseup', function() { | ||
| if (isResizing) { | ||
| isResizing = false; | ||
| document.body.style.cursor = ''; | ||
| document.body.style.userSelect = ''; | ||
| } | ||
| }); | ||
| }); | ||
| </script> | ||
| {% endblock %} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.