pdated settings/git-projects/

This commit is contained in:
null 2026-05-25 17:07:09 -05:00
parent 59e739768f
commit 809975cb76
2 changed files with 164 additions and 35 deletions

View File

@ -28,6 +28,7 @@ import { useAuth } from "@/auth/clerk";
import { ForgejoConnectionsTable } from "@/components/git/ForgejoConnectionsTable"; import { ForgejoConnectionsTable } from "@/components/git/ForgejoConnectionsTable";
import { ForgejoRepositoriesTable } from "@/components/git/ForgejoRepositoriesTable"; import { ForgejoRepositoriesTable } from "@/components/git/ForgejoRepositoriesTable";
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog"; import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog";
import { import {
@ -37,6 +38,7 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { getApiBaseUrl } from "@/lib/api-base"; import { getApiBaseUrl } from "@/lib/api-base";
import { cn } from "@/lib/utils";
import { import {
deleteForgejoConnection, deleteForgejoConnection,
deleteForgejoRepository, deleteForgejoRepository,
@ -71,6 +73,72 @@ type AttentionItem = {
href?: string; href?: string;
}; };
type SettingsTone = "accent" | "success" | "warning" | "danger" | "neutral";
const toneStyles: Record<
SettingsTone,
{
panel: string;
icon: string;
rail: string;
text: string;
}
> = {
accent: {
panel:
"border-[color:rgba(96,165,250,0.24)] bg-[linear-gradient(145deg,rgba(96,165,250,0.1),var(--surface)_58%)]",
icon: "border-[color:rgba(96,165,250,0.3)] bg-[color:var(--accent-soft)] text-[color:var(--accent-strong)]",
rail: "bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.82),rgba(96,165,250,0))]",
text: "text-[color:var(--accent-strong)]",
},
success: {
panel:
"border-[color:rgba(52,211,153,0.22)] bg-[linear-gradient(145deg,rgba(52,211,153,0.09),var(--surface)_58%)]",
icon: "border-[color:rgba(52,211,153,0.3)] bg-[color:rgba(52,211,153,0.14)] text-[color:var(--success)]",
rail: "bg-[linear-gradient(90deg,rgba(52,211,153,0),rgba(52,211,153,0.78),rgba(52,211,153,0))]",
text: "text-[color:var(--success)]",
},
warning: {
panel:
"border-[color:rgba(251,191,36,0.24)] bg-[linear-gradient(145deg,rgba(251,191,36,0.09),var(--surface)_58%)]",
icon: "border-[color:rgba(251,191,36,0.32)] bg-[color:rgba(251,191,36,0.14)] text-[color:var(--warning)]",
rail: "bg-[linear-gradient(90deg,rgba(251,191,36,0),rgba(251,191,36,0.82),rgba(251,191,36,0))]",
text: "text-[color:var(--warning)]",
},
danger: {
panel:
"border-[color:rgba(248,113,113,0.24)] bg-[linear-gradient(145deg,rgba(248,113,113,0.09),var(--surface)_58%)]",
icon: "border-[color:rgba(248,113,113,0.3)] bg-[color:rgba(248,113,113,0.12)] text-[color:var(--danger)]",
rail: "bg-[linear-gradient(90deg,rgba(248,113,113,0),rgba(248,113,113,0.78),rgba(248,113,113,0))]",
text: "text-[color:var(--danger)]",
},
neutral: {
panel:
"border-[color:var(--border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0)_72%),var(--surface)]",
icon: "border-[color:var(--border-strong)] bg-[color:var(--surface-muted)] text-muted",
rail: "bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.28),rgba(52,211,153,0.2),rgba(96,165,250,0))]",
text: "text-muted",
},
};
function ToneRail({ tone }: { tone: SettingsTone }) {
return (
<span
className={cn(
"pointer-events-none absolute inset-x-4 top-0 h-px",
toneStyles[tone].rail,
)}
/>
);
}
function panelClass(tone: SettingsTone = "neutral") {
return cn(
"relative overflow-hidden rounded-xl border p-4 shadow-lush md:p-5",
toneStyles[tone].panel,
);
}
const repositoryName = (repository: ForgejoRepository) => const repositoryName = (repository: ForgejoRepository) =>
repository.display_name || `${repository.owner}/${repository.repo}`; repository.display_name || `${repository.owner}/${repository.repo}`;
@ -114,16 +182,23 @@ function SectionHeader({
title, title,
description, description,
action, action,
tone = "accent",
}: { }: {
icon: ReactNode; icon: ReactNode;
title: string; title: string;
description: string; description: string;
action?: ReactNode; action?: ReactNode;
tone?: SettingsTone;
}) { }) {
return ( return (
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 items-start gap-3"> <div className="flex min-w-0 items-start gap-3">
<div className="shrink-0 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-2 text-[color:var(--accent)]"> <div
className={cn(
"shrink-0 rounded-lg border p-2",
toneStyles[tone].icon,
)}
>
{icon} {icon}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
@ -141,14 +216,22 @@ function StatCard({
label, label,
value, value,
caption, caption,
tone = "accent",
}: { }: {
icon: ReactNode; icon: ReactNode;
label: string; label: string;
value: string; value: string;
caption: string; caption: string;
tone?: SettingsTone;
}) { }) {
return ( return (
<div className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush"> <div
className={cn(
"relative overflow-hidden rounded-xl border p-4 shadow-lush",
toneStyles[tone].panel,
)}
>
<ToneRail tone={tone} />
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted"> <p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted">
@ -158,7 +241,7 @@ function StatCard({
{value} {value}
</p> </p>
</div> </div>
<div className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-2 text-[color:var(--accent)]"> <div className={cn("rounded-lg border p-2", toneStyles[tone].icon)}>
{icon} {icon}
</div> </div>
</div> </div>
@ -190,11 +273,32 @@ function HealthSummaryPanel({
: status === "warning" : status === "warning"
? "border-[color:rgba(245,158,11,0.35)] bg-[color:rgba(245,158,11,0.08)] text-[color:var(--warning)]" ? "border-[color:rgba(245,158,11,0.35)] bg-[color:rgba(245,158,11,0.08)] text-[color:var(--warning)]"
: "border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] text-[color:var(--danger)]"; : "border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] text-[color:var(--danger)]";
const metricTones: SettingsTone[] = [
"accent",
"success",
"warning",
"danger",
];
return ( return (
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush md:p-5"> <section
className={cn(
"relative overflow-hidden rounded-xl border border-[color:rgba(96,165,250,0.28)] bg-[linear-gradient(135deg,rgba(96,165,250,0.16),rgba(52,211,153,0.09)_34%,rgba(251,191,36,0.08)_68%,var(--surface)_100%)] p-4 shadow-lush md:p-6",
status === "danger" &&
"border-[color:rgba(248,113,113,0.28)] bg-[linear-gradient(135deg,rgba(248,113,113,0.12),rgba(96,165,250,0.1)_42%,var(--surface)_100%)]",
status === "warning" &&
"border-[color:rgba(251,191,36,0.28)] bg-[linear-gradient(135deg,rgba(251,191,36,0.12),rgba(96,165,250,0.1)_42%,var(--surface)_100%)]",
)}
>
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.85),rgba(52,211,153,0.85),rgba(251,191,36,0))]" />
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between"> <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0"> <div className="min-w-0">
<Badge
variant="accent"
className="mb-3 w-fit shadow-[0_0_24px_rgba(96,165,250,0.16)]"
>
Git operations
</Badge>
<div <div
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-wide ${statusStyles}`} className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-wide ${statusStyles}`}
> >
@ -229,11 +333,15 @@ function HealthSummaryPanel({
</Button> </Button>
</div> </div>
<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-4"> <div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{metrics.map((metric) => ( {metrics.map((metric, index) => (
<div <div
key={metric.label} key={metric.label}
className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] p-3" className={cn(
"relative overflow-hidden rounded-lg border p-3",
toneStyles[metricTones[index % metricTones.length]].panel,
)}
> >
<ToneRail tone={metricTones[index % metricTones.length]} />
<p className="text-xs font-semibold uppercase tracking-wide text-muted"> <p className="text-xs font-semibold uppercase tracking-wide text-muted">
{metric.label} {metric.label}
</p> </p>
@ -281,11 +389,35 @@ function AttentionPanel({
const hasWebhookItems = items.some((item) => item.kind === "webhook"); const hasWebhookItems = items.some((item) => item.kind === "webhook");
return ( return (
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush md:p-5"> <section
className={panelClass(
items.some((item) => item.tone === "danger")
? "danger"
: items.length
? "warning"
: "success",
)}
>
<ToneRail
tone={
items.some((item) => item.tone === "danger")
? "danger"
: items.length
? "warning"
: "success"
}
/>
<SectionHeader <SectionHeader
icon={<ListChecks className="h-4 w-4" />} icon={<ListChecks className="h-4 w-4" />}
title="Needs Attention" title="Needs Attention"
description="Connection and repository signals that can block fresh issue data." description="Connection and repository signals that can block fresh issue data."
tone={
items.some((item) => item.tone === "danger")
? "danger"
: items.length
? "warning"
: "success"
}
action={ action={
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{hasSyncItems ? ( {hasSyncItems ? (
@ -340,7 +472,7 @@ function AttentionPanel({
return ( return (
<div <div
key={group.key} key={group.key}
className="rounded-lg border border-[color:var(--border)]" className="rounded-lg border border-[color:var(--border)] bg-[color:rgba(15,23,36,0.22)]"
> >
<div className="flex items-center justify-between border-b border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2"> <div className="flex items-center justify-between border-b border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2">
<p className="text-xs font-semibold uppercase tracking-wide text-muted"> <p className="text-xs font-semibold uppercase tracking-wide text-muted">
@ -393,11 +525,13 @@ function LastImportPanel({
}) { }) {
const lastImport = importRuns[0] ?? null; const lastImport = importRuns[0] ?? null;
return ( return (
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush md:p-5"> <section className={panelClass("accent")}>
<ToneRail tone="accent" />
<SectionHeader <SectionHeader
icon={<Download className="h-4 w-4" />} icon={<Download className="h-4 w-4" />}
title="Full Import" title="Full Import"
description="Run a complete issue pull when webhooks or scheduled sync need a reset." description="Run a complete issue pull when webhooks or scheduled sync need a reset."
tone="accent"
action={ action={
<Button type="button" variant="outline" size="sm" onClick={onOpen}> <Button type="button" variant="outline" size="sm" onClick={onOpen}>
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
@ -1141,6 +1275,7 @@ export default function GitProjectSettingsPage() {
title="Git Project Settings" title="Git Project Settings"
description="Manage Forgejo connections, tracked repositories, and issue sync." description="Manage Forgejo connections, tracked repositories, and issue sync."
stickyHeader stickyHeader
mainClassName="relative bg-app"
headerActions={ headerActions={
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Link href="/git-projects/issues"> <Link href="/git-projects/issues">
@ -1192,7 +1327,9 @@ export default function GitProjectSettingsPage() {
</div> </div>
} }
> >
<div className="space-y-6"> <div className="pointer-events-none absolute inset-x-0 top-0 h-72 bg-[linear-gradient(135deg,rgba(96,165,250,0.16),rgba(52,211,153,0.1)_38%,rgba(251,191,36,0.08)_64%,rgba(96,165,250,0)_100%)] blur-2xl" />
<div className="relative space-y-6">
{notice ? <NoticeBanner notice={notice} /> : null} {notice ? <NoticeBanner notice={notice} /> : null}
{error ? ( {error ? (
<div className="flex items-start gap-3 rounded-xl border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-3 text-sm text-[color:var(--danger)]"> <div className="flex items-start gap-3 rounded-xl border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-3 text-sm text-[color:var(--danger)]">
@ -1247,11 +1384,13 @@ export default function GitProjectSettingsPage() {
</div> </div>
{/* Connections */} {/* Connections */}
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush md:p-5"> <section className={panelClass("accent")}>
<ToneRail tone="accent" />
<SectionHeader <SectionHeader
icon={<Link2 className="h-4 w-4" />} icon={<Link2 className="h-4 w-4" />}
title="Forgejo Connections" title="Forgejo Connections"
description="URL and token records used by tracked repositories." description="URL and token records used by tracked repositories."
tone="accent"
action={ action={
<Link href="/git-projects/connections"> <Link href="/git-projects/connections">
<Button size="sm" variant="outline"> <Button size="sm" variant="outline">
@ -1274,11 +1413,13 @@ export default function GitProjectSettingsPage() {
</section> </section>
{/* Repositories */} {/* Repositories */}
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush md:p-5"> <section className={panelClass("success")}>
<ToneRail tone="success" />
<SectionHeader <SectionHeader
icon={<GitBranch className="h-4 w-4" />} icon={<GitBranch className="h-4 w-4" />}
title="Tracked Repositories" title="Tracked Repositories"
description="Repositories whose issues are cached and shown in Pipeline." description="Repositories whose issues are cached and shown in Pipeline."
tone="success"
action={ action={
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Link href="/git-projects/repositories/new"> <Link href="/git-projects/repositories/new">
@ -1313,12 +1454,14 @@ export default function GitProjectSettingsPage() {
{/* Webhook Setup */} {/* Webhook Setup */}
<section <section
id="git-project-webhooks" id="git-project-webhooks"
className="scroll-mt-24 rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush md:p-5" className={cn("scroll-mt-24", panelClass("warning"))}
> >
<ToneRail tone="warning" />
<SectionHeader <SectionHeader
icon={<Webhook className="h-4 w-4" />} icon={<Webhook className="h-4 w-4" />}
title="Webhook Setup" title="Webhook Setup"
description="Configure Forgejo webhooks to push issue updates to Pipeline in real time." description="Configure Forgejo webhooks to push issue updates to Pipeline in real time."
tone="warning"
/> />
{repositories.length === 0 ? ( {repositories.length === 0 ? (
@ -1415,11 +1558,13 @@ export default function GitProjectSettingsPage() {
</section> </section>
{/* Scheduled Sync */} {/* Scheduled Sync */}
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush md:p-5"> <section className={panelClass("neutral")}>
<ToneRail tone="neutral" />
<SectionHeader <SectionHeader
icon={<Clock className="h-4 w-4" />} icon={<Clock className="h-4 w-4" />}
title="Scheduled Sync" title="Scheduled Sync"
description="Pipeline runs a background sync for all active repositories every 60 minutes." description="Pipeline runs a background sync for all active repositories every 60 minutes."
tone="neutral"
/> />
<div className="mt-4 pl-0 sm:pl-11"> <div className="mt-4 pl-0 sm:pl-11">
<p className="text-sm text-muted"> <p className="text-sm text-muted">

View File

@ -594,26 +594,10 @@ export default function SettingsPage() {
forceRedirectUrl: "/settings", forceRedirectUrl: "/settings",
signUpForceRedirectUrl: "/settings", signUpForceRedirectUrl: "/settings",
}} }}
title="Settings" title={null}
description="Manage your profile, workspace configuration, and integrations."
stickyHeader stickyHeader
headerActions={
<div className="flex flex-wrap gap-2">
<Link href="/settings/ai-providers">
<Button variant="outline" size="sm">
<KeyRound className="h-4 w-4" />
AI Providers
</Button>
</Link>
<Link href="/settings/git-projects">
<Button variant="outline" size="sm">
<GitBranch className="h-4 w-4" />
Git Settings
</Button>
</Link>
</div>
}
mainClassName="relative bg-app" mainClassName="relative bg-app"
headerClassName="hidden"
> >
<div className="pointer-events-none absolute inset-x-0 top-0 h-72 bg-[linear-gradient(135deg,rgba(96,165,250,0.16),rgba(52,211,153,0.1)_38%,rgba(251,191,36,0.08)_64%,rgba(96,165,250,0)_100%)] blur-2xl" /> <div className="pointer-events-none absolute inset-x-0 top-0 h-72 bg-[linear-gradient(135deg,rgba(96,165,250,0.16),rgba(52,211,153,0.1)_38%,rgba(251,191,36,0.08)_64%,rgba(96,165,250,0)_100%)] blur-2xl" />
@ -666,8 +650,8 @@ export default function SettingsPage() {
</div> </div>
</section> </section>
<div className="grid gap-6 lg:grid-cols-[280px_minmax(0,1fr)]"> <div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_280px]">
<aside className="lg:sticky lg:top-28 lg:self-start"> <aside className="order-2 lg:sticky lg:top-8 lg:self-start">
<nav className="relative overflow-hidden rounded-xl border border-[color:var(--border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0)_72%),var(--surface)] p-3 shadow-lush"> <nav className="relative overflow-hidden rounded-xl border border-[color:var(--border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0)_72%),var(--surface)] p-3 shadow-lush">
<span className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.46),rgba(52,211,153,0.32),rgba(96,165,250,0))]" /> <span className="pointer-events-none absolute inset-x-4 top-0 h-px bg-[linear-gradient(90deg,rgba(96,165,250,0),rgba(96,165,250,0.46),rgba(52,211,153,0.32),rgba(96,165,250,0))]" />
<p className="px-2 pb-2 pt-1 text-xs font-semibold uppercase tracking-wider text-muted"> <p className="px-2 pb-2 pt-1 text-xs font-semibold uppercase tracking-wider text-muted">
@ -712,7 +696,7 @@ export default function SettingsPage() {
</nav> </nav>
</aside> </aside>
<div className="space-y-6"> <div className="order-1 space-y-6">
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(320px,0.7fr)]"> <div className="grid gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(320px,0.7fr)]">
<SettingsPanel <SettingsPanel
id="profile" id="profile"