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
This commit is contained in:
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
- 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
|
||||
|
||||
@@ -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() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SortDropdown options={SORT_OPTIONS} value={sort} order={order} onChange={handleSortChange} />
|
||||
</div>
|
||||
|
||||
{hasActiveFilters && (
|
||||
@@ -304,70 +298,78 @@ function ProjectPage() {
|
||||
</FilterChipGroup>
|
||||
)}
|
||||
|
||||
{packages.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
{hasActiveFilters ? (
|
||||
<p>No packages match your filters. Try adjusting your search.</p>
|
||||
) : (
|
||||
<p>No packages yet. Create your first package to start uploading artifacts!</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="project-grid">
|
||||
{packages.map((pkg) => (
|
||||
<Link to={`/project/${projectName}/${pkg.name}`} key={pkg.id} className="project-card card">
|
||||
<div className="package-card__header">
|
||||
<h3>{pkg.name}</h3>
|
||||
<Badge variant="default">{pkg.format}</Badge>
|
||||
</div>
|
||||
{pkg.description && <p>{pkg.description}</p>}
|
||||
<div className="data-table--responsive">
|
||||
<DataTable
|
||||
data={packages}
|
||||
keyExtractor={(pkg) => 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) => <span className="cell-name">{pkg.name}</span>,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
header: 'Description',
|
||||
className: 'cell-description',
|
||||
render: (pkg) => pkg.description || '—',
|
||||
},
|
||||
{
|
||||
key: 'format',
|
||||
header: 'Format',
|
||||
render: (pkg) => <Badge variant="default">{pkg.format}</Badge>,
|
||||
},
|
||||
{
|
||||
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 ? <strong style={{ color: 'var(--accent-primary)' }}>{pkg.latest_tag}</strong> : '—',
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
header: 'Created',
|
||||
sortable: true,
|
||||
className: 'cell-date',
|
||||
render: (pkg) => new Date(pkg.created_at).toLocaleDateString(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(pkg.tag_count !== undefined || pkg.artifact_count !== undefined) && (
|
||||
<div className="package-stats">
|
||||
{pkg.tag_count !== undefined && (
|
||||
<div className="package-stats__item">
|
||||
<span className="package-stats__value">{pkg.tag_count}</span>
|
||||
<span className="package-stats__label">Tags</span>
|
||||
</div>
|
||||
)}
|
||||
{pkg.artifact_count !== undefined && (
|
||||
<div className="package-stats__item">
|
||||
<span className="package-stats__value">{pkg.artifact_count}</span>
|
||||
<span className="package-stats__label">Artifacts</span>
|
||||
</div>
|
||||
)}
|
||||
{pkg.total_size !== undefined && pkg.total_size > 0 && (
|
||||
<div className="package-stats__item">
|
||||
<span className="package-stats__value">{formatBytes(pkg.total_size)}</span>
|
||||
<span className="package-stats__label">Size</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="project-meta">
|
||||
{pkg.latest_tag && (
|
||||
<span className="latest-tag">
|
||||
Latest: <strong>{pkg.latest_tag}</strong>
|
||||
</span>
|
||||
)}
|
||||
<span className="date">Created {new Date(pkg.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pagination && pagination.total_pages > 1 && (
|
||||
<Pagination
|
||||
page={pagination.page}
|
||||
totalPages={pagination.total_pages}
|
||||
total={pagination.total}
|
||||
limit={pagination.limit}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{pagination && pagination.total_pages > 1 && (
|
||||
<Pagination
|
||||
page={pagination.page}
|
||||
totalPages={pagination.total_pages}
|
||||
total={pagination.total}
|
||||
limit={pagination.limit}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canAdmin && projectName && (
|
||||
|
||||
Reference in New Issue
Block a user