diff --git a/backend/apps/owasp/Makefile b/backend/apps/owasp/Makefile index cecfb2a1dd..65eae92517 100644 --- a/backend/apps/owasp/Makefile +++ b/backend/apps/owasp/Makefile @@ -18,14 +18,6 @@ owasp-enrich-projects: @echo "Enriching OWASP projects" @CMD="python manage.py owasp_enrich_projects" $(MAKE) exec-backend-command -owasp-generate-project-health-metrics-overview-pdf: - @echo "Generating OWASP project health metrics overview PDF" - @CMD="python manage.py owasp_generate_project_health_metrics_overview_pdf" $(MAKE) exec-backend-command - -owasp-generate-project-health-metrics-pdf: - @echo "Generating OWASP project health metrics PDF for project: $(project_key)" - @CMD="python manage.py owasp_generate_project_health_metrics_pdf --project-key=$(project_key)" $(MAKE) exec-backend-command - owasp-process-snapshots: @echo "Processing OWASP snapshots" @CMD="python manage.py owasp_process_snapshots" $(MAKE) exec-backend-command diff --git a/backend/apps/owasp/api/internal/views/project_health_metrics.py b/backend/apps/owasp/api/internal/views/project_health_metrics.py index 694d074741..a4e97003bb 100644 --- a/backend/apps/owasp/api/internal/views/project_health_metrics.py +++ b/backend/apps/owasp/api/internal/views/project_health_metrics.py @@ -1,16 +1,36 @@ """Views for OWASP project health metrics.""" -from django.http import FileResponse +from django.http import FileResponse, Http404 from django.views.decorators.http import require_GET -from apps.owasp.utils.pdf import generate_metrics_overview_pdf +from apps.owasp.models.project import Project +from apps.owasp.models.project_health_metrics import ProjectHealthMetrics +from apps.owasp.utils.pdf import generate_latest_metrics_pdf, generate_metrics_overview_pdf @require_GET -def generate_overview_pdf(_request): +def generate_overview_pdf(request): # noqa: ARG001 """Generate a PDF overview of OWASP project health metrics.""" return FileResponse( - generate_metrics_overview_pdf(), + generate_metrics_overview_pdf(ProjectHealthMetrics.get_stats()), as_attachment=True, filename="owasp_project_health_metrics_overview.pdf", ) + + +@require_GET +def generate_project_health_metrics_pdf(request, project_key: str): # noqa: ARG001 + """Generate and return a PDF report of project health metrics.""" + if not (project := Project.objects.filter(key=f"www-project-{project_key}").first()): + raise Http404 + + if pdf := generate_latest_metrics_pdf( + ProjectHealthMetrics.get_latest_health_metrics().filter(project=project).first() + ): + return FileResponse( + pdf, + as_attachment=True, + filename=f"{project_key}_health_metrics_report.pdf", + ) + + raise Http404 diff --git a/backend/apps/owasp/api/internal/views/urls.py b/backend/apps/owasp/api/internal/views/urls.py index 64df900847..b9fa5430f9 100644 --- a/backend/apps/owasp/api/internal/views/urls.py +++ b/backend/apps/owasp/api/internal/views/urls.py @@ -2,7 +2,10 @@ from django.urls import path -from apps.owasp.api.internal.views.project_health_metrics import generate_overview_pdf +from apps.owasp.api.internal.views.project_health_metrics import ( + generate_overview_pdf, + generate_project_health_metrics_pdf, +) urlpatterns = [ path( @@ -10,4 +13,9 @@ generate_overview_pdf, name="project_health_metrics_overview_pdf", ), + path( + "project-health-metrics//pdf/", + generate_project_health_metrics_pdf, + name="project_health_metrics_pdf", + ), ] diff --git a/backend/apps/owasp/management/commands/owasp_generate_project_health_metrics_pdf.py b/backend/apps/owasp/management/commands/owasp_generate_project_health_metrics_pdf.py deleted file mode 100644 index a3da95e2f1..0000000000 --- a/backend/apps/owasp/management/commands/owasp_generate_project_health_metrics_pdf.py +++ /dev/null @@ -1,104 +0,0 @@ -"""A command to generate a PDF report of health metrics for an OWASP project.""" - -from io import BytesIO -from pathlib import Path - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.utils import timezone -from reportlab.lib.pagesizes import letter -from reportlab.pdfgen import canvas -from reportlab.platypus import Table, TableStyle - -from apps.owasp.models.project_health_metrics import ProjectHealthMetrics - - -class Command(BaseCommand): - """Generate a PDF report of project health metrics.""" - - help = "Generate a PDF report of project health metrics for an OWASP project." - - def add_arguments(self, parser): - """Add command line arguments.""" - parser.add_argument( - "--project-key", type=str, help="The key of the OWASP project to generate metrics for." - ) - - def handle(self, *args, **options): - project_key = options["project_key"] - metrics = ( - ProjectHealthMetrics.get_latest_health_metrics() - .filter(project__key=f"www-project-{project_key}") - .first() - ) - - if not metrics: - self.stdout.write( - self.style.ERROR(f"No health metrics found for project '{project_key}'") - ) - return - - buffer = BytesIO() - pdf = canvas.Canvas(buffer, pagesize=letter) - pdf.setTitle(f"Health Metrics Report for {metrics.project.name}") - pdf.setFont("Helvetica", 12) - pdf.drawCentredString(300, 700, f"Health Metrics Report for {metrics.project.name}") - pdf.drawCentredString(300, 680, f"Health Score: {metrics.score:.2f}") - table_data = [ - ["Metric", "Value"], - ["Project Age (days)", metrics.age_days], - ["Last Commit (days)", metrics.last_commit_days], - ["Last Commit Requirement (days)", metrics.last_commit_days_requirement], - ["Last Pull Request (days)", metrics.last_pull_request_days], - ["Last Release (days)", metrics.last_release_days], - ["Last Release Requirement (days)", metrics.last_release_days_requirement], - ["OWASP Page Last Update (days)", metrics.owasp_page_last_update_days], - ["Open Issues", metrics.open_issues_count], - ["Total Issues", metrics.total_issues_count], - ["Open Pull Requests", metrics.open_pull_requests_count], - ["Total Pull Requests", metrics.total_pull_requests_count], - ["Recent Releases", metrics.recent_releases_count], - ["Total Releases", metrics.total_releases_count], - ["Forks", metrics.forks_count], - ["Stars", metrics.stars_count], - ["Contributors", metrics.contributors_count], - ["Unassigned Issues", metrics.unassigned_issues_count], - ["Unanswered Issues", metrics.unanswered_issues_count], - [ - "Has funding policy issues", - "No" if metrics.is_funding_requirements_compliant else "Yes", - ], - [ - "Has leadership policy issues", - "No" if metrics.is_leader_requirements_compliant else "Yes", - ], - ] - table = Table(table_data, colWidths="*") - table.setStyle( - TableStyle( - [ - ("BACKGROUND", (0, 0), (-1, 0), "lightgrey"), - ("TEXTCOLOR", (0, 0), (-1, 0), "black"), - ("ALIGN", (0, 0), (-1, -1), "CENTER"), - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("BOTTOMPADDING", (0, 0), (-1, 0), 5), - ("BACKGROUND", (0, 1), (-1, -1), "white"), - ] - ) - ) - table.wrapOn(pdf, 500, 300) - table.drawOn(pdf, 50, 280) - pdf.drawCentredString( - 300, 100, f"Report Generated on: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - pdf.showPage() - pdf.save() - - pdf_path = Path(settings.BASE_DIR) / "reports" / f"{project_key}_health_metrics_report.pdf" - - pdf_path.parent.mkdir(parents=True, exist_ok=True) - pdf_path.write_bytes(buffer.getvalue()) - buffer.close() - self.stdout.write( - self.style.SUCCESS(f"Health metrics PDF report for {metrics.project.name} generated.") - ) diff --git a/backend/apps/owasp/utils/pdf.py b/backend/apps/owasp/utils/pdf.py index 5362b98754..b69dd5d9fa 100644 --- a/backend/apps/owasp/utils/pdf.py +++ b/backend/apps/owasp/utils/pdf.py @@ -3,21 +3,43 @@ from io import BytesIO from django.utils import timezone +from reportlab.lib.pagesizes import letter from reportlab.pdfgen.canvas import Canvas from reportlab.platypus import Table, TableStyle +from apps.owasp.api.internal.nodes.project_health_stats import ProjectHealthStatsNode from apps.owasp.models.project_health_metrics import ProjectHealthMetrics -def generate_metrics_overview_pdf() -> BytesIO: +def create_table(data, col_widths="*"): + """Create a styled table for PDF generation.""" + return Table( + data, + colWidths=col_widths, + style=TableStyle( + ( + ("BACKGROUND", (0, 0), (-1, 0), "#f2f2f2"), + ("TEXTCOLOR", (0, 0), (-1, 0), "#000000"), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("FONTNAME", (0, 0), (-1, -1), "Helvetica"), + ("BOTTOMPADDING", (0, 0), (-1, -1), 12), + ("BACKGROUND", (0, 1), (-1, -1), "#ffffff"), + ("GRID", (0, 0), (-1, -1), 1, "#dddddd"), + ) + ), + ) + + +def generate_metrics_overview_pdf(metrics_stats: ProjectHealthStatsNode) -> BytesIO: """Generate a PDF overview of project health metrics. + Args: + metrics_stats: The project health stats node. + Returns: BytesIO: PDF content as bytes. """ - metrics_stats = ProjectHealthMetrics.get_stats() - buffer = BytesIO() canvas = Canvas(buffer) canvas.setFont("Helvetica", 12) @@ -52,22 +74,9 @@ def generate_metrics_overview_pdf() -> BytesIO: ), ) - table = Table( - table_data, - colWidths="*", - style=TableStyle( - ( - ("BACKGROUND", (0, 0), (-1, 0), "lightgrey"), - ("TEXTCOLOR", (0, 0), (-1, 0), "black"), - ("ALIGN", (0, 0), (-1, -1), "CENTER"), - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("BOTTOMPADDING", (0, 0), (-1, 0), 5), - ("BACKGROUND", (0, 1), (-1, -1), "white"), - ) - ), - ) - table.wrapOn(canvas, 400, 600) - table.drawOn(canvas, 100, 570) + table = create_table(table_data) + table.wrapOn(canvas, 400, 500) + table.drawOn(canvas, 100, 470) canvas.drawCentredString( 300, 100, f"Generated on: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}" ) @@ -75,3 +84,93 @@ def generate_metrics_overview_pdf() -> BytesIO: canvas.save() buffer.seek(0) return buffer + + +def generate_latest_metrics_pdf(metrics: ProjectHealthMetrics) -> BytesIO | None: + """Generate a PDF report of the latest health metrics for a project. + + Args: + metrics (ProjectHealthMetrics): The project health metrics. + + Returns: + BytesIO: A buffer containing the generated PDF report. + + """ + if not metrics: + return None + + buffer = BytesIO() + pdf = Canvas(buffer, pagesize=letter) + pdf.setTitle(f"Health Metrics Report for {metrics.project.name}") + pdf.setFont("Helvetica", 12) + pdf.drawCentredString(300, 700, f"Health Metrics Report for {metrics.project.name}") + pdf.drawCentredString( + 300, 680, f"Health Score: {metrics.score:.2f}" if metrics.score is not None else "N/A" + ) + table_data = ( + ("Metric", "Value"), + ("Project Age", f"{metrics.age_days}/{metrics.age_days_requirement} days"), + ( + "Last Commit", + f"{metrics.last_commit_days}/{metrics.last_commit_days_requirement} days", + ), + ( + "Last Pull Request", + # To bypass ruff long line error + "/".join( + [ + str(metrics.last_pull_request_days), + f"{metrics.last_pull_request_days_requirement} days", + ] + ), + ), + ( + "Last Release", + f"{metrics.last_release_days}/{metrics.last_release_days_requirement} days", + ), + ( + "OWASP Page Last Update", + # To bypass ruff long line error + "/".join( + [ + str(metrics.owasp_page_last_update_days), + f"{metrics.owasp_page_last_update_days_requirement} days", + ] + ), + ), + ("Open/Total Issues", f"{metrics.open_issues_count}/{metrics.total_issues_count}"), + ( + "Open/Total Pull Requests", + f"{metrics.open_pull_requests_count}/{metrics.total_pull_requests_count}", + ), + ( + "Recent/Total Releases", + f"{metrics.recent_releases_count}/{metrics.total_releases_count}", + ), + ("Forks", metrics.forks_count), + ("Stars", metrics.stars_count), + ( + "Unassigned/Unanswered Issues", + f"{metrics.unassigned_issues_count}/{metrics.unanswered_issues_count}", + ), + ("Contributors", metrics.contributors_count), + ( + "Has funding policy issues", + "No" if metrics.is_funding_requirements_compliant else "Yes", + ), + ( + "Has leadership policy issues", + "No" if metrics.is_leader_requirements_compliant else "Yes", + ), + ) + table = create_table(table_data) + table.wrapOn(pdf, 500, 250) + table.drawOn(pdf, 50, 220) + pdf.drawCentredString( + 300, 100, f"Report Generated on: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + pdf.showPage() + pdf.save() + buffer.seek(0) + + return buffer diff --git a/backend/tests/apps/owasp/management/commands/owasp_generate_project_health_metrics_test.py b/backend/tests/apps/owasp/management/commands/owasp_generate_project_health_metrics_test.py deleted file mode 100644 index e2937ca6f2..0000000000 --- a/backend/tests/apps/owasp/management/commands/owasp_generate_project_health_metrics_test.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import MagicMock, patch - -from django.conf import settings -from django.core.management import call_command -from reportlab.lib.pagesizes import letter - - -class TestOwaspGenerateProjectHealthMetricsPdf: - @patch( - "apps.owasp.models.project_health_metrics.ProjectHealthMetrics.get_latest_health_metrics", - ) - @patch("reportlab.pdfgen.canvas.Canvas") - @patch("reportlab.platypus.Table") - @patch("reportlab.platypus.TableStyle") - @patch("io.BytesIO") - @patch("pathlib.Path") - def test_handle( - self, - mock_path, - mock_bytes_io, - mock_table_style, - mock_table, - mock_canvas, - mock_get_latest_health_metrics, - ): - metrics = MagicMock( - age_days=30, - contributors_count=20, - forks_count=15, - is_funding_requirements_compliant=True, - is_leader_requirements_compliant=True, - last_commit_days=5, - last_commit_days_requirement=7, - last_pull_request_days=2, - last_release_days=10, - last_release_days_requirement=14, - open_issues_count=10, - open_pull_requests_count=3, - owasp_page_last_update_days=20, - project=MagicMock(name="Test Project", key="www-project-test"), - recent_releases_count=1, - score=85.0, - stars_count=100, - total_issues_count=20, - total_pull_requests_count=8, - total_releases_count=10, - unanswered_issues_count=2, - unassigned_issues_count=5, - ) - mock_get_latest_health_metrics.return_value.filter.return_value.first.return_value = ( - metrics - ) - call_command("owasp_generate_project_health_metrics_pdf", project_key="test") - mock_bytes_io.assert_called_once() - mock_canvas.assert_called_once_with(mock_bytes_io.return_value, pagesize=letter) - canvas = mock_canvas.return_value - table_data = [ - ["Metric", "Value"], - ["Project Age (days)", metrics.age_days], - ["Last Commit (days)", metrics.last_commit_days], - ["Last Commit Requirement (days)", metrics.last_commit_days_requirement], - ["Last Pull Request (days)", metrics.last_pull_request_days], - ["Last Release (days)", metrics.last_release_days], - ["Last Release Requirement (days)", metrics.last_release_days_requirement], - ["OWASP Page Last Update (days)", metrics.owasp_page_last_update_days], - ["Open Issues", metrics.open_issues_count], - ["Total Issues", metrics.total_issues_count], - ["Open Pull Requests", metrics.open_pull_requests_count], - ["Total Pull Requests", metrics.total_pull_requests_count], - ["Recent Releases", metrics.recent_releases_count], - ["Total Releases", metrics.total_releases_count], - ["Forks", metrics.forks_count], - ["Stars", metrics.stars_count], - ["Contributors", metrics.contributors_count], - ["Unassigned Issues", metrics.unassigned_issues_count], - ["Unanswered Issues", metrics.unanswered_issues_count], - ["Has funding policy issues", "No"], - [ - "Has leadership policy issues", - "No", - ], - ] - mock_table.assert_called_once_with(table_data, colWidths="*") - mock_table_style.assert_called_once() - - mock_table.return_value.wrapOn.assert_called_once_with(canvas, 500, 300) - mock_table.return_value.drawOn.assert_called_once_with(canvas, 50, 280) - canvas.showPage.assert_called_once() - canvas.save.assert_called_once() - mock_path.assert_called_once_with(settings.BASE_DIR) - mock_bytes_io.return_value.close.assert_called_once() diff --git a/backend/tests/apps/owasp/utils/pdf_test.py b/backend/tests/apps/owasp/utils/pdf_test.py index e83ac31eda..8435807ea5 100644 --- a/backend/tests/apps/owasp/utils/pdf_test.py +++ b/backend/tests/apps/owasp/utils/pdf_test.py @@ -1,9 +1,11 @@ """Test cases for OWASP project health metrics pdf generation.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +from reportlab.lib.pagesizes import letter from apps.owasp.api.internal.nodes.project_health_stats import ProjectHealthStatsNode -from apps.owasp.utils.pdf import generate_metrics_overview_pdf +from apps.owasp.utils.pdf import generate_latest_metrics_pdf, generate_metrics_overview_pdf class TestGenerateMetricsPDF: @@ -63,7 +65,7 @@ def test_generate_overview_pdf( ), ) mock_get_stats.return_value = metrics_stats - generate_metrics_overview_pdf() + generate_metrics_overview_pdf(metrics_stats) mock_bytes_io.assert_called_once() mock_canvas.assert_called_once_with(mock_bytes_io.return_value) canvas = mock_canvas.return_value @@ -71,7 +73,119 @@ def test_generate_overview_pdf( table_data, colWidths="*", style=mock_table_style.return_value ) mock_table_style.assert_called_once() - mock_table.return_value.wrapOn.assert_called_once_with(canvas, 400, 600) - mock_table.return_value.drawOn.assert_called_once_with(canvas, 100, 570) + mock_table.return_value.wrapOn.assert_called_once_with(canvas, 400, 500) + mock_table.return_value.drawOn.assert_called_once_with(canvas, 100, 470) + canvas.showPage.assert_called_once() + canvas.save.assert_called_once() + + @patch( + "apps.owasp.utils.pdf.ProjectHealthMetrics.get_latest_health_metrics", + ) + @patch("apps.owasp.utils.pdf.Canvas") + @patch("apps.owasp.utils.pdf.Table") + @patch("apps.owasp.utils.pdf.TableStyle") + @patch("apps.owasp.utils.pdf.BytesIO") + def test_generate_detailed_pdf( + self, + mock_bytes_io, + mock_table_style, + mock_table, + mock_canvas, + mock_get_latest_health_metrics, + ): + metrics = MagicMock( + age_days=30, + age_days_requirement=365, + contributors_count=20, + forks_count=15, + is_funding_requirements_compliant=True, + is_leader_requirements_compliant=True, + last_commit_days=5, + last_commit_days_requirement=7, + last_pull_request_days=2, + last_pull_request_days_requirement=30, + last_release_days=10, + last_release_days_requirement=14, + open_issues_count=10, + open_pull_requests_count=3, + owasp_page_last_update_days=20, + owasp_page_last_update_days_requirement=90, + project=MagicMock(name="Test Project", key="www-project-test"), + recent_releases_count=1, + score=85.0, + stars_count=100, + total_issues_count=20, + total_pull_requests_count=8, + total_releases_count=10, + unanswered_issues_count=2, + unassigned_issues_count=5, + ) + generate_latest_metrics_pdf(metrics) + mock_bytes_io.assert_called_once() + mock_canvas.assert_called_once_with(mock_bytes_io.return_value, pagesize=letter) + canvas = mock_canvas.return_value + table_data = ( + ("Metric", "Value"), + ("Project Age", f"{metrics.age_days}/{metrics.age_days_requirement} days"), + ( + "Last Commit", + f"{metrics.last_commit_days}/{metrics.last_commit_days_requirement} days", + ), + ( + "Last Pull Request", + # To bypass ruff long line error + "/".join( + [ + str(metrics.last_pull_request_days), + f"{metrics.last_pull_request_days_requirement} days", + ] + ), + ), + ( + "Last Release", + f"{metrics.last_release_days}/{metrics.last_release_days_requirement} days", + ), + ( + "OWASP Page Last Update", + # To bypass ruff long line error + "/".join( + [ + str(metrics.owasp_page_last_update_days), + f"{metrics.owasp_page_last_update_days_requirement} days", + ] + ), + ), + ("Open/Total Issues", f"{metrics.open_issues_count}/{metrics.total_issues_count}"), + ( + "Open/Total Pull Requests", + f"{metrics.open_pull_requests_count}/{metrics.total_pull_requests_count}", + ), + ( + "Recent/Total Releases", + f"{metrics.recent_releases_count}/{metrics.total_releases_count}", + ), + ("Forks", metrics.forks_count), + ("Stars", metrics.stars_count), + ( + "Unassigned/Unanswered Issues", + f"{metrics.unassigned_issues_count}/{metrics.unanswered_issues_count}", + ), + ("Contributors", metrics.contributors_count), + ( + "Has funding policy issues", + "No" if metrics.is_funding_requirements_compliant else "Yes", + ), + ( + "Has leadership policy issues", + "No" if metrics.is_leader_requirements_compliant else "Yes", + ), + ) + mock_table.assert_called_once_with( + table_data, colWidths="*", style=mock_table_style.return_value + ) + mock_table_style.assert_called_once() + + mock_table.return_value.wrapOn.assert_called_once_with(canvas, 500, 250) + mock_table.return_value.drawOn.assert_called_once_with(canvas, 50, 220) canvas.showPage.assert_called_once() canvas.save.assert_called_once() diff --git a/frontend/src/app/projects/dashboard/metrics/[projectKey]/page.tsx b/frontend/src/app/projects/dashboard/metrics/[projectKey]/page.tsx index f038b15a1d..eed4ff8c55 100644 --- a/frontend/src/app/projects/dashboard/metrics/[projectKey]/page.tsx +++ b/frontend/src/app/projects/dashboard/metrics/[projectKey]/page.tsx @@ -21,7 +21,9 @@ import BarChart from 'components/BarChart' import GeneralCompliantComponent from 'components/GeneralCompliantComponent' import LineChart from 'components/LineChart' import LoadingSpinner from 'components/LoadingSpinner' +import MetricsPDFButton from 'components/MetricsPDFButton' import MetricsScoreCircle from 'components/MetricsScoreCircle' + const ProjectHealthMetricsDetails: FC = () => { const { projectKey } = useParams() const [metricsList, setMetricsList] = useState() @@ -62,7 +64,13 @@ const ProjectHealthMetricsDetails: FC = () => { {metricsList && metricsLatest ? ( <>
-

{metricsLatest.projectName}

+
+

{metricsLatest.projectName}

+ +
{ @@ -101,20 +98,7 @@ const ProjectsDashboardPage: FC = () => { <>

Project Health Dashboard Overview

- - await fetchMetricsOverviewPDF()} - /> - +
{projectsCardsItems.map((item) => ( diff --git a/frontend/src/components/MetricsPDFButton.tsx b/frontend/src/components/MetricsPDFButton.tsx new file mode 100644 index 0000000000..1a1db63d0d --- /dev/null +++ b/frontend/src/components/MetricsPDFButton.tsx @@ -0,0 +1,31 @@ +'use client' + +import { faFileArrowDown } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Tooltip } from '@heroui/tooltip' +import { FC } from 'react' +import { fetchMetricsPDF } from 'server/fetchMetricsPDF' + +const MetricsPDFButton: FC<{ + path: string + fileName: string +}> = ({ path, fileName }) => { + return ( + + await fetchMetricsPDF(path, fileName)} + /> + + ) +} + +export default MetricsPDFButton diff --git a/frontend/src/server/fetchMetricsOverviewPDF.ts b/frontend/src/server/fetchMetricsOverviewPDF.ts deleted file mode 100644 index 1c258919c6..0000000000 --- a/frontend/src/server/fetchMetricsOverviewPDF.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { handleAppError } from 'app/global-error' -import { API_URL } from 'utils/credentials' - -export const fetchMetricsOverviewPDF = async (): Promise => { - try { - const response = await fetch(`${API_URL}/owasp/project-health-metrics/overview/pdf`, { - method: 'GET', - headers: { - accept: 'application/pdf', - }, - }) - - if (!response.ok) { - const message = `Failed to fetch metrics overview PDF: ${response.status} ${response.statusText}` - handleAppError(new Error(message)) - } - - const pdfBlob = await response.blob() - if (pdfBlob.size === 0) { - handleAppError(new Error('PDF blob is empty or undefined')) - } - const pdfUrl = window.URL.createObjectURL(pdfBlob) - const link = document.createElement('a') - link.href = pdfUrl - const date = new Date().toISOString().split('T')[0].replaceAll('-', '_') - const fileName = `owasp_metrics_overview_${date}.pdf` - link.setAttribute('download', fileName) - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - window.URL.revokeObjectURL(pdfUrl) - } catch (error) { - handleAppError( - error instanceof Error - ? error - : new Error('An unexpected error occurred while fetching the PDF') - ) - } -} diff --git a/frontend/src/server/fetchMetricsPDF.ts b/frontend/src/server/fetchMetricsPDF.ts new file mode 100644 index 0000000000..cf7e2527ab --- /dev/null +++ b/frontend/src/server/fetchMetricsPDF.ts @@ -0,0 +1,32 @@ +import { handleAppError } from 'app/global-error' +import { API_URL } from 'utils/credentials' + +export const fetchMetricsPDF = async (path: string, fileName: string): Promise => { + const response = await fetch(`${API_URL}owasp/project-health-metrics/${path}`, { + method: 'GET', + headers: { + accept: 'application/pdf', + }, + }) + if (!response.ok) { + handleAppError(new Error(`Failed to fetch PDF: ${response.status} ${response.statusText}`)) + return + } + try { + const blob = await response.blob() + if (!blob) { + handleAppError(new Error('No data received from the server')) + return + } + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', `${fileName}-${new Date().toISOString().split('T')[0]}.pdf`) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + } catch (error) { + handleAppError(error) + } +}