Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-03-11 18:23

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('library_metric_values', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='librarymetricvalue',
name='description',
field=models.TextField(blank=True, null=True),
),
]
1 change: 1 addition & 0 deletions src/backend/api/database/library_metric_values/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class LibraryMetricValue(models.Model):
metric = models.ForeignKey(Metric, on_delete=models.CASCADE)
value = models.JSONField(null=True, blank=True)
evidence = models.TextField(blank=True, null=True)
description = models.TextField(blank=True, null=True)
collected_by = models.CharField(max_length=100, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ class FlatMetricValueSerializer(serializers.ModelSerializer):
# Note: These fields directly reference the ForeignKey IDs
class Meta:
model = LibraryMetricValue
fields = ('library', 'metric', 'value')
fields = ('library', 'metric', 'value', 'description')
6 changes: 5 additions & 1 deletion src/backend/api/database/library_metric_values/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ def domain_comparison(request, domain_id):
if lib_id in by_lib:
by_lib[lib_id]["metrics"][metric_name] = val.value
by_lib[lib_id]["metrics"][f"{metric_name}_evidence"] = val.evidence
by_lib[lib_id]["metrics"][f"{metric_name}_description"] = val.description

return Response(
{
Expand Down Expand Up @@ -191,6 +192,9 @@ def post(self, request, library_id):
if key.endswith("_evidence"):
metric_name = key.replace("_evidence", "")
field_to_update = "evidence"
elif key.endswith("_description"):
metric_name = key.replace("_description", "")
field_to_update = "description"
else:
metric_name = key
field_to_update = "value"
Expand All @@ -204,7 +208,7 @@ def post(self, request, library_id):
if field_to_update == "value":
error_message, validated_value = validate_metric_value(metric, value_to_store)
if error_message:
return Response({"error": error_message}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": f"{metric_name}: {error_message}"}, status=status.HTTP_400_BAD_REQUEST)
value_to_store = validated_value

LibraryMetricValue.objects.update_or_create(
Expand Down
261 changes: 133 additions & 128 deletions src/frontend/src/pages/ComparisonTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface Metric {
metric_ID: string;
metric_name: string;
metric_key?: string | null;
description?: string | null;
}

interface LibraryMetricRow {
Expand Down Expand Up @@ -102,107 +103,74 @@ const ExpandableText: React.FC<{
lines?: 2 | 3;
emptyText?: string;
textStyle?: React.CSSProperties;
}> = ({ text, lines = 2, emptyText = "—", textStyle }) => {
description?: string;
}> = ({ text, lines = 2, emptyText = "—", textStyle, description }) => {
const [open, setOpen] = useState(false);
const [truncated, setTruncated] = useState(false);
const textRef = React.useRef<HTMLDivElement>(null);

const wrapRef = React.useRef<HTMLDivElement>(null);
const textRef = React.useRef<HTMLDivElement>(null);

React.useLayoutEffect(() => {
const el = textRef.current;
if (!el) return;
const clampStyle = lines === 2 ? clamp2Style : clamp3Style;

const check = () => {
setTruncated(
el.scrollHeight > el.clientHeight + 1 ||
el.scrollWidth > el.clientWidth + 1
);
};

check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, [text, lines]);
useEffect(() => {
if (textRef.current) {
const isOverflowing = textRef.current.scrollHeight > textRef.current.clientHeight;
setTruncated(isOverflowing);
}
}, [text]);

React.useEffect(() => {
useEffect(() => {
if (!open) return;

const onDocClick = (e: MouseEvent) => {
if (!wrapRef.current) return;
if (!wrapRef.current.contains(e.target as Node)) {
const handleClick = (e: MouseEvent) => {
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
setOpen(false);
}
};

const onEsc = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};

document.addEventListener("mousedown", onDocClick);
document.addEventListener("keydown", onEsc);
return () => {
document.removeEventListener("mousedown", onDocClick);
document.removeEventListener("keydown", onEsc);
};
window.addEventListener("mousedown", handleClick);
return () => window.removeEventListener("mousedown", handleClick);
}, [open]);

if (!text) {
const showMoreButton = truncated || !!description;

if (!text && !description) {
return <div style={textStyle}>{emptyText}</div>;
}

const clampStyle = lines === 3 ? clamp3Style : clamp2Style;

return (
<div
ref={wrapRef}
style={{
position: "relative",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
minWidth: 0,
width: "100%",
}}
>
<div
ref={textRef}
style={{
...clampStyle,
...textStyle,
width: "100%",
overflowWrap: "anywhere",
}}
title={open ? "" : text}
>
{text}
<div ref={wrapRef} style={{ position: "relative" }}>
<div ref={textRef} style={{ ...clampStyle, ...textStyle }}>
{text || "—"}
</div>

{truncated && (
{showMoreButton && (
<button
type="button"
onClick={() => setOpen((v) => !v)}
style={compactButtonStyle}
onClick={(e) => {
e.stopPropagation();
setOpen(!open);
}}
>
{open ? "less" : "more"}
{open ? "Less" : "More..."}
</button>
)}

{open && (
<div style={overlayCardStyle}>
<div style={{ marginBottom: 8 }}>{text}</div>

<button
type="button"
className="dx-btn dx-btn-outline"
style={{ padding: "5px 8px", fontSize: 12 }}
onClick={async () => {
try {
await navigator.clipboard.writeText(text);
} catch {}
}}
>
Copy
</button>
{description ? (
<>
<div style={{ fontWeight: 700, marginBottom: 4, color: "var(--accent)" }}>
Value:
</div>
<div style={{ marginBottom: 12 }}>{text || "—"}</div>
<div style={{ fontWeight: 700, marginBottom: 4, color: "var(--accent)" }}>
Description:
</div>
<div>{description}</div>
</>
) : (
text
)}
</div>
)}
</div>
Expand Down Expand Up @@ -297,18 +265,22 @@ const ComparisonToolPage: React.FC = () => {
row.github_url || "",
...metricList.map((m) => {
const v = row.metrics[m.metric_name];
const vDesc = row.metrics[`${m.metric_name}_description`];

if (m.metric_key === "gitstats_report") {
const url = v ? String(v) : "";

if (!url) return "";
if (url.startsWith("http://") || url.startsWith("https://")) return url;
if (url.startsWith("/")) return `${SITE_BASE}${url}`;

return url;
}

return v ?? "";
const mainValue = v ?? "";
if (vDesc) {
return `${mainValue} (${vDesc})`;
}

return mainValue;
}),
];

Expand Down Expand Up @@ -465,7 +437,37 @@ const ComparisonToolPage: React.FC = () => {
}}
title={m.metric_name}
>
<div style={clamp2Style}>{m.metric_name}</div>
<div style={{ display: "flex", alignItems: "flex-start", gap: "6px" }}>
<div style={clamp2Style}>{m.metric_name}</div>

{/* Added Help Icon Badge */}
{m.description && (
<span
title={m.description}
style={{
cursor: "help",
fontSize: "10px",
background: "rgba(255, 255, 255, 0.1)",
color: "var(--accent)",
width: "14px",
height: "14px",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
border: "1px solid rgba(255, 255, 255, 0.2)",
opacity: 0.6,
transition: "opacity 0.2s",
flexShrink: 0,
marginTop: "2px"
}}
onMouseEnter={(e) => (e.currentTarget.style.opacity = "1")}
onMouseLeave={(e) => (e.currentTarget.style.opacity = "0.6")}
>
?
</span>
)}
</div>
</th>
))}
</tr>
Expand Down Expand Up @@ -597,56 +599,59 @@ const ComparisonToolPage: React.FC = () => {
</td>

{metricList.map((m) => {
const cellVal = row.metrics[m.metric_name];
const cellVal = row.metrics[m.metric_name];
const cellDesc = row.metrics[`${m.metric_name}_description`];

if (m.metric_key === "gitstats_report") {
const url = cellVal ? String(cellVal) : null;
return (
<td
key={m.metric_ID}
style={{
...metricCellStyle,
whiteSpace: "normal",
wordBreak: "break-word",
}}
title={url || "—"}
>
<div style={clamp2Style}>
{url ? (
<a
href={url}
target="_blank"
rel="noreferrer"
style={{ color: "var(--accent)", textDecoration: "none" }}
>
View report
</a>
) : (
"—"
)}
</div>
</td>
);
}

if (m.metric_key === "gitstats_report") {
const url = cellVal ? String(cellVal) : null;
return (
<td
key={m.metric_ID}
style={{
...metricCellStyle,
whiteSpace: "normal",
wordBreak: "break-word",
}}
title={url || "—"}
>
<div style={clamp2Style}>
{url ? (
<a
href={url}
target="_blank"
rel="noreferrer"
style={{ color: "var(--accent)", textDecoration: "none" }}
>
View report
</a>
) : (
"—"
)}
</div>
</td>
);
}

return (
<td
key={m.metric_ID}
style={{
...metricCellStyle,
whiteSpace: "normal",
wordBreak: "break-word",
}}
title={cellVal != null ? String(cellVal) : "—"}
>
<ExpandableText
text={cellVal != null ? String(cellVal) : ""}
lines={3}
emptyText="—"
/>
</td>
);
})}
return (
<td
key={m.metric_ID}
style={{
...metricCellStyle,
whiteSpace: "normal",
wordBreak: "break-word",
position: "relative",
}}
title={cellDesc ? `Value: ${cellVal}\n\nDescription: ${cellDesc}` : String(cellVal || "—")}
>
<ExpandableText
text={cellVal != null ? String(cellVal) : ""}
lines={3}
emptyText="—"
description={cellDesc ? String(cellDesc) : undefined}
/>
</td>
);
})}
</tr>
))}
</tbody>
Expand Down
Loading
Loading