-
-
Notifications
You must be signed in to change notification settings - Fork 388
feat(ui): add health score widget powered by npm Pulse #2333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f23f805
e27282a
0bc148e
8972153
60833c7
bcf3455
88bc6b1
056fcc3
45c3238
f60b04c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| <script setup lang="ts"> | ||
| interface HealthScoreDimension { | ||
| score: number | ||
| weight: number | ||
| } | ||
|
|
||
| interface HealthScoreResponse { | ||
| package: string | ||
| version: string | ||
| score: number | ||
| grade: 'A' | 'B' | 'C' | 'D' | 'F' | ||
| dimensions: { | ||
| maintenance: HealthScoreDimension | ||
| quality: HealthScoreDimension | ||
| security: HealthScoreDimension | ||
| popularity: HealthScoreDimension | ||
| } | ||
| analyzedAt: string | ||
| } | ||
|
|
||
| const props = defineProps<{ | ||
| packageName: string | ||
| version?: string | ||
| }>() | ||
|
|
||
| const { data, status } = useFetch<HealthScoreResponse>( | ||
| () => { | ||
| const base = `https://npm-pulse.vercel.app/api/v1/score/${props.packageName}` | ||
| return props.version ? `${base}?version=${props.version}` : base | ||
| }, | ||
| { | ||
| key: () => `health-score-${props.packageName}-${props.version ?? 'latest'}`, | ||
| server: false, | ||
| lazy: true, | ||
| }, | ||
| ) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const isLoading = computed(() => status.value === 'pending' || status.value === 'idle') | ||
| const isError = computed(() => status.value === 'error') | ||
|
|
||
| function gradeColor(grade: string | undefined): string { | ||
| switch (grade) { | ||
| case 'A': | ||
| return 'text-emerald-500' | ||
| case 'B': | ||
| return 'text-lime-500' | ||
| case 'C': | ||
| return 'text-amber-500' | ||
| case 'D': | ||
| return 'text-orange-500' | ||
| case 'F': | ||
| return 'text-red-500' | ||
| default: | ||
| return 'text-fg-subtle' | ||
| } | ||
| } | ||
|
|
||
| function scoreBarColor(score: number): string { | ||
| if (score >= 80) return 'bg-emerald-500' | ||
| if (score >= 60) return 'bg-lime-500' | ||
| if (score >= 40) return 'bg-amber-500' | ||
| if (score >= 20) return 'bg-orange-500' | ||
| return 'bg-red-500' | ||
| } | ||
|
|
||
| const dimensions = computed(() => { | ||
| if (!data.value?.dimensions) return [] | ||
| const d = data.value.dimensions | ||
| return [ | ||
| { | ||
| key: 'maintenance', | ||
| label: $t('package.health_score.dimension_maintenance'), | ||
| score: d.maintenance?.score ?? 0, | ||
| weight: d.maintenance?.weight ?? 0, | ||
| }, | ||
| { | ||
| key: 'quality', | ||
| label: $t('package.health_score.dimension_quality'), | ||
| score: d.quality?.score ?? 0, | ||
| weight: d.quality?.weight ?? 0, | ||
| }, | ||
| { | ||
| key: 'security', | ||
| label: $t('package.health_score.dimension_security'), | ||
| score: d.security?.score ?? 0, | ||
| weight: d.security?.weight ?? 0, | ||
| }, | ||
| { | ||
| key: 'popularity', | ||
| label: $t('package.health_score.dimension_popularity'), | ||
| score: d.popularity?.score ?? 0, | ||
| weight: d.popularity?.weight ?? 0, | ||
| }, | ||
| ] | ||
|
Comment on lines
+66
to
+94
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clamp score values to 0–100 before binding to progress UI. Scores from a remote API are trusted as-is. If a value is out of range, Suggested fix+function clampScore(score: number): number {
+ if (!Number.isFinite(score))
+ return 0
+ return Math.max(0, Math.min(100, score))
+}
+
const dimensions = computed(() => {
if (!data.value?.dimensions) return []
const d = data.value.dimensions
return [
- { key: 'maintenance', label: $t('package.health_score.dimension_maintenance'), score: d.maintenance?.score ?? 0, weight: d.maintenance?.weight ?? 0 },
- { key: 'quality', label: $t('package.health_score.dimension_quality'), score: d.quality?.score ?? 0, weight: d.quality?.weight ?? 0 },
- { key: 'security', label: $t('package.health_score.dimension_security'), score: d.security?.score ?? 0, weight: d.security?.weight ?? 0 },
- { key: 'popularity', label: $t('package.health_score.dimension_popularity'), score: d.popularity?.score ?? 0, weight: d.popularity?.weight ?? 0 },
+ { key: 'maintenance', label: $t('package.health_score.dimension_maintenance'), score: clampScore(d.maintenance?.score ?? 0), weight: d.maintenance?.weight ?? 0 },
+ { key: 'quality', label: $t('package.health_score.dimension_quality'), score: clampScore(d.quality?.score ?? 0), weight: d.quality?.weight ?? 0 },
+ { key: 'security', label: $t('package.health_score.dimension_security'), score: clampScore(d.security?.score ?? 0), weight: d.security?.weight ?? 0 },
+ { key: 'popularity', label: $t('package.health_score.dimension_popularity'), score: clampScore(d.popularity?.score ?? 0), weight: d.popularity?.weight ?? 0 },
]
})Also applies to: 134-143 |
||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <CollapsibleSection | ||
| :title="$t('package.health_score.title')" | ||
| :subtitle="$t('package.health_score.algorithm_subtitle')" | ||
| :is-loading="isLoading" | ||
| icon="i-lucide:activity" | ||
| id="health-score" | ||
| > | ||
| <!-- Error state --> | ||
| <div v-if="isError" class="flex items-center gap-2 text-fg-subtle text-sm"> | ||
| <span class="i-lucide:circle-alert w-4 h-4" aria-hidden="true" /> | ||
| <span>{{ $t('package.health_score.error') }}</span> | ||
| </div> | ||
|
|
||
| <!-- Score display --> | ||
| <div v-else-if="data" class="space-y-3"> | ||
| <!-- Score + grade --> | ||
| <div class="flex items-center gap-3"> | ||
| <TooltipApp :text="$t('package.health_score.score_tooltip')" strategy="fixed"> | ||
| <div class="flex items-baseline gap-1 cursor-default" tabindex="0"> | ||
| <span class="font-mono text-2xl font-bold text-fg leading-none">{{ data.score }}</span> | ||
| <span class="text-xs text-fg-subtle">/100</span> | ||
| </div> | ||
| </TooltipApp> | ||
|
|
||
| <TooltipApp | ||
| :text="$t('package.health_score.grade_tooltip', { grade: data.grade })" | ||
| strategy="fixed" | ||
| > | ||
| <TagStatic | ||
| tabindex="0" | ||
| :class="gradeColor(data.grade)" | ||
| class="font-mono font-bold text-sm! min-w-8 justify-center" | ||
| variant="ghost" | ||
| > | ||
| {{ data.grade }} | ||
| </TagStatic> | ||
| </TooltipApp> | ||
| </div> | ||
|
|
||
| <!-- Dimension bars --> | ||
| <ul | ||
| class="space-y-2 list-none m-0 p-0" | ||
| :aria-label="$t('package.health_score.dimensions_label')" | ||
| > | ||
| <li v-for="dim in dimensions" :key="dim.key"> | ||
| <div class="flex items-center justify-between mb-0.5"> | ||
| <span class="text-xs text-fg-subtle"> | ||
| {{ dim.label }} | ||
| <span class="text-fg-muted">({{ dim.weight }}%)</span> | ||
| </span> | ||
| <span class="font-mono text-xs text-fg-muted">{{ dim.score }}</span> | ||
| </div> | ||
| <div | ||
| class="h-1.5 w-full rounded-full overflow-hidden" | ||
| style="background-color: var(--border)" | ||
| role="progressbar" | ||
| :aria-valuenow="dim.score" | ||
| aria-valuemin="0" | ||
| aria-valuemax="100" | ||
| :aria-label="`${dim.label}: ${dim.score}/100`" | ||
| > | ||
| <div | ||
| class="h-full rounded-full transition-all duration-500" | ||
| :class="scoreBarColor(dim.score)" | ||
| :style="{ width: `${dim.score}%` }" | ||
| /> | ||
| </div> | ||
| </li> | ||
| </ul> | ||
|
|
||
| <!-- Footer: link to npm Pulse (homepage, not raw JSON) --> | ||
| <a | ||
| href="https://npm-pulse.vercel.app" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| class="inline-flex items-center gap-1 text-xs text-fg-subtle hover:text-fg transition-colors duration-150 underline underline-offset-2 decoration-fg-subtle/40" | ||
| > | ||
| {{ $t('package.health_score.powered_by') }} | ||
| <span class="i-lucide:external-link w-3 h-3" aria-hidden="true" /> | ||
| </a> | ||
| </div> | ||
| </CollapsibleSection> | ||
| </template> | ||
Uh oh!
There was an error while loading. Please reload this page.