Skip to content

Commit 6369404

Browse files
authored
feat: display vulnerability count in stats banner (#312)
1 parent ac387de commit 6369404

4 files changed

Lines changed: 104 additions & 39 deletions

File tree

app/components/PackageVulnerabilityTree.vue

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -188,44 +188,9 @@ function getDepthStyle(depth: string | undefined) {
188188
</div>
189189
</section>
190190

191-
<!-- Loading state - muted -->
192-
<section
193-
v-else-if="status === 'pending' || status === 'idle'"
194-
aria-labelledby="vuln-tree-loading"
195-
>
196-
<div class="rounded-lg border border-border bg-bg-subtle px-4 py-3">
197-
<div class="flex items-center gap-2">
198-
<span
199-
class="i-carbon-circle-dash w-4 h-4 animate-spin motion-reduce:animate-none text-fg-subtle"
200-
aria-hidden="true"
201-
/>
202-
<span class="text-sm text-fg-muted">{{ $t('package.vulnerabilities.scanning_tree') }}</span>
203-
</div>
204-
</div>
205-
</section>
191+
<!-- Loading state - hidden (loading indicator shown in stats banner) -->
206192

207-
<!-- No vulnerabilities found - muted, not attention-grabbing -->
208-
<section
209-
v-else-if="status === 'success' && !hasVulnerabilities"
210-
aria-labelledby="vuln-tree-success"
211-
>
212-
<div class="rounded-lg border border-border bg-bg-subtle px-4 py-3">
213-
<div class="flex items-center gap-2">
214-
<span class="i-carbon-checkmark w-4 h-4 text-fg-subtle" aria-hidden="true" />
215-
<span class="text-sm text-fg-muted">
216-
{{ $t('package.vulnerabilities.no_known', { count: vulnTree?.totalPackages ?? 0 }) }}
217-
</span>
218-
</div>
219-
<!-- Warning if some queries failed -->
220-
<div
221-
v-if="vulnTree?.failedQueries"
222-
class="flex items-center gap-2 mt-2 text-xs text-fg-subtle"
223-
>
224-
<span class="i-carbon-warning w-3 h-3" aria-hidden="true" />
225-
<span>{{ $t('package.vulnerabilities.packages_failed', vulnTree.failedQueries) }}</span>
226-
</div>
227-
</div>
228-
</section>
193+
<!-- No vulnerabilities found - don't show anything (count is shown in stats banner) -->
229194

230195
<!-- Error state - subtle, not alarming -->
231196
<section v-else-if="status === 'error'" aria-labelledby="vuln-tree-error">

app/pages/[...package].vue

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@ const displayVersion = computed(() => {
8585
return pkg.value.versions[latestTag] ?? null
8686
})
8787
88+
// Fetch vulnerability tree (lazy, client-side)
89+
// This is the same composable used by PackageVulnerabilityTree
90+
const {
91+
data: vulnTree,
92+
status: vulnTreeStatus,
93+
fetch: fetchVulnTree,
94+
} = useVulnerabilityTree(packageName, () => displayVersion.value?.version ?? '')
95+
onMounted(() => {
96+
// Fetch vulnerability tree once displayVersion is available
97+
if (displayVersion.value) {
98+
fetchVulnTree()
99+
}
100+
})
101+
watch(
102+
() => displayVersion.value?.version,
103+
() => {
104+
if (displayVersion.value) {
105+
fetchVulnTree()
106+
}
107+
},
108+
)
109+
88110
// Keep latestVersion for comparison (to show "(latest)" badge)
89111
const latestVersion = computed(() => {
90112
if (!pkg.value) return null
@@ -131,6 +153,22 @@ const hasDependencies = computed(() => {
131153
)
132154
})
133155
156+
// Vulnerability count for the stats banner
157+
const vulnCount = computed(() => vulnTree.value?.totalCounts.total ?? 0)
158+
const hasVulnerabilities = computed(() => vulnCount.value > 0)
159+
160+
// Total transitive dependencies count (from either vuln tree or install size)
161+
// Subtract 1 to exclude the root package itself
162+
const totalDepsCount = computed(() => {
163+
if (vulnTree.value) {
164+
return vulnTree.value.totalPackages - 1
165+
}
166+
if (installSize.value) {
167+
return installSize.value.dependencyCount
168+
}
169+
return null
170+
})
171+
134172
const repositoryUrl = computed(() => {
135173
const repo = displayVersion.value?.repository
136174
if (!repo?.url) return null
@@ -728,7 +766,7 @@ defineOgImageComponent('Package', {
728766

729767
<!-- Stats grid -->
730768
<dl
731-
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-border"
769+
class="grid grid-cols-2 sm:grid-cols-5 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-border"
732770
>
733771
<div v-if="pkg.license" class="space-y-1">
734772
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
@@ -744,7 +782,31 @@ defineOgImageComponent('Package', {
744782
{{ $t('package.stats.deps') }}
745783
</dt>
746784
<dd class="font-mono text-sm text-fg flex items-center justify-start gap-2">
747-
{{ getDependencyCount(displayVersion) }}
785+
<!-- Direct deps (muted) -->
786+
<span class="text-fg-muted">{{ getDependencyCount(displayVersion) }}</span>
787+
788+
<!-- Separator and total transitive deps -->
789+
<span class="text-fg-subtle mx-1">/</span>
790+
791+
<ClientOnly>
792+
<span
793+
v-if="
794+
vulnTreeStatus === 'pending' || (installSizeStatus === 'pending' && !vulnTree)
795+
"
796+
class="inline-flex items-center gap-1 text-fg-subtle"
797+
>
798+
<span
799+
class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin"
800+
aria-hidden="true"
801+
/>
802+
</span>
803+
<span v-else-if="totalDepsCount !== null">{{ totalDepsCount }}</span>
804+
<span v-else class="text-fg-subtle">-</span>
805+
<template #fallback>
806+
<span class="text-fg-subtle">-</span>
807+
</template>
808+
</ClientOnly>
809+
748810
<a
749811
v-if="getDependencyCount(displayVersion) > 0"
750812
:href="`https://npmgraph.js.org/?q=${pkg.name}`"
@@ -811,6 +873,42 @@ defineOgImageComponent('Package', {
811873
</dd>
812874
</div>
813875

876+
<!-- Vulnerabilities count -->
877+
<ClientOnly>
878+
<div class="space-y-1">
879+
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
880+
{{ $t('package.stats.vulns') }}
881+
</dt>
882+
<dd class="font-mono text-sm text-fg">
883+
<span
884+
v-if="vulnTreeStatus === 'pending' || vulnTreeStatus === 'idle'"
885+
class="inline-flex items-center gap-1 text-fg-subtle"
886+
>
887+
<span
888+
class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin"
889+
aria-hidden="true"
890+
/>
891+
</span>
892+
<span v-else-if="vulnTreeStatus === 'success'">
893+
<span v-if="hasVulnerabilities" class="text-amber-500">{{ vulnCount }}</span>
894+
<span v-else class="inline-flex items-center gap-1 text-fg-muted">
895+
<span class="i-carbon-checkmark w-3 h-3" aria-hidden="true" />
896+
0
897+
</span>
898+
</span>
899+
<span v-else class="text-fg-subtle">-</span>
900+
</dd>
901+
</div>
902+
<template #fallback>
903+
<div class="space-y-1">
904+
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
905+
{{ $t('package.stats.vulns') }}
906+
</dt>
907+
<dd class="font-mono text-sm text-fg-subtle">-</dd>
908+
</div>
909+
</template>
910+
</ClientOnly>
911+
814912
<div v-if="pkg.time?.modified" class="space-y-1">
815913
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
816914
{{ $t('package.stats.updated') }}

i18n/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"license": "License",
121121
"deps": "Deps",
122122
"install_size": "Install Size",
123+
"vulns": "Vulns",
123124
"updated": "Updated",
124125
"view_dependency_graph": "View dependency graph",
125126
"inspect_dependency_tree": "Inspect dependency tree"

lunaria/files/en-US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"license": "License",
121121
"deps": "Deps",
122122
"install_size": "Install Size",
123+
"vulns": "Vulns",
123124
"updated": "Updated",
124125
"view_dependency_graph": "View dependency graph",
125126
"inspect_dependency_tree": "Inspect dependency tree"

0 commit comments

Comments
 (0)