From 7d24e3fd52c1ae6b230340c1c0f36fc29daee361 Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 8 Oct 2025 17:26:46 +0100 Subject: [PATCH 01/12] Complete Allow the user to delete the rules file from the downloads --- .../0002_add_deleted_at_to_filedownload.py | 28 +++++++ app/api/files/models.py | 2 + app/api/files/views.py | 67 +++++++++++++-- app/next-client-app/api/files.ts | 17 ++++ .../scanreports/[id]/downloads/columns.tsx | 18 +++++ .../components/files/DeleteFileDialog.tsx | 81 +++++++++++++++++++ docker-compose.yml | 1 + 7 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 app/api/files/migrations/0002_add_deleted_at_to_filedownload.py create mode 100644 app/next-client-app/components/files/DeleteFileDialog.tsx diff --git a/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py b/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py new file mode 100644 index 000000000..d32ad2822 --- /dev/null +++ b/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.1 on 2025-10-08 16:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='filedownload', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='filedownload', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='filetype', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/app/api/files/models.py b/app/api/files/models.py index 9f7aab82c..43d9a60db 100644 --- a/app/api/files/models.py +++ b/app/api/files/models.py @@ -40,6 +40,7 @@ class FileDownload(BaseModel): user (User, optional): The user who generated the file. Defaults to None. file_type (FileType): The type of the file. file_url (str, optional): The URL for downloading the file. Defaults to None. + deleted_at (datetime, optional): Timestamp when the file was deleted. Defaults to None. Returns: str: The name of the file. @@ -55,6 +56,7 @@ class FileDownload(BaseModel): ) file_type = models.ForeignKey(FileType, on_delete=models.CASCADE) file_url = models.CharField(max_length=500, null=True, blank=True) + deleted_at = models.DateTimeField(null=True, blank=True) def __str__(self): return str(self.name) diff --git a/app/api/files/views.py b/app/api/files/views.py index 10bc67360..7ac7b3df2 100644 --- a/app/api/files/views.py +++ b/app/api/files/views.py @@ -1,14 +1,17 @@ import json +import os +from datetime import timedelta from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404 +from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema from jobs.models import Job, JobStage, StageStatus from mapping.models import ScanReport from rest_framework.filters import OrderingFilter from rest_framework.generics import GenericAPIView -from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin from rest_framework.permissions import IsAuthenticated from services.storage_service import StorageService from services.worker_service import get_worker_service @@ -22,13 +25,17 @@ worker_service = get_worker_service() -class FileDownloadView(GenericAPIView, ListModelMixin, RetrieveModelMixin): +class FileDownloadView( + GenericAPIView, ListModelMixin, RetrieveModelMixin, DestroyModelMixin +): """ A view for handling file downloads and file generation requests. This view provides functionality to: - Retrieve a list of downloadable files associated with a specific scan report. - Download a specific file by its primary key. - Request the generation of a file for download by sending a message to a queue. + - Delete a file manually from storage and database. + - Automatically filter files older than FILE_RETENTION_DAYS from the list. Attributes: serializer_class (Serializer): The serializer class used for file downloads. filter_backends (list): The list of filter backends for filtering querysets. @@ -37,12 +44,15 @@ class FileDownloadView(GenericAPIView, ListModelMixin, RetrieveModelMixin): ordering (str): The default ordering for querysets. Methods: get_queryset(): - Retrieves the queryset of FileDownload objects filtered by the scan report ID. + Retrieves the queryset of FileDownload objects filtered by the scan report ID + and age (files older than FILE_RETENTION_DAYS are excluded). get(request, *args, **kwargs): Handles GET requests. If a primary key is provided, it downloads the file. Otherwise, it returns a paginated list of files. post(request, *args, **kwargs): Handles POST requests to request the generation of a file for download. + delete(request, *args, **kwargs): + Handles DELETE requests to manually remove a file from storage and database. """ serializer_class = FileDownloadSerializer @@ -56,7 +66,16 @@ def get_queryset(self): scan_report_id = self.kwargs["scanreport_pk"] scan_report = get_object_or_404(ScanReport, pk=scan_report_id) - return FileDownload.objects.filter(scan_report=scan_report) + # Hide files older than retention period (default: 30 days) + retention_days = int(os.getenv("FILE_RETENTION_DAYS", "30")) + cutoff_date = timezone.now() - timedelta(days=retention_days) + + # Only show recent, non-deleted files + return FileDownload.objects.filter( + scan_report=scan_report, + created_at__gte=cutoff_date, + deleted_at__isnull=True, + ) def get(self, request, *args, **kwargs): if "pk" in kwargs: @@ -135,9 +154,7 @@ def post(self, request, *args, **kwargs): file_type_description = ( "JSON V1" if file_type == "application/json_v1" - else "JSON V2" - if file_type == "application/json_v2" - else "CSV" + else "JSON V2" if file_type == "application/json_v2" else "CSV" ) Job.objects.create( scan_report=ScanReport.objects.get(id=scan_report_id), @@ -151,3 +168,39 @@ def post(self, request, *args, **kwargs): return JsonResponse({"error": "Internal server error."}, status=500) return HttpResponse(status=202) + + def delete(self, request, *args, **kwargs): + """ + Handles DELETE requests to soft-delete a file from storage. + + This allows users to immediately delete unwanted files before the automatic + retention period expires. The physical file is removed from storage (Azure/MinIO) + but the database record is kept for audit purposes with a deleted_at timestamp. + + Args: + request: The HTTP request object. + *args: Variable length argument list. + **kwargs: Must contain 'pk' - the primary key of the FileDownload to delete. + + Returns: + - 204 No Content: If the file is successfully deleted. + - 404 Not Found: If the file doesn't exist. + - 500 Internal Server Error: If an error occurs during deletion. + """ + try: + file_download = get_object_or_404(FileDownload, pk=kwargs["pk"]) + + # Delete the physical file from storage if file_url exists + if file_download.file_url: + try: + storage_service.delete_file(file_download.file_url, "rules-exports") + except Exception: + pass # File may not exist in storage + + # Mark as deleted but keep record for audit + file_download.deleted_at = timezone.now() + file_download.save() + + return HttpResponse(status=204) + except Exception: + return JsonResponse({"error": "Internal server error."}, status=500) diff --git a/app/next-client-app/api/files.ts b/app/next-client-app/api/files.ts index 912ceab60..7c3075088 100644 --- a/app/next-client-app/api/files.ts +++ b/app/next-client-app/api/files.ts @@ -12,6 +12,8 @@ const fetchKeys = { file_id ? `v2/scanreports/${scan_report_id}/rules/downloads/${file_id}/` : `v2/scanreports/${scan_report_id}/download/`, + deleteFile: (scan_report_id: number, file_id: number) => + `v2/scanreports/${scan_report_id}/rules/downloads/${file_id}/`, }; export async function list( @@ -79,3 +81,18 @@ export async function downloadFile( return { success: false, errorMessage: error.message }; } } + +export async function deleteFile( + scan_report_id: number, + file_id: number, +): Promise<{ success: boolean; errorMessage?: string }> { + try { + await request(fetchKeys.deleteFile(scan_report_id, file_id), { + method: "DELETE", + }); + revalidatePath(`/scanreports/${scan_report_id}/downloads`); + return { success: true }; + } catch (error: any) { + return { success: false, errorMessage: error.message }; + } +} diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/downloads/columns.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/downloads/columns.tsx index 9e3a9d057..70ca4c519 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/downloads/columns.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/downloads/columns.tsx @@ -9,6 +9,7 @@ import { saveAs } from "file-saver"; import { Badge } from "@/components/ui/badge"; import { downloadFile } from "@/api/files"; import { toast } from "sonner"; +import DeleteFileDialog from "@/components/files/DeleteFileDialog"; export const columns: ColumnDef[] = [ { @@ -92,4 +93,21 @@ export const columns: ColumnDef[] = [ enableHiding: true, enableSorting: false, }, + { + id: "Delete", + header: ({ column }) => , + cell: ({ row }) => { + const { id, scan_report, name } = row.original; + return ( + + ); + }, + enableHiding: true, + enableSorting: false + } ]; diff --git a/app/next-client-app/components/files/DeleteFileDialog.tsx b/app/next-client-app/components/files/DeleteFileDialog.tsx new file mode 100644 index 000000000..33db55bff --- /dev/null +++ b/app/next-client-app/components/files/DeleteFileDialog.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger +} from "../ui/dialog"; +import { toast } from "sonner"; +import { Button } from "../ui/button"; +import { deleteFile } from "@/api/files"; +import { useRouter } from "next/navigation"; +import { Trash2 } from "lucide-react"; + +interface DeleteFileDialogProps { + fileId: number; + scanReportId: number; + fileName: string; + isOpen?: boolean; + setOpen?: (isOpen: boolean) => void; + needTrigger?: boolean; +} + +const DeleteFileDialog = ({ + fileId, + scanReportId, + fileName, + isOpen, + setOpen = () => {}, + needTrigger = false +}: DeleteFileDialogProps) => { + const router = useRouter(); + + const handleDelete = async () => { + const response = await deleteFile(scanReportId, fileId); + if (response.success) { + toast.success(`File "${fileName}" deleted successfully`); + router.refresh(); + } else { + toast.error( + `Failed to delete file: ${response.errorMessage || "Unknown error"}` + ); + } + setOpen(false); + }; + + return ( + setOpen(false)}> + {needTrigger && ( + + + + )} + + + Delete File + + Are you sure you want to delete "{fileName}"? This action cannot be + undone and will permanently remove the file from storage. + + + + + + + + + + + ); +}; + +export default DeleteFileDialog; diff --git a/docker-compose.yml b/docker-compose.yml index 38439dcbf..80d390dec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -110,6 +110,7 @@ services: - MINIO_ENDPOINT=minio:9000 - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin + - FILE_RETENTION_DAYS=${FILE_RETENTION_DAYS:-30} - WORKER_SERVICE_TYPE=airflow # TODO: update to API v2 when updating Airflow to 3.0.0 - AIRFLOW_BASE_URL=http://airflow-webserver:8080/api/v1/ From 665e65266aefe11e4cf2a2a8dc5ab74905e730d8 Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 8 Oct 2025 18:15:51 +0100 Subject: [PATCH 02/12] Fix File Formatting --- .../0002_add_deleted_at_to_filedownload.py | 22 +++++++++++-------- app/api/files/views.py | 4 +++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py b/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py index d32ad2822..2ddefcf75 100644 --- a/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py +++ b/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py @@ -6,23 +6,27 @@ class Migration(migrations.Migration): dependencies = [ - ('files', '0001_initial'), + ("files", "0001_initial"), ] operations = [ migrations.AddField( - model_name='filedownload', - name='deleted_at', + model_name="filedownload", + name="deleted_at", field=models.DateTimeField(blank=True, null=True), ), migrations.AlterField( - model_name='filedownload', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="filedownload", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( - model_name='filetype', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="filetype", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), ] diff --git a/app/api/files/views.py b/app/api/files/views.py index 7ac7b3df2..c7c80d891 100644 --- a/app/api/files/views.py +++ b/app/api/files/views.py @@ -154,7 +154,9 @@ def post(self, request, *args, **kwargs): file_type_description = ( "JSON V1" if file_type == "application/json_v1" - else "JSON V2" if file_type == "application/json_v2" else "CSV" + else "JSON V2" + if file_type == "application/json_v2" + else "CSV" ) Job.objects.create( scan_report=ScanReport.objects.get(id=scan_report_id), From 6523768afcd5a6ac452dba574a97cf850b9ca6be Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 8 Oct 2025 18:17:09 +0100 Subject: [PATCH 03/12] Fix File Formatting --- app/api/files/migrations/0002_add_deleted_at_to_filedownload.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py b/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py index 2ddefcf75..48f9cb6ef 100644 --- a/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py +++ b/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("files", "0001_initial"), ] From c440c125aa13db16dad86f58b03fff620ae75605 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 9 Oct 2025 17:35:15 +0100 Subject: [PATCH 04/12] Improve Rules download delete Feature --- app/api/api/urls.py | 5 ++ .../0002_add_deleted_at_to_filedownload.py | 31 ------- ...ted_at_filedownload_deleted_by_and_more.py | 36 ++++++++ app/api/files/models.py | 8 ++ app/api/files/views.py | 13 +-- app/api/uv.lock | 4 +- app/next-client-app/api/files.ts | 2 +- .../[id]/downloads/FileDownloadsTable.tsx | 87 +++++++++++++++++++ .../scanreports/[id]/downloads/page.tsx | 10 +-- .../components/files/DeleteFileDialog.tsx | 9 +- app/next-client-app/constants/config.js | 7 ++ app/next-client-app/next-env.d.ts | 1 - docker-compose.yml | 2 +- 13 files changed, 159 insertions(+), 56 deletions(-) delete mode 100644 app/api/files/migrations/0002_add_deleted_at_to_filedownload.py create mode 100644 app/api/files/migrations/0002_filedownload_deleted_at_filedownload_deleted_by_and_more.py create mode 100644 app/next-client-app/app/(protected)/scanreports/[id]/downloads/FileDownloadsTable.tsx diff --git a/app/api/api/urls.py b/app/api/api/urls.py index 161124483..aecf0593e 100644 --- a/app/api/api/urls.py +++ b/app/api/api/urls.py @@ -79,6 +79,11 @@ FileDownloadView.as_view(), name="scan-report-downloads-get", ), + path( + "v2/scanreports//rules/delete//", + FileDownloadView.as_view(), + name="delete-rules-export-file", + ), path( "v2/scanreports//tables//", views.ScanReportTableDetailV2.as_view(), diff --git a/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py b/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py deleted file mode 100644 index 48f9cb6ef..000000000 --- a/app/api/files/migrations/0002_add_deleted_at_to_filedownload.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.2.1 on 2025-10-08 16:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("files", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="filedownload", - name="deleted_at", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name="filedownload", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - migrations.AlterField( - model_name="filetype", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ] diff --git a/app/api/files/migrations/0002_filedownload_deleted_at_filedownload_deleted_by_and_more.py b/app/api/files/migrations/0002_filedownload_deleted_at_filedownload_deleted_by_and_more.py new file mode 100644 index 000000000..8426443be --- /dev/null +++ b/app/api/files/migrations/0002_filedownload_deleted_at_filedownload_deleted_by_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 5.2.1 on 2025-10-09 12:25 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='filedownload', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='filedownload', + name='deleted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_files', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='filedownload', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='filetype', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/app/api/files/models.py b/app/api/files/models.py index 43d9a60db..2844c448a 100644 --- a/app/api/files/models.py +++ b/app/api/files/models.py @@ -41,6 +41,7 @@ class FileDownload(BaseModel): file_type (FileType): The type of the file. file_url (str, optional): The URL for downloading the file. Defaults to None. deleted_at (datetime, optional): Timestamp when the file was deleted. Defaults to None. + deleted_by (User, optional): The user who deleted the file. Defaults to None. Returns: str: The name of the file. @@ -57,6 +58,13 @@ class FileDownload(BaseModel): file_type = models.ForeignKey(FileType, on_delete=models.CASCADE) file_url = models.CharField(max_length=500, null=True, blank=True) deleted_at = models.DateTimeField(null=True, blank=True) + deleted_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name="deleted_files", + ) def __str__(self): return str(self.name) diff --git a/app/api/files/views.py b/app/api/files/views.py index c7c80d891..acf664247 100644 --- a/app/api/files/views.py +++ b/app/api/files/views.py @@ -66,14 +66,10 @@ def get_queryset(self): scan_report_id = self.kwargs["scanreport_pk"] scan_report = get_object_or_404(ScanReport, pk=scan_report_id) - # Hide files older than retention period (default: 30 days) - retention_days = int(os.getenv("FILE_RETENTION_DAYS", "30")) - cutoff_date = timezone.now() - timedelta(days=retention_days) - - # Only show recent, non-deleted files + # Return all non-deleted files + # Frontend will handle hiding files older than FILE_RETENTION_DAYS return FileDownload.objects.filter( scan_report=scan_report, - created_at__gte=cutoff_date, deleted_at__isnull=True, ) @@ -154,9 +150,7 @@ def post(self, request, *args, **kwargs): file_type_description = ( "JSON V1" if file_type == "application/json_v1" - else "JSON V2" - if file_type == "application/json_v2" - else "CSV" + else "JSON V2" if file_type == "application/json_v2" else "CSV" ) Job.objects.create( scan_report=ScanReport.objects.get(id=scan_report_id), @@ -201,6 +195,7 @@ def delete(self, request, *args, **kwargs): # Mark as deleted but keep record for audit file_download.deleted_at = timezone.now() + file_download.deleted_by = request.user file_download.save() return HttpResponse(status=204) diff --git a/app/api/uv.lock b/app/api/uv.lock index 5a36a41a1..e45a02f81 100644 --- a/app/api/uv.lock +++ b/app/api/uv.lock @@ -1,10 +1,10 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11, <4" [[package]] name = "api" -version = "4.2.2" +version = "4.2.4" source = { editable = "." } dependencies = [ { name = "azure-common" }, diff --git a/app/next-client-app/api/files.ts b/app/next-client-app/api/files.ts index 7c3075088..4455e16c2 100644 --- a/app/next-client-app/api/files.ts +++ b/app/next-client-app/api/files.ts @@ -13,7 +13,7 @@ const fetchKeys = { ? `v2/scanreports/${scan_report_id}/rules/downloads/${file_id}/` : `v2/scanreports/${scan_report_id}/download/`, deleteFile: (scan_report_id: number, file_id: number) => - `v2/scanreports/${scan_report_id}/rules/downloads/${file_id}/`, + `v2/scanreports/${scan_report_id}/rules/delete/${file_id}/`, }; export async function list( diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/downloads/FileDownloadsTable.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/downloads/FileDownloadsTable.tsx new file mode 100644 index 000000000..e97be8834 --- /dev/null +++ b/app/next-client-app/app/(protected)/scanreports/[id]/downloads/FileDownloadsTable.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { ColumnDef } from "@tanstack/react-table"; +import { DataTable } from "@/components/data-table"; +import { Button } from "@/components/ui/button"; +import { Eye, EyeOff } from "lucide-react"; +import { FILE_RETENTION_DAYS } from "@/constants/config"; + +interface FileDownloadsTableProps { + data: FileDownload[]; + count: number; + columns: ColumnDef[]; + Filter?: JSX.Element; + defaultPageSize: 10 | 20 | 30 | 40 | 50; +} + +export function FileDownloadsTable({ + data, + count, + columns, + Filter, + defaultPageSize +}: FileDownloadsTableProps) { + const [showOldFiles, setShowOldFiles] = useState(false); + + const cutoffDate = useMemo(() => { + const millisecondsPerDay = 24 * 60 * 60 * 1000; + return new Date(Date.now() - FILE_RETENTION_DAYS * millisecondsPerDay); + }, []); + + const { recentFiles, oldFiles } = useMemo(() => { + const recent: FileDownload[] = []; + const old: FileDownload[] = []; + + data.forEach((file) => { + const fileDate = new Date(file.created_at); + if (fileDate >= cutoffDate) { + recent.push(file); + } else { + old.push(file); + } + }); + + return { recentFiles: recent, oldFiles: old }; + }, [data, cutoffDate]); + + const displayedFiles = showOldFiles ? data : recentFiles; + const displayedCount = showOldFiles ? count : recentFiles.length; + + const filterComponent = ( +
+ {Filter} + {oldFiles.length > 0 && ( + + )} +
+ ); + + return ( + + ); +} diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/downloads/page.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/downloads/page.tsx index fa04c4030..c669e46ba 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/downloads/page.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/downloads/page.tsx @@ -1,9 +1,9 @@ import { objToQuery } from "@/lib/client-utils"; -import { DataTable } from "@/components/data-table"; import { list } from "@/api/files"; import { columns } from "./columns"; import { getJobs } from "@/api/scanreports"; import { DownloadStatus } from "./download-status"; +import { FileDownloadsTable } from "./FileDownloadsTable"; interface DownloadsProps { params: Promise<{ @@ -16,14 +16,12 @@ export default async function Downloads(props: DownloadsProps) { const searchParams = await props.searchParams; const params = await props.params; - const { - id - } = params; + const { id } = params; const defaultPageSize = 20; const defaultParams = { p: 1, - page_size: defaultPageSize, + page_size: defaultPageSize }; const combinedParams = { ...defaultParams, ...searchParams }; const query = objToQuery(combinedParams); @@ -39,7 +37,7 @@ export default async function Downloads(props: DownloadsProps) { return (
{filesList && ( - Delete File - - Are you sure you want to delete "{fileName}"? This action cannot be - undone and will permanently remove the file from storage. - +

+ Are you sure you want to delete "{fileName}"? This action cannot be + undone and will permanently remove the file from storage. +