dashboard: REPOS tier tab + Repos page shell
Inserts a 6th tier tab between AGENT and INFRA (Tier type + TIER_TABS + rail icon). Wires two new sidebar/canvas components with the same list+detail pattern as loops/research: - RepoList: header (provider count · repo count), + button opens the connection wizard, groups repos by provider connection (empty state prompts the user to connect the first). Fetches /api/repos/connections and /api/repos — those routes land in tasks #12-14. - RepoCanvas: repo detail (name, owner, private badge, description, stars/forks/branch/updated, clone URL with copy, open-on-provider link, last-synced footer). Empty + loading + error placeholders. - RepoConnectionWizardStub: minimal 'coming next' modal so the + button is wired end-to-end; real wizard replaces it in task #15. Sidebar header branch updated so the tier renders its own header. Build is clean; the sidebar is fully functional once the backend endpoints respond.
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
"use client";
|
||||
|
||||
// Canvas for a selected repo. Renders detail (name, owner, description,
|
||||
// default branch, private/public, stars/forks, updated_at, clone URL with copy
|
||||
// button, external link) plus a placeholder actions row for future work
|
||||
// ("run agent against this repo", "clone into workspace drive", etc.).
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Copy, ExternalLink, GitBranch, Lock, Star } from "lucide-react";
|
||||
|
||||
const mono =
|
||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||
|
||||
interface RepoDetail {
|
||||
id: string;
|
||||
connection_id: string;
|
||||
provider: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
private: boolean;
|
||||
description: string | null;
|
||||
default_branch: string | null;
|
||||
clone_url: string | null;
|
||||
html_url: string | null;
|
||||
stars: number;
|
||||
forks: number;
|
||||
updated_at: string | null;
|
||||
last_synced_at: string | null;
|
||||
}
|
||||
|
||||
export function RepoCanvas({
|
||||
selectedId,
|
||||
refreshKey,
|
||||
}: {
|
||||
selectedId: string | null;
|
||||
refreshKey: number;
|
||||
}) {
|
||||
const [repo, setRepo] = useState<RepoDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function load() {
|
||||
if (!selectedId) {
|
||||
setRepo(null);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const r = await fetch(`/api/repos/${selectedId}`);
|
||||
if (!r.ok) throw new Error(`load failed (${r.status})`);
|
||||
const d = (await r.json()) as RepoDetail;
|
||||
if (!cancelled) setRepo(d);
|
||||
} catch (e) {
|
||||
if (!cancelled) setError(e instanceof Error ? e.message : "load failed");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
void load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedId, refreshKey]);
|
||||
|
||||
if (!selectedId) return <Placeholder />;
|
||||
if (loading && !repo) return <PlaceholderText>Loading repo…</PlaceholderText>;
|
||||
if (error) return <PlaceholderText color="#ff8a7a">{error}</PlaceholderText>;
|
||||
if (!repo) return <Placeholder />;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
overflow: "auto",
|
||||
background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)",
|
||||
padding: 32,
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 820, margin: "0 auto", display: "flex", flexDirection: "column", gap: 24 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
fontFamily: mono,
|
||||
fontSize: 11,
|
||||
color: "#8a8a92",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#c98af0", fontWeight: 700 }}>{repo.provider}</span>
|
||||
<span style={{ opacity: 0.5 }}>·</span>
|
||||
<span>{repo.owner}</span>
|
||||
{repo.private ? (
|
||||
<>
|
||||
<span style={{ opacity: 0.5 }}>·</span>
|
||||
<span style={{ color: "#c8a464", display: "inline-flex", alignItems: "center", gap: 4 }}>
|
||||
<Lock size={11} /> private
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<h1 style={{ fontSize: 30, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.02em", margin: 0 }}>
|
||||
{repo.name}
|
||||
</h1>
|
||||
{repo.description ? (
|
||||
<p style={{ fontFamily: mono, fontSize: 13, color: "#b5b5bd", margin: 0, lineHeight: 1.6 }}>
|
||||
{repo.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 12 }}>
|
||||
<Stat icon={<GitBranch size={13} />} label="Default branch" value={repo.default_branch ?? "—"} />
|
||||
<Stat icon={<Star size={13} />} label="Stars" value={String(repo.stars)} />
|
||||
<Stat icon={<GitBranch size={13} />} label="Forks" value={String(repo.forks)} />
|
||||
<Stat
|
||||
icon={<GitBranch size={13} />}
|
||||
label="Updated"
|
||||
value={repo.updated_at ? new Date(repo.updated_at).toLocaleDateString() : "—"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{repo.clone_url ? (
|
||||
<Section header="Clone URL">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "10px 12px",
|
||||
borderRadius: 8,
|
||||
background: "#101014",
|
||||
border: "1px solid rgba(255,255,255,.06)",
|
||||
fontFamily: mono,
|
||||
fontSize: 12,
|
||||
color: "#eaeaee",
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{repo.clone_url}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void navigator.clipboard?.writeText(repo.clone_url ?? "");
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1200);
|
||||
}}
|
||||
aria-label="Copy clone URL"
|
||||
style={{
|
||||
flex: "none",
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,.1)",
|
||||
background: "transparent",
|
||||
color: copied ? "#7fd0a0" : "#cfcfd5",
|
||||
cursor: "pointer",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Copy size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
{repo.html_url ? (
|
||||
<a
|
||||
href={repo.html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "8px 14px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(255,255,255,.14)",
|
||||
background: "transparent",
|
||||
color: "#cfcfd5",
|
||||
fontSize: 12.5,
|
||||
fontWeight: 600,
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
<ExternalLink size={12} /> Open on {repo.provider}
|
||||
</a>
|
||||
) : null}
|
||||
{/* Future: "Run agent against this repo" + "Clone to shared drive" */}
|
||||
</div>
|
||||
|
||||
<p style={{ ...hintStyle, textAlign: "center", marginTop: 12 }}>
|
||||
{repo.last_synced_at
|
||||
? `Last synced ${new Date(repo.last_synced_at).toLocaleString()}`
|
||||
: "Not synced yet"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({ header, children }: { header: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 10,
|
||||
letterSpacing: ".12em",
|
||||
color: "#5a5a62",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{header}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
padding: "10px 12px",
|
||||
borderRadius: 8,
|
||||
background: "rgba(255,255,255,.03)",
|
||||
border: "1px solid rgba(255,255,255,.06)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 9.5,
|
||||
letterSpacing: ".12em",
|
||||
textTransform: "uppercase",
|
||||
color: "#6a6a72",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: "#eaeaee" }}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Placeholder() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 48,
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 420, textAlign: "center" }}>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: "#f3f3f5", marginBottom: 10 }}>
|
||||
Repositories
|
||||
</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 12, color: "#8a8a92", lineHeight: 1.6 }}>
|
||||
Pick a repo on the left, or hit the + in the sidebar to connect a
|
||||
GitHub, Gitea or GitLab account and pull an organization's repos.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaceholderText({
|
||||
children,
|
||||
color = "#8a8a92",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
color?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontFamily: mono,
|
||||
fontSize: 12,
|
||||
color,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hintStyle: React.CSSProperties = {
|
||||
fontFamily: mono,
|
||||
fontSize: 11,
|
||||
color: "#8a8a92",
|
||||
};
|
||||
Reference in New Issue
Block a user