From 005c3d0f6e3c05cb4e24d712e476434e8855e078 Mon Sep 17 00:00:00 2001 From: Mondo Diaz Date: Thu, 15 Jan 2026 15:07:38 +0000 Subject: [PATCH] Replace package cards with sortable data table on Project page - Convert package grid to DataTable matching Home page style - Add sortable columns: Name, Created - Show package stats: Tags, Artifacts, Size, Latest tag - Row click navigates to package page - Keep existing search and format filter working --- CHANGELOG.md | 1 + frontend/src/pages/ProjectPage.tsx | 150 +++++++++++++++-------------- 2 files changed, 77 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f64383..be9ae22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Adjusted dark mode color palette to use lighter background tones for better readability and reduced eye strain (#52) - Replaced project card grid with sortable data table on Home page for better handling of large project lists +- Replaced package card grid with sortable data table on Project page for consistency - Improved pod naming: Orchard pods now named `orchard-{env}-server-*` for clarity (#51) ### Fixed diff --git a/frontend/src/pages/ProjectPage.tsx b/frontend/src/pages/ProjectPage.tsx index 6b8a99e..75ad577 100644 --- a/frontend/src/pages/ProjectPage.tsx +++ b/frontend/src/pages/ProjectPage.tsx @@ -1,23 +1,17 @@ import { useState, useEffect, useCallback } from 'react'; -import { useParams, Link, useSearchParams, useNavigate, useLocation } from 'react-router-dom'; +import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom'; import { Project, Package, PaginatedResponse, AccessLevel } from '../types'; import { getProject, listPackages, createPackage, getMyProjectAccess, UnauthorizedError, ForbiddenError } from '../api'; import { Breadcrumb } from '../components/Breadcrumb'; import { Badge } from '../components/Badge'; +import { DataTable } from '../components/DataTable'; import { SearchInput } from '../components/SearchInput'; -import { SortDropdown, SortOption } from '../components/SortDropdown'; import { FilterChip, FilterChipGroup } from '../components/FilterChip'; import { Pagination } from '../components/Pagination'; import { AccessManagement } from '../components/AccessManagement'; import { useAuth } from '../contexts/AuthContext'; import './Home.css'; -const SORT_OPTIONS: SortOption[] = [ - { value: 'name', label: 'Name' }, - { value: 'created_at', label: 'Created' }, - { value: 'updated_at', label: 'Updated' }, -]; - const FORMAT_OPTIONS = ['generic', 'npm', 'pypi', 'docker', 'deb', 'rpm', 'maven', 'nuget', 'helm']; function formatBytes(bytes: number): string { @@ -140,8 +134,9 @@ function ProjectPage() { updateParams({ search: value, page: '1' }); }; - const handleSortChange = (newSort: string, newOrder: 'asc' | 'desc') => { - updateParams({ sort: newSort, order: newOrder, page: '1' }); + const handleSortChange = (columnKey: string) => { + const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc'; + updateParams({ sort: columnKey, order: newOrder, page: '1' }); }; const handleFormatChange = (value: string) => { @@ -294,7 +289,6 @@ function ProjectPage() { ))} - {hasActiveFilters && ( @@ -304,70 +298,78 @@ function ProjectPage() { )} - {packages.length === 0 ? ( -
- {hasActiveFilters ? ( -

No packages match your filters. Try adjusting your search.

- ) : ( -

No packages yet. Create your first package to start uploading artifacts!

- )} -
- ) : ( - <> -
- {packages.map((pkg) => ( - -
-

{pkg.name}

- {pkg.format} -
- {pkg.description &&

{pkg.description}

} +
+ pkg.id} + onRowClick={(pkg) => navigate(`/project/${projectName}/${pkg.name}`)} + onSort={handleSortChange} + sortKey={sort} + sortOrder={order} + emptyMessage={ + hasActiveFilters + ? 'No packages match your filters. Try adjusting your search.' + : 'No packages yet. Create your first package to start uploading artifacts!' + } + columns={[ + { + key: 'name', + header: 'Name', + sortable: true, + render: (pkg) => {pkg.name}, + }, + { + key: 'description', + header: 'Description', + className: 'cell-description', + render: (pkg) => pkg.description || '—', + }, + { + key: 'format', + header: 'Format', + render: (pkg) => {pkg.format}, + }, + { + key: 'tags', + header: 'Tags', + render: (pkg) => pkg.tag_count ?? '—', + }, + { + key: 'artifacts', + header: 'Artifacts', + render: (pkg) => pkg.artifact_count ?? '—', + }, + { + key: 'size', + header: 'Size', + render: (pkg) => + pkg.total_size !== undefined && pkg.total_size > 0 ? formatBytes(pkg.total_size) : '—', + }, + { + key: 'latest_tag', + header: 'Latest', + render: (pkg) => + pkg.latest_tag ? {pkg.latest_tag} : '—', + }, + { + key: 'created_at', + header: 'Created', + sortable: true, + className: 'cell-date', + render: (pkg) => new Date(pkg.created_at).toLocaleDateString(), + }, + ]} + /> +
- {(pkg.tag_count !== undefined || pkg.artifact_count !== undefined) && ( -
- {pkg.tag_count !== undefined && ( -
- {pkg.tag_count} - Tags -
- )} - {pkg.artifact_count !== undefined && ( -
- {pkg.artifact_count} - Artifacts -
- )} - {pkg.total_size !== undefined && pkg.total_size > 0 && ( -
- {formatBytes(pkg.total_size)} - Size -
- )} -
- )} - -
- {pkg.latest_tag && ( - - Latest: {pkg.latest_tag} - - )} - Created {new Date(pkg.created_at).toLocaleDateString()} -
- - ))} -
- - {pagination && pagination.total_pages > 1 && ( - - )} - + {pagination && pagination.total_pages > 1 && ( + )} {canAdmin && projectName && (