diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 79defa6..330e83a 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -9,9 +9,10 @@ import { useRouter } from "next/navigation"; import { useAuth, useUser } from "@/auth/clerk"; import { useQueryClient } from "@tanstack/react-query"; import { - ArrowRight, + ArrowUpRight, Bot, Building2, + CheckCircle2, Folder, GitBranch, Globe, @@ -21,6 +22,7 @@ import { RefreshCw, RotateCcw, Save, + ShieldAlert, SlidersHorizontal, Tags, Trash2, @@ -37,11 +39,13 @@ import { } from "@/api/generated/users/users"; import { ApiError, customFetch } from "@/api/mutator"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog"; import { Input } from "@/components/ui/input"; import SearchableSelect from "@/components/ui/searchable-select"; import { getSupportedTimezones } from "@/lib/timezones"; +import { cn } from "@/lib/utils"; import { useOrganizationMembership } from "@/lib/use-organization-membership"; type ClerkGlobal = { @@ -53,51 +57,255 @@ type SettingsLink = { label: string; description: string; icon: LucideIcon; + tone: SettingsTone; }; -function SettingsLinkSection({ +type SettingsTone = "accent" | "success" | "warning" | "danger" | "neutral"; + +type SettingsNavItem = { + href: string; + label: string; + description: string; + icon: LucideIcon; + tone: SettingsTone; + count?: number; +}; + +const toneStyles: Record< + SettingsTone, + { + card: string; + icon: string; + rail: string; + text: string; + } +> = { + accent: { + card: "border-[color:rgba(96,165,250,0.24)] bg-[linear-gradient(145deg,rgba(96,165,250,0.12),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: { + card: "border-[color:rgba(52,211,153,0.22)] bg-[linear-gradient(145deg,rgba(52,211,153,0.1),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: { + card: "border-[color:rgba(251,191,36,0.24)] bg-[linear-gradient(145deg,rgba(251,191,36,0.1),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: { + card: "border-[color:rgba(248,113,113,0.24)] bg-[linear-gradient(145deg,rgba(248,113,113,0.1),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: { + card: "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 StatusBanner({ + children, + tone, +}: { + children: React.ReactNode; + tone: Exclude; +}) { + const toneClass = + tone === "success" + ? "border-[color:rgba(52,211,153,0.35)] bg-[color:rgba(52,211,153,0.08)] text-[color:var(--success)]" + : "border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] text-[color:var(--danger)]"; + + return ( +
+ {children} +
+ ); +} + +function SettingsPanel({ + id, title, description, - items, + icon: Icon, + tone = "neutral", + children, + className, }: { + id: string; title: string; description: string; + icon: LucideIcon; + tone?: SettingsTone; + children: React.ReactNode; + className?: string; +}) { + return ( +
+ +
+ + + +
+

{title}

+

{description}

+
+
+ {children} +
+ ); +} + +function SettingsSummaryCard({ + label, + value, + detail, + icon: Icon, + tone, +}: { + label: string; + value: string; + detail: string; + icon: LucideIcon; + tone: SettingsTone; +}) { + return ( +
+ +
+
+

+ {label} +

+

+ {value} +

+

{detail}

+
+ + + +
+
+ ); +} + +function SettingsLinkSection({ + id, + title, + description, + icon, + tone, + items, +}: { + id: string; + title: string; + description: string; + icon: LucideIcon; + tone: SettingsTone; items: SettingsLink[]; }) { if (items.length === 0) return null; return ( -
-
-

{title}

-

{description}

-
-
+ +
{items.map((item) => { const Icon = item.icon; return ( - - - - - - {item.label} + + + + - - {item.description} + + + + {item.label} + + + + + {item.description} + - ); })}
-
+ ); } @@ -247,6 +455,7 @@ export default function SettingsPage() { label: "Organization", description: "Manage members, invites, access, and organization details.", icon: Building2, + tone: "accent", }, { href: "/board-groups", @@ -254,6 +463,7 @@ export default function SettingsPage() { description: "Group related boards so agents and operators share context.", icon: Folder, + tone: "success", }, ...(isAdmin ? [ @@ -262,6 +472,7 @@ export default function SettingsPage() { label: "Tags", description: "Maintain reusable task labels used across boards.", icon: Tags, + tone: "warning" as const, }, { href: "/custom-fields", @@ -269,6 +480,7 @@ export default function SettingsPage() { description: "Configure organization-level task metadata and board bindings.", icon: SlidersHorizontal, + tone: "accent" as const, }, ] : []), @@ -281,6 +493,7 @@ export default function SettingsPage() { description: "Manage model credentials, endpoints, and provider usage tracking.", icon: KeyRound, + tone: "accent", }, { href: "/settings/git-projects", @@ -288,12 +501,14 @@ export default function SettingsPage() { description: "Review Forgejo sync health and Git Project automation settings.", icon: GitBranch, + tone: "success", }, { href: "/git-projects/connections", label: "Git connections", description: "Connect Git providers used for repository and issue sync.", icon: GitBranch, + tone: "warning", }, ]; @@ -305,16 +520,72 @@ export default function SettingsPage() { description: "Provision and monitor the agents available to this organization.", icon: Bot, + tone: "accent" as const, }, { href: "/gateways", label: "Gateways", description: "Manage gateway connections used by boards and agents.", icon: Network, + tone: "success" as const, }, ] : []; + const settingsNavItems: SettingsNavItem[] = [ + { + href: "#profile", + label: "Account", + description: "Name, email, and timezone", + icon: User, + tone: "accent", + }, + { + href: "#profile-source", + label: "Profile source", + description: profile?.use_forgejo_profile + ? "Forgejo enabled" + : "App profile enabled", + icon: GitBranch, + tone: profile?.use_forgejo_profile ? "success" : "neutral", + }, + { + href: "#workspace", + label: "Workspace", + description: "Organization structure", + icon: Building2, + tone: "success", + count: workspaceItems.length, + }, + { + href: "#integrations", + label: "Integrations", + description: "Models and Git providers", + icon: KeyRound, + tone: "accent", + count: integrationItems.length, + }, + ...(operationItems.length + ? [ + { + href: "#operations", + label: "Operations", + description: "Runtime admin tools", + icon: Network, + tone: "warning" as const, + count: operationItems.length, + }, + ] + : []), + { + href: "#danger-zone", + label: "Danger zone", + description: "Permanent account actions", + icon: ShieldAlert, + tone: "danger", + }, + ]; + return ( <> + + + + + + + + } + mainClassName="relative bg-app" > -
-
-

Profile

-

- Keep your identity and timezone up to date. -

+
-
-
-
- - { - setName(event.target.value); - setNameEdited(true); - }} - placeholder="Your name" - disabled={isSaving} - className="border-input [--input-bg:var(--card)] [--input-text:var(--card-foreground)] placeholder:text-muted-foreground focus-visible:ring-ring" - /> -
-
- - { - setTimezone(value); - setTimezoneEdited(true); - }} - options={timezoneOptions} - placeholder="Select timezone" - searchPlaceholder="Search timezones..." - emptyMessage="No matching timezones." - disabled={isSaving} - triggerClassName="h-11 w-full px-3 py-2" - contentClassName="shadow-lg" - itemClassName="px-4 py-3" - /> -
+
+
+
+
+
+ + Account & workspace + +

+ Pipeline settings +

+

+ Account identity, organization structure, provider + connections, and runtime administration. +

- -
- - + + +
+
+
- {saveError ? ( -
- {saveError} +
+ -
- - + +
+
+ + { + setName(event.target.value); + setNameEdited(true); + }} + placeholder="Your name" + disabled={isSaving} + className="border-[color:var(--border-strong)]" + /> +
+
+ + { + setTimezone(value); + setTimezoneEdited(true); + }} + options={timezoneOptions} + placeholder="Select timezone" + searchPlaceholder="Search timezones..." + emptyMessage="No matching timezones." + disabled={isSaving} + triggerClassName="h-11 w-full px-3 py-2" + contentClassName="shadow-lg" + itemClassName="px-4 py-3" + /> +
+
+ +
+ + +
+ + {saveError ? ( + {saveError} + ) : null} + {saveSuccess ? ( + {saveSuccess} + ) : null} + +
+ + +
+ + + + +
+ {profile?.use_forgejo_profile ? ( +
+
+ {profile.forgejo_avatar_url ? ( + // eslint-disable-next-line @next/next/no-img-element + Forgejo avatar + ) : ( + + + + )} +
+

+ + Forgejo profile active +

+

+ {profile.forgejo_display_name ?? + "No Forgejo name returned"} +

+
+
+
+ + +
+
+ ) : ( +
+

+ App profile active +

+

+ Forgejo can supply your display name and avatar. +

+ +
+ )} + + {forgejoSyncError ? ( + + {forgejoSyncError} + + ) : null} + {forgejoSyncSuccess ? ( + + {forgejoSyncSuccess} + + ) : null} +
+
- -
-
-

- - Profile Source -

-

- Use your Forgejo account name and avatar throughout the app. - Requires an active Forgejo connection. -

+ -
- {profile?.use_forgejo_profile ? ( -
- {profile.forgejo_avatar_url ? ( - // eslint-disable-next-line @next/next/no-img-element - Forgejo avatar - ) : null} - - Using Forgejo profile:{" "} - - {profile.forgejo_display_name ?? "—"} - - - - -
- ) : ( -
- -
- )} + - {forgejoSyncError ? ( -
- {forgejoSyncError} -
- ) : null} - {forgejoSyncSuccess ? ( -
- {forgejoSyncSuccess} -
- ) : null} -
-
+ - - - - - - -
-

- Delete account -

-

- This permanently removes your Pipeline account and related - personal data. This action cannot be undone. -

-
- +
+

+ This action cannot be undone. Your account is removed from + Pipeline after confirmation. +

+ +
+
-
+