chore: import upstream snapshot with attribution
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:28:05 +08:00
commit 41cb1c0170
1830 changed files with 38276124 additions and 0 deletions
+245
View File
@@ -0,0 +1,245 @@
import { useMemo, useState, useEffect } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { colorForLabel } from "../lib/colors";
import { callTool } from "../api/rpc";
import type { GraphNode, GraphEdge, RepoInfo } from "../lib/types";
interface Connection {
node: GraphNode;
edgeType: string;
direction: "inbound" | "outbound";
}
interface NodeDetailPanelProps {
node: GraphNode;
allNodes: GraphNode[];
allEdges: GraphEdge[];
project: string | null;
repoInfo: RepoInfo | null;
onClose: () => void;
onNavigate: (node: GraphNode) => void;
}
interface SnippetResult {
source?: string;
start_line?: number;
end_line?: number;
}
function lineSuffix(node: GraphNode): string {
if (!node.start_line) return "";
const end = node.end_line && node.end_line !== node.start_line ? `-L${node.end_line}` : "";
return `#L${node.start_line}${end}`;
}
/* Encode each path segment so an unusual file_path can't break (or escape) the
* URL. The scheme is already https-forced by the backend (/api/repo-info);
* this is defense-in-depth on the path. */
function encodePath(p: string): string {
return p.split("/").map(encodeURIComponent).join("/");
}
/* GitHub (or GitLab) deep-link, or null when we lack remote/path/line info. */
function githubUrl(node: GraphNode, repoInfo: RepoInfo | null): string | null {
if (!repoInfo?.blob_base || !node.file_path) return null;
return `${repoInfo.blob_base}/${encodePath(node.file_path)}${lineSuffix(node)}`;
}
export function NodeDetailPanel({
node,
allNodes,
allEdges,
project,
repoInfo,
onClose,
onNavigate,
}: NodeDetailPanelProps) {
const [code, setCode] = useState<string | null>(null);
const [codeLoading, setCodeLoading] = useState(false);
const [codeError, setCodeError] = useState<string | null>(null);
/* Reset the fetched code whenever the selected node changes. */
useEffect(() => {
setCode(null);
setCodeError(null);
setCodeLoading(false);
}, [node.id]);
const canFetchCode = Boolean(project && node.qualified_name);
const ghUrl = githubUrl(node, repoInfo);
const loadCode = async () => {
if (!project || !node.qualified_name) return;
setCodeLoading(true);
setCodeError(null);
try {
const res = await callTool<SnippetResult>("get_code_snippet", {
qualified_name: node.qualified_name,
project,
});
setCode(res.source ?? "(source not available)");
} catch (e) {
setCodeError(e instanceof Error ? e.message : "Failed to load code");
} finally {
setCodeLoading(false);
}
};
const connections = useMemo(() => {
const nodeMap = new Map<number, GraphNode>();
for (const n of allNodes) nodeMap.set(n.id, n);
const conns: Connection[] = [];
for (const edge of allEdges) {
if (edge.source === node.id) {
const t = nodeMap.get(edge.target);
if (t) conns.push({ node: t, edgeType: edge.type, direction: "outbound" });
}
if (edge.target === node.id) {
const s = nodeMap.get(edge.source);
if (s) conns.push({ node: s, edgeType: edge.type, direction: "inbound" });
}
}
return conns;
}, [node, allNodes, allEdges]);
const outbound = connections.filter((c) => c.direction === "outbound");
const inbound = connections.filter((c) => c.direction === "inbound");
const groupByType = (conns: Connection[]) => {
const g = new Map<string, Connection[]>();
for (const c of conns) g.set(c.edgeType, [...(g.get(c.edgeType) ?? []), c]);
return [...g.entries()].sort((a, b) => b[1].length - a[1].length);
};
return (
<div className="w-full bg-[#0b1920]/95 backdrop-blur-xl flex flex-col h-full min-h-0 overflow-hidden">
{/* Header */}
<div className="px-4 pt-4 pb-3 border-b border-border/30">
<div className="flex items-start justify-between gap-2 mb-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-1.5">
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: colorForLabel(node.label) }} />
<h3 className="text-[13px] font-semibold text-foreground truncate">{node.name}</h3>
</div>
<span
className="inline-block px-2 py-0.5 rounded-md text-[10px] font-medium"
style={{ backgroundColor: colorForLabel(node.label) + "18", color: colorForLabel(node.label) }}
>
{node.label}
</span>
</div>
<button onClick={onClose} className="text-foreground/20 hover:text-foreground/50 transition-colors text-[16px] leading-none p-1">×</button>
</div>
{node.file_path && (
<p className="text-[11px] text-foreground/30 font-mono mt-2 break-all leading-relaxed">
{node.file_path}
{node.start_line ? (
<span className="text-foreground/45">
{" "}:{node.start_line}
{node.end_line && node.end_line !== node.start_line ? `-${node.end_line}` : ""}
</span>
) : null}
</p>
)}
{/* Code actions */}
<div className="flex flex-wrap items-center gap-2 mt-2.5">
{canFetchCode && (
<button
onClick={code ? () => setCode(null) : loadCode}
disabled={codeLoading}
className="px-2.5 py-1 rounded-md bg-primary/15 text-primary text-[11px] font-medium hover:bg-primary/25 transition-colors disabled:opacity-50"
>
{codeLoading ? "Loading…" : code ? "Hide code" : "Show code"}
</button>
)}
{ghUrl && (
<a
href={ghUrl}
target="_blank"
rel="noopener noreferrer"
className="px-2.5 py-1 rounded-md bg-white/[0.05] text-foreground/60 text-[11px] font-medium hover:bg-white/[0.09] hover:text-foreground/90 transition-colors"
>
Open on GitHub
</a>
)}
</div>
{codeError && <p className="text-[11px] text-red-400/80 mt-2">{codeError}</p>}
{code && (
<pre className="mt-2 max-h-[300px] overflow-auto rounded-md bg-black/40 border border-white/[0.06] p-2.5 text-[10.5px] leading-relaxed font-mono text-foreground/75 whitespace-pre">
{code}
</pre>
)}
{/* Stats */}
<div className="flex gap-5 mt-3">
{[
{ label: "Out", value: outbound.length, color: "text-primary" },
{ label: "In", value: inbound.length, color: "text-accent" },
{ label: "Total", value: connections.length, color: "text-foreground" },
].map((s) => (
<div key={s.label}>
<p className="text-[9px] text-foreground/25 uppercase tracking-widest">{s.label}</p>
<p className={`text-[18px] font-semibold tabular-nums ${s.color}`}>{s.value}</p>
</div>
))}
</div>
</div>
{/* Connections */}
<ScrollArea className="flex-1 min-h-0">
<div className="px-4 py-3 space-y-4">
{outbound.length > 0 && (
<ConnectionSection title="References" count={outbound.length} icon="→" groups={groupByType(outbound)} onNavigate={onNavigate} />
)}
{inbound.length > 0 && (
<ConnectionSection title="Referenced by" count={inbound.length} icon="←" groups={groupByType(inbound)} onNavigate={onNavigate} />
)}
{connections.length === 0 && (
<p className="text-[12px] text-foreground/20 text-center py-8">No connections</p>
)}
</div>
</ScrollArea>
</div>
);
}
function ConnectionSection({ title, count, icon, groups, onNavigate }: {
title: string; count: number; icon: string;
groups: [string, Connection[]][];
onNavigate: (n: GraphNode) => void;
}) {
return (
<div>
<p className="text-[11px] font-medium text-foreground/40 mb-2">
{title} <span className="text-foreground/15">({count})</span>
</p>
{groups.map(([type, conns]) => (
<div key={type} className="mb-2">
<p className="text-[9px] text-foreground/20 uppercase tracking-wider mb-1 font-medium">
{type.replace(/_/g, " ").toLowerCase()}
</p>
<div className="space-y-px">
{conns.slice(0, 25).map((c, i) => (
<button
key={`${c.node.id}-${i}`}
onClick={() => onNavigate(c.node)}
className="flex items-center gap-1.5 w-full text-left px-2 py-[4px] rounded-md hover:bg-white/[0.04] text-[11px] transition-colors group"
>
<span className="text-foreground/15 text-[10px] group-hover:text-foreground/30">{icon}</span>
<span className="w-[5px] h-[5px] rounded-full shrink-0" style={{ backgroundColor: colorForLabel(c.node.label) }} />
<span className="text-foreground/55 group-hover:text-foreground/80 truncate">{c.node.name}</span>
<span className="text-foreground/10 ml-auto text-[10px] shrink-0">{c.node.label}</span>
</button>
))}
{conns.length > 25 && (
<p className="text-[10px] text-foreground/15 px-2 py-1">+{conns.length - 25} more</p>
)}
</div>
</div>
))}
</div>
);
}