Show missing dependencies in dependency graph instead of failing

When dependencies are not cached on the server (common since we removed
proactive caching), the dependency graph now:
- Continues resolving what it can find
- Shows missing dependencies in a separate section with amber styling
- Displays the constraint and which package required them
- Updates the header stats to show "X cached • Y not cached"

This provides a better user experience than showing an error when
some dependencies haven't been downloaded yet.
This commit is contained in:
Mondo Diaz
2026-02-02 16:29:37 -06:00
parent 152af0a852
commit 88765b4f50
5 changed files with 111 additions and 6 deletions

View File

@@ -42,6 +42,7 @@ from .schemas import (
ResolvedArtifact, ResolvedArtifact,
DependencyResolutionResponse, DependencyResolutionResponse,
DependencyConflict, DependencyConflict,
MissingDependency,
PaginationMeta, PaginationMeta,
) )
@@ -690,6 +691,8 @@ def resolve_dependencies(
# Track resolved artifacts and their versions # Track resolved artifacts and their versions
resolved_artifacts: Dict[str, ResolvedArtifact] = {} resolved_artifacts: Dict[str, ResolvedArtifact] = {}
# Track missing dependencies (not cached on server)
missing_dependencies: List[MissingDependency] = []
# Track version requirements for conflict detection # Track version requirements for conflict detection
version_requirements: Dict[str, List[Dict[str, Any]]] = {} # pkg_key -> [(version, required_by)] version_requirements: Dict[str, List[Dict[str, Any]]] = {} # pkg_key -> [(version, required_by)]
# Track visiting/visited for cycle detection # Track visiting/visited for cycle detection
@@ -773,12 +776,15 @@ def resolve_dependencies(
) )
if not resolved_dep: if not resolved_dep:
# Dependency not cached on server - track as missing but continue
constraint = dep.version_constraint or dep.tag_constraint constraint = dep.version_constraint or dep.tag_constraint
raise DependencyNotFoundError( missing_dependencies.append(MissingDependency(
dep.dependency_project, project=dep.dependency_project,
dep.dependency_package, package=dep.dependency_package,
constraint, constraint=constraint,
) required_by=pkg_key,
))
continue
dep_artifact_id, dep_version, dep_size = resolved_dep dep_artifact_id, dep_version, dep_size = resolved_dep
_resolve_recursive( _resolve_recursive(
@@ -828,6 +834,7 @@ def resolve_dependencies(
"ref": ref, "ref": ref,
}, },
resolved=resolved_list, resolved=resolved_list,
missing=missing_dependencies,
total_size=total_size, total_size=total_size,
artifact_count=len(resolved_list), artifact_count=len(resolved_list),
) )

View File

@@ -1033,10 +1033,19 @@ class ResolvedArtifact(BaseModel):
download_url: str download_url: str
class MissingDependency(BaseModel):
"""A dependency that could not be resolved (not cached on server)"""
project: str
package: str
constraint: Optional[str] = None
required_by: Optional[str] = None
class DependencyResolutionResponse(BaseModel): class DependencyResolutionResponse(BaseModel):
"""Response from dependency resolution endpoint""" """Response from dependency resolution endpoint"""
requested: Dict[str, str] # project, package, ref requested: Dict[str, str] # project, package, ref
resolved: List[ResolvedArtifact] resolved: List[ResolvedArtifact]
missing: List[MissingDependency] = []
total_size: int total_size: int
artifact_count: int artifact_count: int

View File

@@ -55,6 +55,10 @@
font-size: 0.8125rem; font-size: 0.8125rem;
} }
.missing-count {
color: #f59e0b;
}
.close-btn { .close-btn {
background: transparent; background: transparent;
border: none; border: none;
@@ -314,6 +318,63 @@
font-size: 0.75rem; font-size: 0.75rem;
} }
/* Missing Dependencies */
.missing-dependencies {
border-top: 1px solid var(--border-primary);
padding: 16px 20px;
background: rgba(245, 158, 11, 0.05);
max-height: 200px;
overflow-y: auto;
}
.missing-dependencies h3 {
margin: 0 0 8px 0;
font-size: 0.875rem;
font-weight: 600;
color: #f59e0b;
}
.missing-hint {
margin: 0 0 12px 0;
font-size: 0.75rem;
color: var(--text-muted);
}
.missing-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.missing-item {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--bg-tertiary);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: var(--radius-sm);
padding: 4px 8px;
font-size: 0.75rem;
}
.missing-name {
font-family: 'JetBrains Mono', monospace;
color: var(--text-secondary);
}
.missing-constraint {
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
}
.missing-required-by {
color: var(--text-muted);
font-size: 0.6875rem;
}
/* Responsive */ /* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.dependency-graph-modal { .dependency-graph-modal {

View File

@@ -244,7 +244,11 @@ function DependencyGraph({ projectName, packageName, tagName, onClose }: Depende
<span>{projectName}/{packageName} @ {tagName}</span> <span>{projectName}/{packageName} @ {tagName}</span>
{resolution && ( {resolution && (
<span className="graph-stats"> <span className="graph-stats">
{resolution.artifact_count} packages {formatBytes(resolution.total_size)} total {resolution.artifact_count} cached
{resolution.missing && resolution.missing.length > 0 && (
<span className="missing-count"> {resolution.missing.length} not cached</span>
)}
{formatBytes(resolution.total_size)} total
</span> </span>
)} )}
</div> </div>
@@ -307,6 +311,22 @@ function DependencyGraph({ projectName, packageName, tagName, onClose }: Depende
)} )}
</div> </div>
{resolution && resolution.missing && resolution.missing.length > 0 && (
<div className="missing-dependencies">
<h3>Not Cached ({resolution.missing.length})</h3>
<p className="missing-hint">These dependencies are referenced but not yet cached on the server.</p>
<ul className="missing-list">
{resolution.missing.map((dep, i) => (
<li key={i} className="missing-item">
<span className="missing-name">{dep.project}/{dep.package}</span>
{dep.constraint && <span className="missing-constraint">@{dep.constraint}</span>}
{dep.required_by && <span className="missing-required-by"> {dep.required_by}</span>}
</li>
))}
</ul>
</div>
)}
{hoveredNode && ( {hoveredNode && (
<div className="graph-tooltip"> <div className="graph-tooltip">
<strong>{hoveredNode.project}/{hoveredNode.package}</strong> <strong>{hoveredNode.project}/{hoveredNode.package}</strong>

View File

@@ -433,6 +433,13 @@ export interface ResolvedArtifact {
download_url: string; download_url: string;
} }
export interface MissingDependency {
project: string;
package: string;
constraint: string | null;
required_by: string | null;
}
export interface DependencyResolutionResponse { export interface DependencyResolutionResponse {
requested: { requested: {
project: string; project: string;
@@ -440,6 +447,7 @@ export interface DependencyResolutionResponse {
ref: string; ref: string;
}; };
resolved: ResolvedArtifact[]; resolved: ResolvedArtifact[];
missing: MissingDependency[];
total_size: number; total_size: number;
artifact_count: number; artifact_count: number;
} }