Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e3120d8
trying out stuff
nipunh Mar 16, 2025
51f1a78
basic implementation completed
nipunh Mar 16, 2025
e9060fd
snapshots page completed
nipunh Mar 18, 2025
97d266a
test cases added
nipunh Mar 18, 2025
1179ff6
Merge branch 'main' into feature/communit-snapshots-page
nipunh Mar 18, 2025
391e523
improved code quality
nipunh Mar 18, 2025
03c09b5
corrections
nipunh Mar 18, 2025
6c57eb8
mobile navigation implemented
nipunh Mar 18, 2025
c073c2a
code smell removed
nipunh Mar 18, 2025
d0eb718
code smell removed
nipunh Mar 18, 2025
f59dffd
formatting issue
nipunh Mar 18, 2025
9c69cec
removed trailing spaces
nipunh Mar 18, 2025
33657ea
code formatting improved
nipunh Mar 18, 2025
658de49
code formatting improved
nipunh Mar 18, 2025
4fd3010
test cases updated
nipunh Mar 18, 2025
e0e7f46
Merge branch 'main' into feature/communit-snapshots-page
arkid15r Mar 18, 2025
1aab7ad
updated branch
nipunh Mar 20, 2025
64cfe7c
Merge branch 'main' into feature/communit-snapshots-page
nipunh Mar 20, 2025
81f3fbc
Merge branch 'main' into feature/communit-snapshots-page
arkid15r Mar 24, 2025
89af8c7
Merge branch 'main' into feature/communit-snapshots-page
nipunh Mar 28, 2025
3c79828
Merge branch 'main' into feature/communit-snapshots-page
arkid15r Mar 29, 2025
65f4c4f
test cases updated
nipunh Mar 29, 2025
975d70d
code structure updated
nipunh Mar 29, 2025
4eac7b5
updated few cases
nipunh Mar 29, 2025
d2aed1a
test coverage completed
nipunh Mar 29, 2025
c596951
code cleanup
nipunh Mar 29, 2025
0ec9351
nitpicks resolved
nipunh Mar 29, 2025
e164b45
formating
nipunh Mar 29, 2025
342df6d
updated test case
nipunh Mar 29, 2025
af3a9e5
Merge branch 'main' into pr/nipunh/1130
arkid15r Mar 30, 2025
5bdf0bc
Update code
arkid15r Mar 30, 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
8 changes: 4 additions & 4 deletions backend/apps/owasp/graphql/queries/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class SnapshotQuery(BaseQuery):
key=graphene.String(required=True),
)

recent_snapshots = graphene.List(
snapshots = graphene.List(
SnapshotNode,
limit=graphene.Int(default_value=8),
limit=graphene.Int(default_value=12),
)

def resolve_snapshot(root, info, key):
Expand All @@ -27,6 +27,6 @@ def resolve_snapshot(root, info, key):
except Snapshot.DoesNotExist:
return None

def resolve_recent_snapshots(root, info, limit):
"""Resolve recent snapshots."""
def resolve_snapshots(root, info, limit):
"""Resolve snapshots."""
return Snapshot.objects.order_by("-created_at")[:limit]
1 change: 1 addition & 0 deletions frontend/__tests__/unit/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jest.mock('pages', () => ({
RepositoryDetailsPage: () => (
<div data-testid="repository-details-page">RepositoryDetails Page</div>
),
SnapshotsPage: () => <div data-testid="snapshots-page">Snapshots Page</div>,
SnapshotDetailsPage: () => <div data-testid="snapshot-details-page">SnapshotDetails Page</div>,
UserDetailsPage: () => <div data-testid="user-details-page">UserDetails Page</div>,
UsersPage: () => <div data-testid="users-page">Users Page</div>,
Expand Down
11 changes: 11 additions & 0 deletions frontend/__tests__/unit/data/mockSnapshotData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,14 @@ export const mockSnapshotDetailsData = {
],
},
}

export const mockSnapshotData = {
snapshots: [
{
title: 'New Snapshot',
key: '2024-12',
startAt: '2024-12-01T00:00:00+00:00',
endAt: '2024-12-31T22:00:30+00:00',
},
],
}
124 changes: 124 additions & 0 deletions frontend/__tests__/unit/pages/Snapshots.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useQuery } from '@apollo/client'
import { screen, waitFor, fireEvent } from '@testing-library/react'
import { act } from 'react'
import { useNavigate } from 'react-router-dom'
import { render } from 'wrappers/testUtil'
import { toaster } from 'components/ui/toaster'
import SnapshotsPage from 'pages/Snapshots'

jest.mock('components/ui/toaster', () => ({
toaster: {
create: jest.fn(),
},
}))

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
}))

jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'),
useQuery: jest.fn(),
}))

const mockSnapshots = [
{
key: '2024-12',
title: 'Snapshot 1',
startAt: '2023-01-01T00:00:00Z',
endAt: '2023-01-02T00:00:00Z',
},
{
key: '2024-11',
title: 'Snapshot 2',
startAt: '2022-12-01T00:00:00Z',
endAt: '2022-12-31T23:59:59Z',
},
]

describe('SnapshotsPage', () => {
beforeEach(() => {
;(useQuery as jest.Mock).mockReturnValue({
data: { snapshots: mockSnapshots },
error: null,
})
})

afterEach(() => {
jest.clearAllMocks()
})

it('renders loading spinner initially', async () => {
;(useQuery as jest.Mock).mockReturnValue({
data: null,
error: null,
})

render(<SnapshotsPage />)

await waitFor(() => {
const loadingSpinners = screen.getAllByAltText('Loading indicator')
expect(loadingSpinners.length).toBe(2)
})
})

it('renders snapshots when data is fetched successfully', async () => {
render(<SnapshotsPage />)

await waitFor(() => {
expect(screen.getByText('Snapshot 1')).toBeInTheDocument()
expect(screen.getByText('Snapshot 2')).toBeInTheDocument()
})
})

it('renders "No Snapshots found" when no snapshots are available', async () => {
;(useQuery as jest.Mock).mockReturnValue({
data: { snapshots: [] },
error: null,
})

render(<SnapshotsPage />)

await waitFor(() => {
expect(screen.getByText('No Snapshots found')).toBeInTheDocument()
})
})

it('shows an error toaster when GraphQL request fails', async () => {
;(useQuery as jest.Mock).mockReturnValue({
data: null,
error: new Error('GraphQL error'),
})

render(<SnapshotsPage />)

await waitFor(() => {
expect(toaster.create).toHaveBeenCalledWith({
description: 'Unable to complete the requested operation.',
title: 'GraphQL Request Failed',
type: 'error',
})
})
})

it('navigates to the correct URL when "View Snapshot" button is clicked', async () => {
const navigateMock = jest.fn()
;(useNavigate as jest.Mock).mockReturnValue(navigateMock)
render(<SnapshotsPage />)

// Wait for the "View Snapshot" button to appear
const viewSnapshotButton = await screen.findAllByRole('button', { name: /view snapshot/i })

// Click the button
await act(async () => {
fireEvent.click(viewSnapshotButton[0])
})

// Check if navigate was called with the correct argument
await waitFor(() => {
expect(navigateMock).toHaveBeenCalledTimes(1)
expect(navigateMock).toHaveBeenCalledWith('/community/snapshots/2024-12')
})
})
})
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ProjectsPage,
RepositoryDetailsPage,
SnapshotDetailsPage,
SnapshotsPage,
UserDetailsPage,
UsersPage,
} from 'pages'
Expand Down Expand Up @@ -44,6 +45,7 @@ function App() {
<Route path="/chapters/:chapterKey" element={<ChapterDetailsPage />}></Route>
<Route path="/community/snapshots/:id" element={<SnapshotDetailsPage />}></Route>
<Route path="/community/users" element={<UsersPage />}></Route>
<Route path="/community/snapshots" element={<SnapshotsPage />}></Route>
<Route path="/community/users/:userKey" element={<UserDetailsPage />}></Route>
<Route path="*" element={<ErrorDisplay {...ERROR_CONFIGS['404']} />} />
</Routes>
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/api/queries/snapshotQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,14 @@ export const GET_SNAPSHOT_DETAILS = gql`
}
}
`

export const GET_COMMUNITY_SNAPSHOTS = gql`
query GetCommunitySnapshots {
snapshots(limit: 12) {
key
title
startAt
endAt
}
}
`
107 changes: 80 additions & 27 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,48 @@ export default function Header() {
{/* Desktop Header Links */}
<div className="hidden flex-1 justify-between rounded-lg pl-6 font-medium md:block">
<div className="flex justify-start pl-6">
{headerLinks.map((link, i) => (
<NavLink
key={i}
to={link.href}
className={cn(
'navlink px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === link.href && 'font-bold text-blue-800 dark:text-white'
)}
aria-current="page"
>
{link.text}
</NavLink>
))}
{headerLinks.map((link) => {
return link.submenu ? (
<div
key={link.text}
className={cn(
'dropdown navlink group px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
link.submenu.map((sub) => sub.href).includes(location.pathname) &&
'font-bold text-blue-800 dark:text-white'
)}
>
{link.text}
<div className="dropdown-menu group-hover:visible group-hover:opacity-100">
{link.submenu.map((sub) => (
<NavLink
key={link.text}
to={sub.href}
className={cn(
'navlink px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === sub.href &&
'font-bold text-blue-800 dark:text-white'
)}
aria-current="page"
>
{sub.text}
</NavLink>
))}
</div>
</div>
) : (
<NavLink
key={link.text}
to={link.href}
className={cn(
'navlink px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === link.href && 'font-bold text-blue-800 dark:text-white'
)}
aria-current="page"
>
{link.text}
</NavLink>
)
})}
</div>
</div>
<div className="flex items-center justify-normal space-x-4">
Expand Down Expand Up @@ -137,20 +166,44 @@ export default function Header() {
</div>
</div>
</NavLink>
{headerLinks.map((link, i) => (
<NavLink
key={i}
to={link.href}
className={cn(
'navlink block px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === link.href &&
'font-bold text-blue-800 dark:text-white'
)}
onClick={toggleMobileMenu}
>
{link.text}
</NavLink>
))}
{headerLinks.map((link) =>
link.submenu ? (
<div key={link.text} className="flex flex-col">
<div className="block px-3 py-2 font-medium text-slate-700 dark:text-slate-300">
{link.text}
</div>
<div className="ml-4">
{link.submenu.map((sub) => (
<NavLink
key={link.text}
to={sub.href}
className={cn(
'navlink block px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === sub.href &&
'font-bold text-blue-800 dark:text-white'
)}
onClick={toggleMobileMenu}
>
{sub.text}
</NavLink>
))}
</div>
</div>
) : (
<NavLink
key={link.text}
to={link.href}
className={cn(
'navlink block px-3 py-2 text-slate-700 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-200',
location.pathname === link.href &&
'font-bold text-blue-800 dark:text-white'
)}
onClick={toggleMobileMenu}
>
{link.text}
</NavLink>
)
)}
</div>

<div className="flex flex-col gap-y-2">
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/SnapshotCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Button } from '@chakra-ui/react'
import { faChevronRight, faCalendar } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { SnapshotCardProps } from 'types/card'
import { formatDate } from 'utils/dateFormatter'

const SnapshotCard = ({ title, button, startAt, endAt }: SnapshotCardProps) => {
return (
<Button
onClick={button.onclick}
className="group flex h-40 w-full flex-col items-center rounded-lg bg-white p-6 text-left shadow-lg transition-transform duration-500 hover:scale-105 hover:shadow-xl dark:bg-gray-800 dark:shadow-gray-900/30"
>
<div className="text-center">
<h3 className="max-w-[250px] text-balance text-lg font-semibold text-gray-900 group-hover:text-blue-500 dark:text-white sm:text-xl">
<p>{title}</p>
</h3>
</div>

<div className="flex flex-wrap items-center gap-2 text-gray-600 dark:text-gray-300">
<div className="flex items-center">
<FontAwesomeIcon icon={faCalendar} className="mr-1 h-4 w-4" />
<span>
{formatDate(startAt)} - {formatDate(endAt)}
</span>
</div>
</div>

<div className="mt-auto inline-flex items-center text-sm font-medium text-blue-500 dark:text-blue-400">
View Snapshot
<FontAwesomeIcon
icon={faChevronRight}
className="ml-2 h-4 w-4 transform transition-transform group-hover:translate-x-1"
/>
</div>
</Button>
)
}

export default SnapshotCard
15 changes: 15 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,21 @@ a {
color: inherit;
}

/* Dropdown container */
.dropdown {
@apply relative;
}

.dropdown-menu {
@apply absolute left-0 top-full mt-2 w-48 rounded-lg bg-white p-3 shadow-lg dark:bg-gray-800;
@apply invisible opacity-0 transition-all duration-200 ease-in-out;
@apply flex flex-col space-y-2; /* Stack items vertically */
}

.dropdown:hover .dropdown-menu {
@apply visible opacity-100;
}

@keyframes fadeIn {
from {
opacity: 0;
Expand Down
Loading