SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
5.4 KB · 128 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { setOrganizationProviderVisibility, setOrganizationSuspended, updateOrganizationLimits, type AdminActionResult } from "@/actions/admin";5import { Button } from "@/components/ui/button";6import { Input, Textarea } from "@/components/ui/input";7import { Field, Hint, Label } from "@/components/ui/label";8import { Switch } from "@/components/ui/switch";9import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";10import { ResultMessage } from "./action-button";1112export function OrgProviderVisibilitySwitch({ orgId, enabled }: { orgId: string; enabled: boolean }) {13  const router = useRouter();14  const [pending, start] = React.useTransition();15  const [result, setResult] = React.useState<AdminActionResult | null>(null);16  return (17    <div className="flex items-start gap-3">18      <Switch19        id={`pv-${orgId}`}20        checked={enabled}21        disabled={pending}22        aria-label="Provider visibility"23        onCheckedChange={(v) =>24          start(async () => {25            const r = await setOrganizationProviderVisibility(orgId, v);26            setResult(r);27            if (r.ok) router.refresh();28          })29        }30      />31      <div className="grid gap-0.5">32        <Label htmlFor={`pv-${orgId}`}>Provider visibility (debug)</Label>33        <Hint>When on, `debug: true` responses include real upstream provider names for this customer. Off for everyone by default.</Hint>34        <ResultMessage result={result} />35      </div>36    </div>37  );38}3940export function OrgSuspendSwitch({ orgId, suspended }: { orgId: string; suspended: boolean }) {41  const router = useRouter();42  const [pending, start] = React.useTransition();43  const [open, setOpen] = React.useState(false);44  const [reason, setReason] = React.useState("");45  const [result, setResult] = React.useState<AdminActionResult | null>(null);46  const apply = (v: boolean) =>47    start(async () => {48      const r = await setOrganizationSuspended(orgId, v, reason);49      setResult(r);50      if (r.ok) {51        setOpen(false);52        setReason("");53        router.refresh();54      }55    });56  return (57    <div className="flex items-start gap-3">58      <Switch id={`sus-${orgId}`} checked={suspended} disabled={pending} aria-label="Suspended" onCheckedChange={(v) => (v ? setOpen(true) : apply(false))} />59      <div className="grid gap-0.5">60        <Label htmlFor={`sus-${orgId}`}>Suspended</Label>61        <Hint>Suspended organizations get `FORBIDDEN` on every API call and see a banner in the dashboard.</Hint>62        <ResultMessage result={result} />63      </div>64      <Dialog open={open} onOpenChange={setOpen}>65        <DialogContent size="sm">66          <DialogHeader>67            <DialogTitle>Suspend this organization?</DialogTitle>68            <DialogDescription>All API keys stop working immediately. Members can still sign in to read logs and contact support.</DialogDescription>69          </DialogHeader>70          <Field>71            <Label htmlFor="sus-reason">Reason (internal)</Label>72            <Textarea id="sus-reason" value={reason} onChange={(e) => setReason(e.target.value)} placeholder="Optional — stored in the audit log" />73          </Field>74          <DialogFooter>75            <Button variant="outline" onClick={() => setOpen(false)}>76              Cancel77            </Button>78            <Button variant="danger" loading={pending} onClick={() => apply(true)}>79              Suspend80            </Button>81          </DialogFooter>82        </DialogContent>83      </Dialog>84    </div>85  );86}8788export function OrgLimitsForm({ orgId, softLimitUsd, hardLimitUsd }: { orgId: string; softLimitUsd: number | null; hardLimitUsd: number | null }) {89  const router = useRouter();90  const [pending, start] = React.useTransition();91  const [soft, setSoft] = React.useState(softLimitUsd?.toString() ?? "");92  const [hard, setHard] = React.useState(hardLimitUsd?.toString() ?? "");93  const [result, setResult] = React.useState<AdminActionResult | null>(null);94  const toNum = (s: string) => (s.trim() === "" ? null : Number(s));95  return (96    <form97      className="grid gap-3"98      onSubmit={(e) => {99        e.preventDefault();100        start(async () => {101          const r = await updateOrganizationLimits(orgId, { softLimitUsd: toNum(soft), hardLimitUsd: toNum(hard) });102          setResult(r);103          if (r.ok) router.refresh();104        });105      }}106    >107      <div className="grid gap-3 sm:grid-cols-2">108        <Field>109          <Label htmlFor="soft">Soft limit (USD / month)</Label>110          <Input id="soft" type="number" min={0} step="0.01" inputMode="decimal" value={soft} onChange={(e) => setSoft(e.target.value)} placeholder="none" className="font-mono" />111          <Hint>Sends a warning email; requests continue.</Hint>112        </Field>113        <Field>114          <Label htmlFor="hard">Hard limit (USD / month)</Label>115          <Input id="hard" type="number" min={0} step="0.01" inputMode="decimal" value={hard} onChange={(e) => setHard(e.target.value)} placeholder="none" className="font-mono" />116          <Hint>Requests fail with `USAGE_LIMIT_REACHED` once exceeded.</Hint>117        </Field>118      </div>119      <div className="flex items-center gap-3">120        <Button type="submit" size="sm" loading={pending}>121          Save limits122        </Button>123        <ResultMessage result={result} />124      </div>125    </form>126  );127}128