Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions scanpipe/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,13 +522,23 @@ def filter(self, qs, value):


class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
detected_license_expression = django_filters.ChoiceFilter(
label="Detected license expression",
choices=[
(EMPTY_VAR, "None"),
(ANY_VAR, "Any"),
],
widget=HasValueDropdownWidget(),
)

dropdown_widget_fields = [
"status",
"type",
"tag",
"compliance_alert",
"in_package",
"relation_map_type",
"detected_license_expression",
]

search = QuerySearchFilter(
Expand Down
14 changes: 13 additions & 1 deletion scanpipe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from django.db import transaction
from django.db.models import Case
from django.db.models import Count
from django.db.models import Exists
from django.db.models import IntegerField
from django.db.models import OuterRef
from django.db.models import Prefetch
Expand Down Expand Up @@ -231,7 +232,7 @@ def delete(self, *args, **kwargs):
Note that projects with queued or running pipeline runs cannot be deleted.
See the `_raise_if_run_in_progress` method.
The following if statements should not be triggered unless the `.delete()`
method is directly call from an instance of this class.
method is directly call from a instance of this class.
"""
with suppress(redis.exceptions.ConnectionError, AttributeError):
if self.status == self.Status.RUNNING:
Expand Down Expand Up @@ -2425,6 +2426,17 @@ def macho_binaries(self):
def executable_binaries(self):
return self.union(self.win_exes(), self.macho_binaries(), self.elfs())

def with_has_children(self):
"""
Annotate the QuerySet with has_children field based on whether
each resource has any children (subdirectories/files).
"""
children_qs = CodebaseResource.objects.filter(
parent_path=OuterRef("path"),
)

return self.annotate(has_children=Exists(children_qs))


class ScanFieldsModelMixin(models.Model):
"""Fields returned by the ScanCode-toolkit scans."""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
<div class="dropdown is-hoverable {% if is_right %}is-right{% endif %}">
<div class="dropdown is-hoverable{% if is_right %} is-right{% endif %}">
<div class="dropdown-trigger">
<a class="{% if filter.data %}has-text-link{% else %}is-grey-link{% endif %}" aria-haspopup="true" aria-controls="{{ filter.id_for_label }}">
<a class="{% if filter.value %}has-text-link{% else %}is-grey-link{% endif %}" aria-haspopup="true" aria-controls="{{ filter.id_for_label }}">
<span class="icon width-1 height-1"><i class="fa-solid fa-filter"></i></span>
</a>
</div>
<div class="dropdown-menu" id="{{ filter.id_for_label }}" role="menu">
<div class="dropdown-content">
{{ filter }}
<div class="dropdown-content p-2">
{% for val, label in filter.field.choices %}
{% if is_htmx %}
<a href="#"
class="dropdown-item{% if filter.value == val or not filter.value and val == '' %} is-active{% endif %}"
hx-get="{% url 'codebase_resource_table' project.slug %}"
hx-target="#right-pane"
hx-push-url="false"
hx-vals='{"path": "{{ path|escapejs }}", "{{ filter.name }}": "{{ val }}"}'>
{{ label }}
</a>
{% else %}
<a href="?{{ filter.name }}={{ val }}"
class="dropdown-item{% if filter.value == val or not filter.value and val == '' %} is-active{% endif %}">
{{ label }}
</a>
{% endif %}
{% endfor %}
</div>
</div>
</div>
23 changes: 18 additions & 5 deletions scanpipe/templates/scanpipe/includes/filter_sort.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
<a href="?{{ column.sort_query }}" class="is-black-link">
<a
class="is-black-link"
{% if is_htmx %}
href="?path={{ path|urlencode }}&sort={% if column.is_sorted and column.sort_direction == '-' %}{{ column.field_name }}{% else %}-{{ column.field_name }}{% endif %}"
hx-get="{% url 'codebase_resource_table' project.slug %}"
hx-target="#right-pane"
hx-push-url="false"
hx-vals='{"path": "{{ path|escapejs }}", "sort": "{% if column.is_sorted and column.sort_direction == '-' %}{{ column.field_name }}{% else %}-{{ column.field_name }}{% endif %}"}'
onclick="return false;"
style="cursor:pointer;"
{% else %}
href="?{{ column.sort_query }}"
{% endif %}
>
{{ column.label }}
</a>
{% if column.is_sorted %}
<i class="fa-solid fa-sort-{% if column.sort_direction == "-" %}down{% else %}up{% endif %}"></i>
{% endif %}
{% if column.is_sorted %}
<i class="fa-solid fa-sort-{% if column.sort_direction == '-' %}down{% else %}up{% endif %}"></i>
{% endif %}
</a>
4 changes: 2 additions & 2 deletions scanpipe/templates/scanpipe/includes/list_view_thead.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
<div class="is-flex is-justify-content-space-between">
<div>
{% if column.sort_query %}
{% include 'scanpipe/includes/filter_sort.html' with column=column only %}
{% include 'scanpipe/includes/filter_sort.html' with column=column is_htmx=False path=path only %}
{% else %}
{{ column.label }}
{% endif %}
</div>
{% if column.filter %}
<div class="ml-1">
{% include 'scanpipe/dropdowns/filter_dropdown_choices_field.html' with filter=column.filter is_right=column.filter_is_right only %}
{% include 'scanpipe/dropdowns/filter_dropdown_choices_field.html' with filter=column.filter is_right=column.filter_is_right is_htmx=False path=path only %}
</div>
{% endif %}
</div>
Expand Down
29 changes: 29 additions & 0 deletions scanpipe/templates/scanpipe/panels/codebase_tree_panel.html
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" 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" 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>
7 changes: 5 additions & 2 deletions scanpipe/templates/scanpipe/panels/project_codebase.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<nav id="codebase-navigation" class="panel is-dark">
<p class="panel-heading">
Codebase
<p class="panel-heading is-flex is-justify-content-space-between is-align-items-center">
<span>Codebase</span>
<a href="{% url 'codebase_resource_tree' project.slug %}" class="ml-2 has-text-white has-text-decoration-none">
<i class="fa-solid fa-folder-tree mr-1"></i>Tree view
</a>
{% if current_dir and current_dir != "." %}
<span class="tag ml-2">
{% for dir_name, full_path in codebase_breadcrumbs.items %}
Expand Down
123 changes: 123 additions & 0 deletions scanpipe/templates/scanpipe/panels/resource_table_panel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{% load humanize %}

<div id="right-pane">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<thead class="is-sticky">
<tr>
{% if select_all %}
<th class="p-2">
<input type="checkbox" id="select-all">
</th>
{% endif %}
{% for column in columns_data %}
<th id="column-{{ column.field_name }}" {% if column.css_class %}class="{{ column.css_class }}"{% elif column.field_name in filter.data.sort %}class="nowrap"{% endif %}>
<div class="is-flex is-justify-content-space-between">
<div>
{% if column.sort_query %}
{% include 'scanpipe/includes/filter_sort.html' with column=column is_htmx=True project=project path=path only %}
{% else %}
{{ column.label }}
{% endif %}
</div>
{% if column.filter %}
<div class="ml-1">
{% include 'scanpipe/dropdowns/filter_dropdown_choices_field.html' with filter=column.filter is_right=True is_htmx=True project=project path=path only %}
</div>
{% endif %}
</div>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% if resources %}
{% for resource in resources %}
<tr>
<td class="break-all" style="min-width: 200px;">
{% if resource.type == "directory" %}
{{ resource.path }}
{% 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.type != "directory" %}
{{ 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:"0: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 %}
{% else %}
<tr>
<td colspan="100%" style="background: none; border: none; padding: 2rem 0;" class="has-text-centered">
<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>
</td>
</tr>
{% endif %}
</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 %}?{% for key, value in request.GET.items %}{% if key != 'page' and value %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}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 %}?{% for key, value in request.GET.items %}{% if key != 'page' and value %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}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 %}
</div>
Loading
Loading