Server Deploy / deploy (push) Successful in 1m12s
/settings/instance held two cards — Members and Single sign-on — behind a sidebar entry called "Instance", next to one called "Settings". Nothing in either name told you which held what, and the split left both pages thin. They are now the Access group at the top of /settings, above Monitoring and Integrations. Six cards on one page needs sorting into groups or it reads as a pile, so each group carries site/'s keyed-label eyebrow on a hairline. The cards move to web/components/settings/ rather than into the page, which would have made it ~600 lines. That is also where the Field and inputClass pair now lives: the two pages each had a byte-identical copy, and folding them together is exactly when three copies would have started to drift. Members and SSO adopt SectionCard, which the rest of the page already used. A card that kept its own header treatment would read as a different kind of thing rather than another setting, which is the problem being fixed. next.config.ts keeps a permanent redirect from the old path, so bookmarks and any support reply linking it still land somewhere useful.
215 lines
11 KiB
TypeScript
215 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { clsx } from "clsx";
|
|
import { useAuth } from "@/components/AuthProvider";
|
|
import { auth } from "@/lib/api";
|
|
import { Logo } from "./Logo";
|
|
|
|
interface NavItem {
|
|
href: string;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
/** Restricted to owner/admin the roles the backing API requires. */
|
|
adminOnly?: boolean;
|
|
}
|
|
|
|
function ServerIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function KeyIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function SecretIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function WorkflowIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9 3.75H6.912a2.25 2.25 0 00-2.15 1.588L2.35 13.177a2.25 2.25 0 00-.1.661V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15M2.25 13.5h3.86a2.25 2.25 0 012.012 1.244l.256.512a2.25 2.25 0 002.012 1.244h3.22a2.25 2.25 0 002.012-1.244l.256-.512a2.25 2.25 0 012.012-1.244h3.86M12 3v8.25m0 0l-3-3m3 3l3-3"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function AuditIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function LicenceIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296A3.745 3.745 0 0 1 21 12Z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function SettingsIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
|
/>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function MonitorIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3 12h4l2 6 4-14 2 8h6" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function StepsIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M6 6.75A.75.75 0 016.75 6h10.5a.75.75 0 010 1.5H6.75A.75.75 0 016 6.75zm0 5.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H6.75A.75.75 0 016 12zm0 5.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H6.75A.75.75 0 016 17.25zM3 6.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM3 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 5.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{ href: "/servers", label: "Servers", icon: <ServerIcon /> },
|
|
{ href: "/monitors", label: "Monitors", icon: <MonitorIcon /> },
|
|
{ href: "/keys", label: "SSH Keys", icon: <KeyIcon /> },
|
|
{ href: "/secrets", label: "Secrets", icon: <SecretIcon /> },
|
|
{ href: "/workflows", label: "Workflows", icon: <WorkflowIcon /> },
|
|
{ href: "/steps", label: "Steps", icon: <StepsIcon /> },
|
|
{ href: "/audit", label: "Audit Log", icon: <AuditIcon /> },
|
|
{ href: "/settings/license", label: "Licence", icon: <LicenceIcon />, adminOnly: true },
|
|
{ href: "/settings", label: "Settings", icon: <SettingsIcon />, adminOnly: true },
|
|
];
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
const { user, instance, isAdmin } = useAuth();
|
|
|
|
const visibleItems = navItems.filter((item) => !item.adminOnly || isAdmin);
|
|
|
|
const activeHref = visibleItems.reduce<string | null>((best, item) => {
|
|
const matches = pathname === item.href || pathname.startsWith(item.href + "/");
|
|
if (!matches) return best;
|
|
return best === null || item.href.length > best.length ? item.href : best;
|
|
}, null);
|
|
|
|
async function handleLogout() {
|
|
try {
|
|
await auth.logout();
|
|
} catch {}
|
|
window.location.href = "/login";
|
|
}
|
|
|
|
return (
|
|
<aside className="flex h-screen w-60 flex-col border-r border-border bg-surface">
|
|
<div className="flex h-16 items-center gap-3 border-b border-border px-5">
|
|
<Logo className="h-8 w-8 text-logo" />
|
|
<div className="min-w-0">
|
|
<span className="block text-base font-extrabold leading-tight tracking-[-0.035em] text-text-primary">Vantage</span>
|
|
{instance && (
|
|
<span className="block truncate font-mono text-[0.68rem] uppercase tracking-[0.1em] text-text-secondary">{instance.name}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="flex-1 overflow-y-auto px-3 py-4">
|
|
<ul className="space-y-1">
|
|
{visibleItems.map((item) => {
|
|
const isActive = activeHref === item.href;
|
|
return (
|
|
<li key={item.href}>
|
|
<Link
|
|
href={item.href}
|
|
// The active marker is an accent bar, the same device
|
|
// site/ uses to mark the chosen plan. A filled pill
|
|
// reads as a button you can press again.
|
|
className={clsx(
|
|
"relative flex items-center gap-3 rounded px-3 py-2.5 text-sm transition-colors",
|
|
isActive
|
|
? "bg-surface-2 font-semibold text-text-primary before:absolute before:inset-y-1 before:left-0 before:w-[2px] before:rounded-full before:bg-accent before:content-['']"
|
|
: "font-medium text-text-secondary hover:bg-surface-2 hover:text-text-primary",
|
|
)}
|
|
>
|
|
{item.icon}
|
|
{item.label}
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
|
|
<div className="border-t border-border px-4 py-3">
|
|
{user && (
|
|
<div className="mb-3">
|
|
<p className="truncate text-sm font-medium text-text-primary">{user.name || user.email}</p>
|
|
<p className="truncate text-xs text-text-secondary">
|
|
{user.email}
|
|
{user.role && <span className="ml-1 text-text-tertiary">· {user.role}</span>}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center justify-between">
|
|
<p className="font-mono text-[0.68rem] uppercase tracking-[0.1em] text-text-secondary">Vantage v1.0</p>
|
|
{user && (
|
|
<button type="button" onClick={handleLogout} className="text-xs text-text-secondary transition-colors hover:text-danger">
|
|
Logout
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|