Skip to content

Commit b547d5c

Browse files
committed
feat: display vulnerability count in stats banner
1 parent 99cc5a8 commit b547d5c

3 files changed

Lines changed: 103 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
@@ -83,6 +83,28 @@ const displayVersion = computed(() => {
8383
return pkg.value.versions[latestTag] ?? null
8484
})
8585
86+
// Fetch vulnerability tree (lazy, client-side)
87+
// This is the same composable used by PackageVulnerabilityTree
88+
const {
89+
data: vulnTree,
90+
status: vulnTreeStatus,
91+
fetch: fetchVulnTree,
92+
} = useVulnerabilityTree(packageName, () => displayVersion.value?.version ?? '')
93+
onMounted(() => {
94+
// Fetch vulnerability tree once displayVersion is available
95+
if (displayVersion.value) {
96+
fetchVulnTree()
97+
}
98+
})
99+
watch(
100+
() => displayVersion.value?.version,
101+
() => {
102+
if (displayVersion.value) {
103+
fetchVulnTree()
104+
}
105+
},
106+
)
107+
86108
// Keep latestVersion for comparison (to show "(latest)" badge)
87109
const latestVersion = computed(() => {
88110
if (!pkg.value) return null
@@ -129,6 +151,22 @@ const hasDependencies = computed(() => {
129151
)
130152
})
131153
154+
// Vulnerability count for the stats banner
155+
const vulnCount = computed(() => vulnTree.value?.totalCounts.total ?? 0)
156+
const hasVulnerabilities = computed(() => vulnCount.value > 0)
157+
158+
// Total transitive dependencies count (from either vuln tree or install size)
159+
// Subtract 1 to exclude the root package itself
160+
const totalDepsCount = computed(() => {
161+
if (vulnTree.value) {
162+
return vulnTree.value.totalPackages - 1
163+
}
164+
if (installSize.value) {
165+
return installSize.value.dependencyCount
166+
}
167+
return null
168+
})
169+
132170
const repositoryUrl = computed(() => {
133171
const repo = displayVersion.value?.repository
134172
if (!repo?.url) return null
@@ -726,7 +764,7 @@ defineOgImageComponent('Package', {
726764

727765
<!-- Stats grid -->
728766
<dl
729-
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"
767+
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"
730768
>
731769
<div v-if="pkg.license" class="space-y-1">
732770
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
@@ -742,7 +780,31 @@ defineOgImageComponent('Package', {
742780
{{ $t('package.stats.deps') }}
743781
</dt>
744782
<dd class="font-mono text-sm text-fg flex items-center justify-start gap-2">
745-
{{ getDependencyCount(displayVersion) }}
783+
<!-- Direct deps (muted) -->
784+
<span class="text-fg-muted">{{ getDependencyCount(displayVersion) }}</span>
785+
786+
<!-- Separator and total transitive deps -->
787+
<span class="text-fg-subtle mx-1">/</span>
788+
789+
<ClientOnly>
790+
<span
791+
v-if="
792+
vulnTreeStatus === 'pending' || (installSizeStatus === 'pending' && !vulnTree)
793+
"
794+
class="inline-flex items-center gap-1 text-fg-subtle"
795+
>
796+
<span
797+
class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin"
798+
aria-hidden="true"
799+
/>
800+
</span>
801+
<span v-else-if="totalDepsCount !== null">{{ totalDepsCount }}</span>
802+
<span v-else class="text-fg-subtle">-</span>
803+
<template #fallback>
804+
<span class="text-fg-subtle">-</span>
805+
</template>
806+
</ClientOnly>
807+
746808
<a
747809
v-if="getDependencyCount(displayVersion) > 0"
748810
:href="`https://npmgraph.js.org/?q=${pkg.name}`"
@@ -809,6 +871,42 @@ defineOgImageComponent('Package', {
809871
</dd>
810872
</div>
811873

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

i18n/locales/en.json

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

0 commit comments

Comments
 (0)