Skip to content

Implement charts for health metrics #1633

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

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4a21f5d
Add apexchart
ahmedxgouda Jun 17, 2025
81baa6a
Add inital implementaion for the charts
ahmedxgouda Jun 17, 2025
22b36f7
Generalize chart component and implement the unanswered Issues Trend …
ahmedxgouda Jun 17, 2025
1db50ba
Add remaining line charts and initial support for the radial bar charts
ahmedxgouda Jun 18, 2025
7194a0b
Improve radial bars and add generalization
ahmedxgouda Jun 18, 2025
34e4b8f
Apply check-spelling
ahmedxgouda Jun 18, 2025
7aa9581
Add colors for Radial Chart
ahmedxgouda Jun 18, 2025
97cf9bd
Update the mock of project data in tests
ahmedxgouda Jun 18, 2025
65a61f6
Update frontend unit tests
ahmedxgouda Jun 18, 2025
88768dc
Apply check-spelling
ahmedxgouda Jun 18, 2025
132ba71
Update frontend with suggestions
ahmedxgouda Jun 20, 2025
76c3c72
Add strawberry fields for release and commit days requirements
ahmedxgouda Jun 20, 2025
b7a2430
Show gradient radial chart based on requirements
ahmedxgouda Jun 20, 2025
79106bb
Merge branch 'main' into dashboard/frontend-implementation
ahmedxgouda Jun 20, 2025
eb335ae
Merge branch 'main' into dashboard/frontend-implementation
ahmedxgouda Jun 20, 2025
d905a12
Update GradiantRadialChart
ahmedxgouda Jun 20, 2025
edbc445
Display score
ahmedxgouda Jun 20, 2025
4be8514
Update frontend tests
ahmedxgouda Jun 20, 2025
d2dfbdc
Merge branch 'main' into dashboard/frontend-implementation
ahmedxgouda Jun 20, 2025
4765aa5
Apply coderabbitai suggestions
ahmedxgouda Jun 20, 2025
a206505
Update Radial Chart styling to use static background
kasya Jun 21, 2025
b225179
Clean up
kasya Jun 21, 2025
0f253ef
Merge branch 'main' into dashboard/frontend-implementation
kasya Jun 21, 2025
9c34bc2
Apply suggestions
ahmedxgouda Jun 21, 2025
633b9a6
Update tooltip theme in LineChart
ahmedxgouda Jun 21, 2025
d6cbc7a
Update gradient radial chart
ahmedxgouda Jun 21, 2025
4850a0c
Merge branch 'main' into dashboard/frontend-implementation
ahmedxgouda Jun 21, 2025
af37662
Apply coderabbitai suggestion
ahmedxgouda Jun 21, 2025
972b2bf
Update radial bar
ahmedxgouda Jun 21, 2025
2e93833
Merge branch 'main' into dashboard/frontend-implementation
ahmedxgouda Jun 21, 2025
b921ad1
Merge branch 'main' into dashboard/frontend-implementation
ahmedxgouda Jun 21, 2025
b19b7cb
Merge branch 'main' into dashboard/frontend-implementation
kasya Jun 21, 2025
71bb30e
Fix issues with extra space shown in Firefox
kasya Jun 21, 2025
cca414a
Merge branch 'dashboard/frontend-implementation' of github.com:ahmedx…
kasya Jun 21, 2025
49d054a
Merge branch 'main' into dashboard/frontend-implementation
kasya Jun 21, 2025
6d01dbf
Merge branch 'main' into dashboard/frontend-implementation
ahmedxgouda Jun 21, 2025
441e029
Update backend resolvers
ahmedxgouda Jun 21, 2025
4b20d03
Update backend tests
ahmedxgouda Jun 21, 2025
de264f7
Apply suggestions
ahmedxgouda Jun 22, 2025
f5c0a13
Fix CardDetailsPage
ahmedxgouda Jun 22, 2025
2a32bb1
Fix tests
ahmedxgouda Jun 22, 2025
a2233b4
Update tests
ahmedxgouda Jun 22, 2025
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
17 changes: 17 additions & 0 deletions backend/apps/owasp/graphql/nodes/project_health_metrics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""OWASP Project Health Metrics Node."""

from datetime import datetime

import strawberry
import strawberry_django

Expand Down Expand Up @@ -30,11 +32,21 @@ def age_days(self) -> int:
"""Resolve project age in days."""
return self.age_days

@strawberry.field
def created_at(self) -> datetime:
"""Resolve metrics creation date."""
return self.nest_created_at

@strawberry.field
def last_commit_days(self) -> int:
"""Resolve last commit age in days."""
return self.last_commit_days

@strawberry.field
def last_commit_days_requirement(self) -> int:
"""Resolve last commit age requirement in days."""
return self.last_commit_days_requirement
Comment on lines +45 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical issue: Incorrect resolver implementation.

This new field follows the same problematic pattern as the existing resolvers, returning the method object instead of the actual integer value.

Apply this fix:

-    @strawberry.field
-    def last_commit_days_requirement(self) -> int:
-        """Resolve last commit age requirement in days."""
-        return self.last_commit_days_requirement
+    @strawberry.field(name="lastCommitDaysRequirement")
+    def resolve_last_commit_days_requirement(self) -> int:
+        """Last commit age requirement in days."""
+        from typing import cast
+        return cast(ProjectHealthMetrics, self).last_commit_days_requirement
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@strawberry.field
def last_commit_days_requirement(self) -> int:
"""Resolve last commit age requirement in days."""
return self.last_commit_days_requirement
@strawberry.field(name="lastCommitDaysRequirement")
def resolve_last_commit_days_requirement(self) -> int:
"""Last commit age requirement in days."""
from typing import cast
return cast(ProjectHealthMetrics, self).last_commit_days_requirement
🤖 Prompt for AI Agents
In backend/apps/owasp/graphql/nodes/project_health_metrics.py around lines 45 to
48, the resolver method last_commit_days_requirement incorrectly returns the
method object itself instead of the integer value. To fix this, rename the
method to avoid name collision with the attribute or access the attribute via
self with a different name, ensuring the method returns the integer value stored
in the instance attribute rather than the method object.


@strawberry.field
def last_pull_request_days(self) -> int:
"""Resolve last pull request age in days."""
Expand All @@ -45,6 +57,11 @@ def last_release_days(self) -> int:
"""Resolve last release age in days."""
return self.last_release_days

@strawberry.field
def last_release_days_requirement(self) -> int:
"""Resolve last release age requirement in days."""
return self.last_release_days_requirement
Comment on lines +60 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical issue: Incorrect resolver implementation.

This new field follows the same problematic pattern, returning the method object instead of the actual integer value.

Apply this fix:

-    @strawberry.field
-    def last_release_days_requirement(self) -> int:
-        """Resolve last release age requirement in days."""
-        return self.last_release_days_requirement
+    @strawberry.field(name="lastReleaseDaysRequirement")
+    def resolve_last_release_days_requirement(self) -> int:
+        """Last release age requirement in days."""
+        from typing import cast
+        return cast(ProjectHealthMetrics, self).last_release_days_requirement
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@strawberry.field
def last_release_days_requirement(self) -> int:
"""Resolve last release age requirement in days."""
return self.last_release_days_requirement
@strawberry.field(name="lastReleaseDaysRequirement")
def resolve_last_release_days_requirement(self) -> int:
"""Last release age requirement in days."""
from typing import cast
return cast(ProjectHealthMetrics, self).last_release_days_requirement
🤖 Prompt for AI Agents
In backend/apps/owasp/graphql/nodes/project_health_metrics.py around lines 60 to
63, the resolver method last_release_days_requirement incorrectly returns the
method itself instead of the integer value. Fix this by returning the actual
attribute or value representing the last release days requirement, not the
method object.


@strawberry.field
def owasp_page_last_update_days(self) -> int:
"""Resolve OWASP page last update age in days."""
Expand Down
16 changes: 16 additions & 0 deletions backend/apps/owasp/models/project_health_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.utils import timezone

from apps.common.models import BulkSaveModel, TimestampedModel
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements


class ProjectHealthMetrics(BulkSaveModel, TimestampedModel):
Expand Down Expand Up @@ -87,6 +88,11 @@ def last_commit_days(self) -> int:
"""Calculate days since last commit."""
return (timezone.now() - self.last_committed_at).days if self.last_committed_at else 0

@property
def last_commit_days_requirement(self) -> int:
"""Get the last commit requirement for the project."""
return self.project_requirements.last_commit_days

@property
def last_pull_request_days(self) -> int:
"""Calculate days since last pull request."""
Expand All @@ -101,6 +107,11 @@ def last_release_days(self) -> int:
"""Calculate days since last release."""
return (timezone.now() - self.last_released_at).days if self.last_released_at else 0

@property
def last_release_days_requirement(self) -> int:
"""Get the last release requirement for the project."""
return self.project_requirements.last_release_days

@property
def owasp_page_last_update_days(self) -> int:
"""Calculate days since OWASP page last update."""
Expand All @@ -110,6 +121,11 @@ def owasp_page_last_update_days(self) -> int:
else 0
)

@property
def project_requirements(self) -> ProjectHealthRequirements:
"""Get the project health requirements for the project's level."""
return ProjectHealthRequirements.objects.get(level=self.project.level)

@staticmethod
def bulk_save(metrics: list, fields: list | None = None) -> None: # type: ignore[override]
"""Bulk save method for ProjectHealthMetrics.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Test cases for ProjectHealthMetricsNode."""

from datetime import datetime

import pytest

from apps.owasp.graphql.nodes.project_health_metrics import ProjectHealthMetricsNode
Expand All @@ -15,13 +17,16 @@ def test_meta_configuration(self):
}
expected_field_names = {
"age_days",
"created_at",
"contributors_count",
"forks_count",
"is_funding_requirements_compliant",
"is_leader_requirements_compliant",
"last_commit_days",
"last_commit_days_requirement",
"last_pull_request_days",
"last_release_days",
"last_release_days_requirement",
"open_issues_count",
"open_pull_requests_count",
"owasp_page_last_update_days",
Expand All @@ -47,13 +52,16 @@ def _get_field_by_name(self, name):
("field_name", "expected_type"),
[
("age_days", int),
("created_at", datetime),
("contributors_count", int),
("forks_count", int),
("is_funding_requirements_compliant", bool),
("is_leader_requirements_compliant", bool),
("last_commit_days", int),
("last_commit_days_requirement", int),
("last_pull_request_days", int),
("last_release_days", int),
("last_release_days_requirement", int),
("open_issues_count", int),
("open_pull_requests_count", int),
("owasp_page_last_update_days", int),
Expand Down
1 change: 1 addition & 0 deletions cspell/custom-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ a2eeef
ahmedxgouda
algoliasearch
ansa
apexcharts
apk
arithmatex
arkid15r
Expand Down
9 changes: 9 additions & 0 deletions frontend/__tests__/e2e/pages/ProjectDetails.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,13 @@ test.describe('Project Details Page', () => {
await page.getByText('Repo One').click()
await expect(page).toHaveURL('organizations/OWASP/repositories/repo-1')
})

test('should display health metrics section', async ({ page }) => {
await expect(page.getByText('Issues Trend')).toBeVisible()
await expect(page.getByText('Pull Requests Trend')).toBeVisible()
await expect(page.getByText('Stars Trend')).toBeVisible()
await expect(page.getByText('Forks Trend')).toBeVisible()
await expect(page.getByText('Days Since Last Commit')).toBeVisible()
await expect(page.getByText('Days Since Last Release')).toBeVisible()
})
})
13 changes: 13 additions & 0 deletions frontend/__tests__/unit/data/mockProjectDetailsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ export const mockProjectDetailsData = {
project: {
contributorsCount: 1200,
forksCount: 10,
healthMetrics: [
{
openIssuesCount: 5,
unassignedIssuesCount: 2,
unansweredIssuesCount: 1,
openPullRequestsCount: 3,
starsCount: 20,
forksCount: 10,
lastCommitDays: 5,
lastReleaseDays: 10,
score: 85,
},
],
isActive: true,
issuesCount: 10,
key: 'example-project',
Expand Down
25 changes: 25 additions & 0 deletions frontend/__tests__/unit/pages/ProjectDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ jest.mock('@fortawesome/react-fontawesome', () => ({
FontAwesomeIcon: () => <span data-testid="mock-icon" />,
}))

jest.mock('react-apexcharts', () => {
return {
__esModule: true,
default: () => {
return <div data-testid="mock-apexcharts">Mock ApexChart</div>
},
}
})

const mockRouter = {
push: jest.fn(),
}
Expand Down Expand Up @@ -142,6 +151,22 @@ describe('ProjectDetailsPage', () => {
})
})

test('Displays health metrics section', async () => {
;(useQuery as jest.Mock).mockReturnValue({
data: mockProjectDetailsData,
error: null,
})
render(<ProjectDetailsPage />)
await waitFor(() => {
expect(screen.getByText('Issues Trend')).toBeInTheDocument()
expect(screen.getByText('Pull Requests Trend')).toBeInTheDocument()
expect(screen.getByText('Stars Trend')).toBeInTheDocument()
expect(screen.getByText('Forks Trend')).toBeInTheDocument()
expect(screen.getByText('Days Since Last Commit')).toBeInTheDocument()
expect(screen.getByText('Days Since Last Release')).toBeInTheDocument()
})
})

test('Handles case when no data is available', async () => {
;(useQuery as jest.Mock).mockReturnValue({
data: { repository: null },
Expand Down
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@testing-library/user-event": "^14.6.1",
"@types/leaflet.markercluster": "^1.5.5",
"@types/lodash": "^4.17.18",
"apexcharts": "^4.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"core-js": "^3.43.0",
Expand All @@ -55,6 +56,7 @@
"next-auth": "^4.24.11",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-apexcharts": "^1.7.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.6.2",
Expand Down
79 changes: 79 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/src/app/projects/[projectKey]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@ const ProjectDetailsPage = () => {
pluralizedName: 'Repositories',
},
]

return (
<DetailsCard
details={projectDetails}
healthMetricsData={project.healthMetrics}
isActive={project.isActive}
languages={project.languages}
pullRequests={project.recentPullRequests}
Expand Down
Loading