diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 58a3bc1..845d602 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -680,14 +680,23 @@ def resolve_dependencies( if not package: raise DependencyNotFoundError(project_name, package_name, ref) - # Try to find artifact by tag or version - resolved = _resolve_dependency_to_artifact( - db, project_name, package_name, ref, ref - ) - if not resolved: - raise DependencyNotFoundError(project_name, package_name, ref) - - root_artifact_id, root_version, root_size = resolved + # Handle artifact: prefix for direct artifact ID references + if ref.startswith("artifact:"): + artifact_id = ref[9:] + artifact = db.query(Artifact).filter(Artifact.id == artifact_id).first() + if not artifact: + raise DependencyNotFoundError(project_name, package_name, ref) + root_artifact_id = artifact.id + root_version = artifact_id[:12] # Use short hash as version display + root_size = artifact.size + else: + # Try to find artifact by tag or version + resolved = _resolve_dependency_to_artifact( + db, project_name, package_name, ref, ref + ) + if not resolved: + raise DependencyNotFoundError(project_name, package_name, ref) + root_artifact_id, root_version, root_size = resolved # Track resolved artifacts and their versions resolved_artifacts: Dict[str, ResolvedArtifact] = {} diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 9d1bd01..e0730fa 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -3,8 +3,8 @@ import { Package, Tag, TagDetail, - Artifact, ArtifactDetail, + PackageArtifact, UploadResponse, PaginatedResponse, ListParams, @@ -276,10 +276,10 @@ export async function listPackageArtifacts( projectName: string, packageName: string, params: ArtifactListParams = {} -): Promise> { +): Promise> { const query = buildQueryString(params as Record); const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/artifacts${query}`); - return handleResponse>(response); + return handleResponse>(response); } // Upload diff --git a/frontend/src/pages/PackagePage.tsx b/frontend/src/pages/PackagePage.tsx index 82b43a8..c0cee96 100644 --- a/frontend/src/pages/PackagePage.tsx +++ b/frontend/src/pages/PackagePage.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useParams, useSearchParams, useNavigate, useLocation, Link } from 'react-router-dom'; -import { TagDetail, Package, PaginatedResponse, AccessLevel, Dependency, DependentInfo } from '../types'; -import { listTags, getDownloadUrl, getPackage, getMyProjectAccess, createTag, getArtifactDependencies, getReverseDependencies, getEnsureFile, UnauthorizedError, ForbiddenError } from '../api'; +import { PackageArtifact, Package, PaginatedResponse, AccessLevel, Dependency, DependentInfo } from '../types'; +import { listPackageArtifacts, getDownloadUrl, getPackage, getMyProjectAccess, createTag, getArtifactDependencies, getReverseDependencies, getEnsureFile, UnauthorizedError, ForbiddenError } from '../api'; import { Breadcrumb } from '../components/Breadcrumb'; import { Badge } from '../components/Badge'; import { SearchInput } from '../components/SearchInput'; @@ -57,7 +57,7 @@ function PackagePage() { const { user } = useAuth(); const [pkg, setPkg] = useState(null); - const [tagsData, setTagsData] = useState | null>(null); + const [artifactsData, setArtifactsData] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [accessDenied, setAccessDenied] = useState(false); @@ -75,7 +75,7 @@ function PackagePage() { const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null); // Dependencies state - const [selectedTag, setSelectedTag] = useState(null); + const [selectedArtifact, setSelectedArtifact] = useState(null); const [dependencies, setDependencies] = useState([]); const [depsLoading, setDepsLoading] = useState(false); const [depsError, setDepsError] = useState(null); @@ -138,13 +138,13 @@ function PackagePage() { try { setLoading(true); setAccessDenied(false); - const [pkgData, tagsResult, accessResult] = await Promise.all([ + const [pkgData, artifactsResult, accessResult] = await Promise.all([ getPackage(projectName, packageName), - listTags(projectName, packageName, { page, search, sort, order }), + listPackageArtifacts(projectName, packageName, { page, search, sort, order }), getMyProjectAccess(projectName), ]); setPkg(pkgData); - setTagsData(tagsResult); + setArtifactsData(artifactsResult); setAccessLevel(accessResult.access_level); setError(null); } catch (err) { @@ -168,25 +168,15 @@ function PackagePage() { loadData(); }, [loadData]); - // Auto-select tag when tags are loaded (prefer version from URL, then first tag) - // Re-run when package changes to pick up new tags + // Auto-select artifact when artifacts are loaded (prefer first artifact) + // Re-run when package changes to pick up new artifacts useEffect(() => { - if (tagsData?.items && tagsData.items.length > 0) { - const versionParam = searchParams.get('version'); - if (versionParam) { - // Find tag matching the version parameter - const matchingTag = tagsData.items.find(t => t.version === versionParam); - if (matchingTag) { - setSelectedTag(matchingTag); - setDependencies([]); - return; - } - } - // Fall back to first tag - setSelectedTag(tagsData.items[0]); + if (artifactsData?.items && artifactsData.items.length > 0) { + // Fall back to first artifact + setSelectedArtifact(artifactsData.items[0]); setDependencies([]); } - }, [tagsData, searchParams, projectName, packageName]); + }, [artifactsData, projectName, packageName]); // Fetch dependencies when selected tag changes const fetchDependencies = useCallback(async (artifactId: string) => { @@ -204,10 +194,10 @@ function PackagePage() { }, []); useEffect(() => { - if (selectedTag) { - fetchDependencies(selectedTag.artifact_id); + if (selectedArtifact) { + fetchDependencies(selectedArtifact.id); } - }, [selectedTag, fetchDependencies]); + }, [selectedArtifact, fetchDependencies]); // Fetch reverse dependencies const fetchReverseDeps = useCallback(async (pageNum: number = 1) => { @@ -254,11 +244,11 @@ function PackagePage() { } }, [projectName, packageName]); - // Fetch ensure file for selected tag + // Fetch ensure file for selected artifact (if it has tags) const fetchEnsureFile = useCallback(async () => { - if (!selectedTag) return; - fetchEnsureFileForTag(selectedTag.name); - }, [selectedTag, fetchEnsureFileForTag]); + if (!selectedArtifact || selectedArtifact.tags.length === 0) return; + fetchEnsureFileForTag(selectedArtifact.tags[0]); + }, [selectedArtifact, fetchEnsureFileForTag]); // Keyboard navigation - go back with backspace useEffect(() => { @@ -331,25 +321,35 @@ function PackagePage() { }; const hasActiveFilters = search !== ''; - const tags = tagsData?.items || []; - const pagination = tagsData?.pagination; + const artifacts = artifactsData?.items || []; + const pagination = artifactsData?.pagination; - const handleTagSelect = (tag: TagDetail) => { - setSelectedTag(tag); + const handleArtifactSelect = (artifact: PackageArtifact) => { + setSelectedArtifact(artifact); }; - const handleMenuOpen = (e: React.MouseEvent, tagId: string) => { + const handleMenuOpen = (e: React.MouseEvent, artifactId: string) => { e.stopPropagation(); - if (openMenuId === tagId) { + if (openMenuId === artifactId) { setOpenMenuId(null); setMenuPosition(null); } else { const rect = e.currentTarget.getBoundingClientRect(); setMenuPosition({ top: rect.bottom + 4, left: rect.right - 180 }); - setOpenMenuId(tagId); + setOpenMenuId(artifactId); } }; + // Helper to get version from artifact metadata + const getArtifactVersion = (a: PackageArtifact): string | null => { + return (a.format_metadata?.version as string) || null; + }; + + // Helper to get download ref - prefer first tag, fallback to artifact ID + const getDownloadRef = (a: PackageArtifact): string => { + return a.tags.length > 0 ? a.tags[0] : `artifact:${a.id}`; + }; + // System projects show Version first, regular projects show Tag first const columns = isSystemProject ? [ @@ -358,44 +358,44 @@ function PackagePage() { key: 'version', header: 'Version', sortable: true, - render: (t: TagDetail) => ( + render: (a: PackageArtifact) => ( handleTagSelect(t)} + className={`tag-name-link ${selectedArtifact?.id === a.id ? 'selected' : ''}`} + onClick={() => handleArtifactSelect(a)} style={{ cursor: 'pointer' }} > - {t.version || t.name} + {getArtifactVersion(a) || a.tags[0] || a.id.slice(0, 12)} ), }, { - key: 'artifact_original_name', + key: 'original_name', header: 'Filename', className: 'cell-truncate', - render: (t: TagDetail) => ( - {t.artifact_original_name || t.name} + render: (a: PackageArtifact) => ( + {a.original_name || a.id.slice(0, 12)} ), }, { - key: 'artifact_size', + key: 'size', header: 'Size', - render: (t: TagDetail) => {formatBytes(t.artifact_size)}, + render: (a: PackageArtifact) => {formatBytes(a.size)}, }, { key: 'created_at', header: 'Cached', sortable: true, - render: (t: TagDetail) => ( - {new Date(t.created_at).toLocaleDateString()} + render: (a: PackageArtifact) => ( + {new Date(a.created_at).toLocaleDateString()} ), }, { key: 'actions', header: '', - render: (t: TagDetail) => ( + render: (a: PackageArtifact) => (
- - + {a.tags.length > 0 && ( + + )} {canWrite && !isSystemProject && ( - )} -
@@ -545,7 +547,7 @@ function PackagePage() { ); }; - if (loading && !tagsData) { + if (loading && !artifactsData) { return
Loading...
; } @@ -653,7 +655,7 @@ function PackagePage() { @@ -666,13 +668,13 @@ function PackagePage() {
t.id} + keyExtractor={(a) => a.id} emptyMessage={ hasActiveFilters - ? 'No tags match your filters. Try adjusting your search.' - : 'No tags yet. Upload an artifact with a tag to create one!' + ? 'No artifacts match your filters. Try adjusting your search.' + : 'No artifacts yet. Upload a file to get started!' } onSort={handleSortChange} sortKey={sort} @@ -752,11 +754,11 @@ function PackagePage() {
{/* Dependency Graph Modal */} - {showGraph && selectedTag && ( + {showGraph && selectedArtifact && ( 0 ? selectedArtifact.tags[0] : `artifact:${selectedArtifact.id}`} onClose={() => setShowGraph(false)} /> )} @@ -916,11 +918,11 @@ function PackagePage() { )} {/* Dependencies Modal */} - {showDepsModal && selectedTag && ( + {showDepsModal && selectedArtifact && (
setShowDepsModal(false)}>
e.stopPropagation()}>
-

Dependencies for {selectedTag.version || selectedTag.name}

+

Dependencies for {selectedArtifact.original_name || selectedArtifact.id.slice(0, 12)}

- + {selectedArtifact?.tags && selectedArtifact.tags.length > 0 && ( + + )}