Files
at-container-registry/pkg/appview/templates/partials/diff-content.html
2026-04-22 20:13:40 -05:00

204 lines
12 KiB
HTML

{{ define "diff-content" }}
<!-- Layers + Vulnerabilities Diff -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Layer Diff (Left) -->
<div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0">
<h2 class="text-lg font-semibold">Layers</h2>
{{ if .LayerDiff }}
<div class="overflow-x-auto">
<table class="table table-xs w-full">
<caption class="sr-only">Layer differences</caption>
<thead>
<tr class="text-xs">
<th scope="col" class="w-6"><span class="sr-only">Change</span></th>
<th scope="col" class="w-8">#</th>
<th scope="col">Command</th>
<th scope="col" class="text-right w-24">Size</th>
</tr>
</thead>
<tbody>
{{ range .LayerDiff }}
<tr class="{{ if eq .Status "added" }}bg-success/10{{ else if eq .Status "removed" }}bg-error/10{{ else if eq .Status "rebuilt" }}bg-warning/10{{ else }}opacity-60{{ end }}">
<td class="font-mono text-xs text-center font-bold {{ if eq .Status "added" }}text-success{{ else if eq .Status "removed" }}text-error{{ else if eq .Status "rebuilt" }}text-warning{{ end }}">{{/* Glyph + sr-only label: color is redundant information. */}}{{ if eq .Status "added" }}<span aria-hidden="true">+</span><span class="sr-only">Added</span>{{ else if eq .Status "removed" }}<span aria-hidden="true">-</span><span class="sr-only">Removed</span>{{ else if eq .Status "rebuilt" }}<span aria-hidden="true">~</span><span class="sr-only">Rebuilt</span>{{ else }}<span class="sr-only">Unchanged</span>{{ end }}</td>
<td class="font-mono text-xs">{{ .Layer.Index }}</td>
<td>
{{ if .Layer.Command }}
<code class="font-mono text-xs break-all line-clamp-2" title="{{ .Layer.Command }}">{{ .Layer.Command }}</code>
{{ end }}
</td>
<td class="text-right text-sm whitespace-nowrap">
{{ if not .Layer.EmptyLayer }}{{ humanizeBytes .Layer.Size }}{{ end }}
{{ if and (eq .Status "rebuilt") .PrevLayer }}
{{ if ne .Layer.Size .PrevLayer.Size }}
<span class="text-xs text-base-content/70">({{ humanizeByteDelta (sub64 .Layer.Size .PrevLayer.Size) }})</span>
{{ end }}
{{ end }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ else }}
{{ template "state-empty" (dict
"Icon" "history"
"Title" "No layer history"
"Subtext" "Neither side of this diff reports layer history."
) }}
{{ end }}
</div>
<!-- Vulnerability Diff (Right) -->
<div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0">
<h2 class="text-lg font-semibold">Vulnerabilities</h2>
{{ if not .HasVulnData }}
{{/* Branch on per-side scan status so users can tell "not scanned
yet" from "hold offline" from transient errors. */}}
{{ if or (eq .FromScanStatus "hold-unreachable") (eq .ToScanStatus "hold-unreachable") }}
<div class="alert alert-warning" role="status">
{{ icon "wifi-off" "size-4 shrink-0" }}
<span>We couldn't reach the hold to fetch scan data. Try again in a moment.</span>
</div>
{{ else if or (eq .FromScanStatus "no-data") (eq .ToScanStatus "no-data") }}
<p class="text-base-content/60">Neither manifest has been scanned yet. Vulnerability comparison will appear after both scans complete.</p>
{{ else }}
<p class="text-base-content/60">Vulnerability scan data isn't available for both manifests.</p>
{{ end }}
{{ else }}
<!-- Fixed Vulns -->
{{ if .FixedVulns }}
<div class="collapse collapse-arrow bg-success/5 border border-success/20 rounded-lg">
<input type="checkbox" checked aria-label="Toggle fixed vulnerabilities ({{ len .FixedVulns }})" />
<div class="collapse-title font-medium text-sm flex items-center gap-2">
{{ icon "shield-check" "size-4 text-success" }}
Fixed ({{ len .FixedVulns }})
</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table table-xs w-full">
<caption class="sr-only">Vulnerabilities fixed in the newer manifest</caption>
<thead>
<tr class="text-xs">
<th scope="col">CVE</th>
<th scope="col">Severity</th>
<th scope="col">Package</th>
<th scope="col">Was</th>
</tr>
</thead>
<tbody>
{{ range .FixedVulns }}
<tr>
<td>
{{ if .CVEURL }}<a href="{{ .CVEURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-xs font-mono">{{ or .CVEID "—" }}</a>
{{ else }}<span class="text-xs font-mono">{{ or .CVEID "—" }}</span>{{ end }}
</td>
<td>
<span class="badge badge-xs {{ if eq .Severity "Critical" }}badge-error{{ else if eq .Severity "High" }}badge-warning{{ else if eq .Severity "Medium" }}badge-info{{ else }}badge-ghost{{ end }}" title="{{ severityLabel .Severity }}">{{ severityLabel .Severity }}</span>
</td>
<td class="text-xs truncate max-w-xs" title="{{ .Package }}">{{ .Package }}</td>
<td class="text-xs font-mono truncate max-w-40" title="{{ .Version }}">{{ .Version }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
{{ end }}
<!-- New Vulns -->
{{ if .NewVulns }}
<div class="collapse collapse-arrow bg-error/5 border border-error/20 rounded-lg">
<input type="checkbox" checked aria-label="Toggle new vulnerabilities ({{ len .NewVulns }})" />
<div class="collapse-title font-medium text-sm flex items-center gap-2">
{{ icon "alert-triangle" "size-4 text-error" }}
New ({{ len .NewVulns }})
</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table table-xs w-full">
<caption class="sr-only">Vulnerabilities new to the newer manifest</caption>
<thead>
<tr class="text-xs">
<th scope="col">CVE</th>
<th scope="col">Severity</th>
<th scope="col">Package</th>
<th scope="col">Version</th>
<th scope="col">Fix</th>
</tr>
</thead>
<tbody>
{{ range .NewVulns }}
<tr>
<td>
{{ if .CVEURL }}<a href="{{ .CVEURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-xs font-mono">{{ or .CVEID "—" }}</a>
{{ else }}<span class="text-xs font-mono">{{ or .CVEID "—" }}</span>{{ end }}
</td>
<td>
<span class="badge badge-xs {{ if eq .Severity "Critical" }}badge-error{{ else if eq .Severity "High" }}badge-warning{{ else if eq .Severity "Medium" }}badge-info{{ else }}badge-ghost{{ end }}" title="{{ severityLabel .Severity }}">{{ severityLabel .Severity }}</span>
</td>
<td class="text-xs truncate max-w-xs" title="{{ .Package }}">{{ .Package }}</td>
<td class="text-xs font-mono truncate max-w-40" title="{{ .Version }}">{{ .Version }}</td>
<td class="text-xs font-mono">{{ .FixedIn }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
{{ end }}
<!-- Unchanged Vulns -->
{{ if .UnchangedVulns }}
<div class="collapse collapse-arrow bg-base-200/50 border border-base-300 rounded-lg">
<input type="checkbox" aria-label="Toggle unchanged vulnerabilities ({{ len .UnchangedVulns }})" />
<div class="collapse-title font-medium text-sm text-base-content/60">
Unchanged ({{ len .UnchangedVulns }})
</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table table-xs w-full">
<caption class="sr-only">Vulnerabilities present in both manifests</caption>
<thead>
<tr class="text-xs">
<th scope="col">CVE</th>
<th scope="col">Severity</th>
<th scope="col">Package</th>
<th scope="col">Version</th>
<th scope="col">Fix</th>
</tr>
</thead>
<tbody>
{{ range .UnchangedVulns }}
<tr>
<td>
{{ if .CVEURL }}<a href="{{ .CVEURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-xs font-mono">{{ or .CVEID "—" }}</a>
{{ else }}<span class="text-xs font-mono">{{ or .CVEID "—" }}</span>{{ end }}
</td>
<td>
<span class="badge badge-xs {{ if eq .Severity "Critical" }}badge-error{{ else if eq .Severity "High" }}badge-warning{{ else if eq .Severity "Medium" }}badge-info{{ else }}badge-ghost{{ end }}" title="{{ severityLabel .Severity }}">{{ severityLabel .Severity }}</span>
</td>
<td class="text-xs truncate max-w-xs" title="{{ .Package }}">{{ .Package }}</td>
<td class="text-xs font-mono truncate max-w-40" title="{{ .Version }}">{{ .Version }}</td>
<td class="text-xs font-mono">{{ .FixedIn }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
{{ end }}
{{ if and (not .FixedVulns) (not .NewVulns) (not .UnchangedVulns) }}
<p class="text-base-content/60">No vulnerabilities found in either manifest</p>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}