Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import LfxChip from '~/components/uikit/chip/chip.vue';

const props = defineProps<{
score: number;
healthLabel?: string | null;
unavailable?: boolean;
}>();

Expand All @@ -39,7 +40,13 @@ const props = defineProps<{
// labeled metrics chip). Duplicated rather than shared: only two call sites (row/card) and the
// logic is ~10 lines, so a composable would be more ceremony than the duplication it avoids.
// health-score.vue itself is intentionally left untouched (used elsewhere in the app).
//
// Prefers the API's real healthLabel (v2) when present; falls back to deriving the label from
// the score for sparse rows where the pipe didn't return a label.
const healthScoreLabel = computed(() => {
if (props.healthLabel) {
return props.healthLabel.charAt(0).toUpperCase() + props.healthLabel.slice(1);
}
const score = props.score;
if (score >= 80) return 'Excellent';
if (score >= 60) return 'Healthy';
Expand All @@ -49,6 +56,11 @@ const healthScoreLabel = computed(() => {
});

const healthScoreDotClass = computed(() => {
if (props.healthLabel) {
if (props.healthLabel === 'excellent' || props.healthLabel === 'healthy') return 'bg-health-healthy';
if (props.healthLabel === 'fair' || props.healthLabel === 'concerning') return 'bg-health-concerning';
return 'bg-health-critical';
}
const score = props.score;
if (score >= 60) return 'bg-health-healthy';
if (score >= 20) return 'bg-health-concerning';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,11 @@ SPDX-License-Identifier: MIT
:unavailable="true"
:score="0"
/>
<lfx-popover
<lfx-collection-health-score-pill
v-else
placement="top"
trigger-event="hover"
:allow-pass-through="true"
>
<lfx-collection-health-score-pill :score="project.healthScore" />
<template #content>
<lfx-health-score-details :project="props.project" />
</template>
</lfx-popover>
:score="project.healthScoreV2 ?? 0"
:health-label="project.healthLabel"
/>
</td>
<td class="py-4 px-2 whitespace-nowrap">
{{ formatNumber(props.project.contributorCount) }}
Expand Down Expand Up @@ -161,7 +155,8 @@ SPDX-License-Identifier: MIT
<div class="flex items-center gap-1.5 mt-1 text-xs text-neutral-500 flex-wrap">
<template v-if="isOnboarded">
<lfx-collection-health-score-pill
:score="project.healthScore"
:score="project.healthScoreV2 ?? 0"
:health-label="project.healthLabel"
:unavailable="isHealthScoreUnavailable"
/>
<span class="text-neutral-400">・</span>
Expand Down Expand Up @@ -190,7 +185,6 @@ import LfxTooltip from '~/components/uikit/tooltip/tooltip.vue';
import { formatNumber } from '~/components/shared/utils/formatter';
import { LfxRoutes } from '~/components/shared/types/routes';
import LfxCollectionHealthScorePill from '~/components/modules/collection/components/details/collection-health-score-pill.vue';
import LfxHealthScoreDetails from '~/components/modules/collection/components/details/health-score-details.vue';
import LfxDependencyColumn from '~/components/modules/collection/components/details/dependency-column.vue';
import LfxDependencyDetails from '~/components/modules/collection/components/details/dependency-details.vue';
import LfxBadgeDetails from '~/components/modules/collection/components/details/badge-details.vue';
Expand Down Expand Up @@ -242,12 +236,7 @@ const isOnboarded = computed(() => {
return props.project.contributorCount > 0 || props.project.organizationCount > 0;
});

const isHealthScoreUnavailable = computed(() => {
const { contributorHealthScore, popularityHealthScore, developmentHealthScore, securityHealthScore } = props.project;
return [contributorHealthScore, popularityHealthScore, developmentHealthScore, securityHealthScore].some(
(score) => !score,
);
});
const isHealthScoreUnavailable = computed(() => props.project.healthScoreV2 == null);

const navigateToItem = () => {
if (props.project.type === 'repo') {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!--
Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<div v-if="!isEmpty">
<div class="flex flex-col-reverse sm:flex-row items-start sm:items-center gap-2 sm:gap-4 pb-2">
<h2 class="text-heading-3 font-secondary font-bold">Health breakdown</h2>
<div
v-if="props.healthScoreV2 !== null"
class="flex items-center gap-1.5"
>
<span
class="h-2 w-2 rounded-full shrink-0"
:class="scoreDotColorClass"
/>
<span class="text-sm font-semibold text-neutral-900">{{ scoreLabel }} ({{ props.healthScoreV2 }}/100)</span>
</div>
</div>
<p class="text-xs text-neutral-500 mb-4">
Maintainer availability, known risks and supply chain posture, and whether the project is actively developed or
appropriately stable.
</p>

<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<lfx-health-breakdown-category-card
name="Maintainer Health"
icon="people-group"
:score="props.maintainerHealthScoreV2"
:max-score="40"
/>
<lfx-health-breakdown-category-card
name="Security & Supply Chain"
icon="lock-keyhole"
:score="props.securitySupplyChainScoreV2"
:max-score="35"
/>
<lfx-health-breakdown-category-card
name="Development Activity"
icon="chart-line"
:score="props.developmentActivityScoreV2"
:max-score="25"
/>
</div>

<p class="text-xs text-neutral-400 mb-4">
Per-signal detail (e.g. maintainer responsiveness, bus factor) isn't available as individually reported data yet —
the categories above reflect the full underlying signal set.
</p>

<nuxt-link :to="{ name: LfxRoutes.PROJECT_CONTRIBUTORS }">
<lfx-button
type="tertiary"
size="small"
class="!text-xs"
>
View more in Contributors
<lfx-icon
name="arrow-up-right"
:size="12"
/>
</lfx-button>
</nuxt-link>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import LfxHealthBreakdownCategoryCard from './health-breakdown/category-card.vue';
import LfxButton from '~/components/uikit/button/button.vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import { LfxRoutes } from '~/components/shared/types/routes';
import { getHealthScoreV2Config } from '~~/config/trust-score';

const props = defineProps<{
healthScoreV2: number | null;
healthLabel: string | null;
maintainerHealthScoreV2: number | null;
securitySupplyChainScoreV2: number | null;
developmentActivityScoreV2: number | null;
}>();

const isEmpty = computed(() => props.healthScoreV2 === null);

const scoreLabel = computed(() => getHealthScoreV2Config(props.healthLabel).label);

const scoreDotColorClass = computed(() => {
const label = props.healthLabel;
if (label === 'excellent' || label === 'healthy') return 'bg-positive-500';
if (label === 'fair' || label === 'concerning') return 'bg-warning-500';
return 'bg-negative-500';
});
</script>

<script lang="ts">
export default {
name: 'LfxHealthBreakdownSection',
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!--
Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<lfx-card class="p-4 flex flex-col gap-3">
<div class="flex items-center gap-2">
<lfx-icon
:name="props.icon"
type="regular"
:size="16"
class="text-neutral-500"
/>
<span class="text-sm font-semibold text-neutral-900">{{ props.name }}</span>
</div>
<span
v-if="props.score !== null"
class="text-xs text-neutral-500"
>{{ props.score }}/{{ props.maxScore }}</span
>
<span
v-else
class="text-xs text-neutral-400"
>No data</span
>
<lfx-progress-bar
:values="[progressPercent]"
color="normal"
size="small"
/>
</lfx-card>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import LfxCard from '~/components/uikit/card/card.vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import LfxProgressBar from '~/components/uikit/progress-bar/progress-bar.vue';

const props = defineProps<{
name: string;
icon: string;
score: number | null;
maxScore: number;
}>();

const progressPercent = computed(() => {
if (props.score === null || props.maxScore === 0) return 0;
return (props.score / props.maxScore) * 100;
});
</script>

<script lang="ts">
export default {
name: 'LfxHealthBreakdownCategoryCard',
};
</script>
Loading
Loading