Files
warmbly/admin/src/components/layout/Sidebar.tsx
T
Matt 93bb56a458 feat(limits): admin queue + customer request form + ToS clause
Three surfaces close the loop on the limit-increase workflow:

  - admin/dashboard/LimitRequestsPage.tsx queues every pending request
    with full context (org → users → field → current vs requested →
    +delta) and one-click approve/reject. Both actions open a review
    dialog; approve notes are optional, reject notes are required and
    surface to the customer.
  - web/settings/limits/page.tsx is the customer-facing form. Resource
    selector, requested value, reason textarea, plus a list of every
    past request with its status (pending/approved/rejected/cancelled)
    and the reviewer's notes when present. Pending rows expose a
    cancel link. Footer links to the ToS limits clause.
  - site/terms.astro grows a new section 07 ("Usage limits and
    increase requests"). Explicit: "unlimited" means no plan-tier cap
    but a product-wide hard ceiling still applies, increases are at
    Warmbly's sole discretion, and previously granted increases can be
    revoked when reputation signals deteriorate. Bumps every existing
    section heading and id from 07 onward.

Admin sidebar grows a "Limit requests" entry under Accounts (Gauge
icon). Web settings layout grows a "Limits" section under owner-only
sections.
2026-05-28 12:16:02 +02:00

182 lines
6.5 KiB
TypeScript

// Left rail navigation. Mirrors the dashboard's general structure
// (icon + label rows, collapsible sections) but uses the admin-tinted
// sidebar background and amber accent for active items so it never
// gets confused with the dashboard's sidebar.
import { NavLink } from "react-router-dom";
import {
Activity,
BarChart3,
Briefcase,
Building2,
Cloud,
Cog,
CreditCard,
Database,
FileText,
Flame,
Gauge,
HardDrive,
Inbox,
LayoutDashboard,
Lock,
Mailbox,
Megaphone,
Network,
Radio,
Rocket,
Send,
Server,
ServerCog,
Users,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { AdminBadge } from "./AdminBadge";
interface NavItem {
to: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
end?: boolean;
}
interface NavGroup {
label: string;
items: NavItem[];
}
const GROUPS: NavGroup[] = [
{
label: "Operations",
items: [
{ to: "/", label: "Overview", icon: LayoutDashboard, end: true },
{ to: "/workers", label: "Workers", icon: Server, end: true },
{
to: "/workers/provisioning-jobs",
label: "Provisioning Jobs",
icon: Rocket,
},
{ to: "/egresses", label: "Egresses", icon: Network },
{ to: "/mailboxes", label: "Mailboxes", icon: Mailbox },
{ to: "/warmup", label: "Warmup", icon: Flame },
{ to: "/campaigns", label: "Campaigns", icon: Megaphone },
],
},
{
label: "Accounts",
items: [
{ to: "/users", label: "Users", icon: Users },
{ to: "/organizations", label: "Organizations", icon: Building2 },
{ to: "/limit-requests", label: "Limit requests", icon: Gauge },
{ to: "/plans", label: "Plans & Billing", icon: CreditCard },
{ to: "/enterprise", label: "Enterprise", icon: Briefcase },
],
},
{
label: "Insight",
items: [
{ to: "/analytics", label: "Analytics", icon: BarChart3 },
{ to: "/audit", label: "Audit Log", icon: FileText },
],
},
{
label: "Settings",
items: [
{ to: "/settings/encryption", label: "Encryption (KMS)", icon: Lock },
{ to: "/settings/storage", label: "Storage", icon: HardDrive },
{ to: "/settings/messaging", label: "Messaging (EventBus)", icon: Radio },
{ to: "/settings/cache", label: "Cache", icon: Database },
{ to: "/settings/transports", label: "Transports (SMTP/OAuth)", icon: Send },
{ to: "/settings/cloud-providers", label: "Cloud Providers", icon: Cloud },
{
to: "/settings/provisioning-templates",
label: "Provisioning Templates",
icon: ServerCog,
},
],
},
];
export function Sidebar() {
return (
<aside
className={cn(
"hidden md:flex md:w-64 lg:w-72 shrink-0 flex-col",
"border-r border-sidebar-border bg-sidebar admin-sidebar-pattern",
)}
>
<div className="flex items-center justify-between gap-2 px-4 pt-4 pb-3 border-b border-sidebar-border">
<div className="flex items-center gap-2 min-w-0">
<div className="size-8 rounded-md bg-zinc-900 text-white flex items-center justify-center font-bold text-sm shrink-0">
W
</div>
<div className="min-w-0">
<div className="text-sm font-semibold text-sidebar-foreground leading-none truncate">
Warmbly
</div>
<div className="text-[11px] text-muted-foreground mt-0.5">
Control plane
</div>
</div>
</div>
<AdminBadge compact />
</div>
<nav className="flex-1 overflow-y-auto px-2 py-3 space-y-5">
{GROUPS.map((group) => (
<div key={group.label}>
<div className="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{group.label}
</div>
<ul className="space-y-0.5">
{group.items.map((item) => (
<li key={item.to}>
<SidebarLink {...item} />
</li>
))}
</ul>
</div>
))}
</nav>
<div className="px-4 py-3 border-t border-sidebar-border flex items-center gap-2 text-[11px] text-muted-foreground">
<Activity className="size-3" />
<span>Admin surface · do not share</span>
</div>
</aside>
);
}
function SidebarLink({ to, label, icon: Icon, end }: NavItem) {
return (
<NavLink
to={to}
end={end}
className={({ isActive }) =>
cn(
"group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors",
"text-sidebar-foreground/80 hover:text-sidebar-foreground hover:bg-sidebar-accent",
isActive &&
// Active state uses the admin accent on the left edge and a
// soft amber wash. Distinct from the dashboard's blue active
// state without losing the same shape.
"bg-[var(--admin-accent-soft)] text-[var(--admin-accent-strong)] font-medium relative " +
"before:absolute before:left-0 before:top-1.5 before:bottom-1.5 before:w-0.5 before:rounded-r before:bg-[var(--admin-accent)]",
)
}
>
<Icon className="size-4 shrink-0 opacity-80 group-hover:opacity-100" />
<span className="truncate">{label}</span>
</NavLink>
);
}
// Standalone icon export so other surfaces (mobile drawer, breadcrumbs)
// can reuse the same icon-per-route mapping if needed later.
export const NAV_GROUPS = GROUPS;
export type { NavItem };
// Re-export the inbox icon so the "Open dashboard" link in UserMenu can
// use a consistent visual without redeclaring the import elsewhere.
export const InboxIcon = Inbox;
export const CogIcon = Cog;