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.1 KB · 122 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { Plus, Trash2 } from "lucide-react";5import { deleteFeatureFlag, setFeatureFlag, upsertFeatureFlag, type AdminActionResult } from "@/actions/admin";6import { Button } from "@/components/ui/button";7import { Input } from "@/components/ui/input";8import { Field, Hint, Label } from "@/components/ui/label";9import { Switch } from "@/components/ui/switch";10import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";11import { ActionButton, ResultMessage } from "./action-button";1213export function FlagSwitch({ flagKey, enabled }: { flagKey: string; enabled: boolean }) {14  const router = useRouter();15  const [pending, start] = React.useTransition();16  const [result, setResult] = React.useState<AdminActionResult | null>(null);17  return (18    <div className="flex items-center gap-2">19      <Switch20        checked={enabled}21        disabled={pending}22        aria-label={`Toggle ${flagKey}`}23        onCheckedChange={(v) =>24          start(async () => {25            const r = await setFeatureFlag(flagKey, v);26            setResult(r);27            if (r.ok) router.refresh();28          })29        }30      />31      {result && !result.ok ? <ResultMessage result={result} /> : null}32    </div>33  );34}3536export function DeleteFlagButton({ flagKey }: { flagKey: string }) {37  return (38    <ActionButton variant="ghost" size="icon-sm" aria-label={`Delete ${flagKey}`} action={() => deleteFeatureFlag(flagKey)} confirm={{ title: `Delete flag ${flagKey}?`, description: "Code that reads this flag will treat it as disabled.", confirmLabel: "Delete" }}>39      <Trash2 className="size-3.5 text-fg-subtle" />40    </ActionButton>41  );42}4344export function NewFlagDialog() {45  const router = useRouter();46  const [open, setOpen] = React.useState(false);47  const [pending, start] = React.useTransition();48  const [key, setKey] = React.useState("");49  const [description, setDescription] = React.useState("");50  const [plans, setPlans] = React.useState("");51  const [orgIds, setOrgIds] = React.useState("");52  const [result, setResult] = React.useState<AdminActionResult | null>(null);53  return (54    <>55      <Button variant="primary" size="sm" onClick={() => setOpen(true)}>56        <Plus className="size-3.5" /> New flag57      </Button>58      <Dialog open={open} onOpenChange={setOpen}>59        <DialogContent size="md">60          <DialogHeader>61            <DialogTitle>Create a feature flag</DialogTitle>62            <DialogDescription>Flags start disabled. Optional targeting by plan or organization id narrows who sees the feature when enabled.</DialogDescription>63          </DialogHeader>64          <form65            className="grid gap-3"66            onSubmit={(e) => {67              e.preventDefault();68              start(async () => {69                const r = await upsertFeatureFlag({70                  key: key.trim(),71                  description,72                  enabled: false,73                  plans: plans.split(",").map((s) => s.trim()).filter(Boolean) as never,74                  organizationIds: orgIds.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean),75                });76                setResult(r);77                if (r.ok) {78                  setOpen(false);79                  setKey("");80                  setDescription("");81                  setPlans("");82                  setOrgIds("");83                  router.refresh();84                }85              });86            }}87          >88            <Field>89              <Label htmlFor="flag-key">Key</Label>90              <Input id="flag-key" value={key} onChange={(e) => setKey(e.target.value)} placeholder="e.g. browser.beta" className="font-mono" required />91              <Hint>Lowercase letters, digits, `.` `_` `-`.</Hint>92            </Field>93            <Field>94              <Label htmlFor="flag-desc">Description</Label>95              <Input id="flag-desc" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What this flag gates" />96            </Field>97            <div className="grid gap-3 sm:grid-cols-2">98              <Field>99                <Label htmlFor="flag-plans">Plans (comma-separated)</Label>100                <Input id="flag-plans" value={plans} onChange={(e) => setPlans(e.target.value)} placeholder="growth, business" className="font-mono" />101              </Field>102              <Field>103                <Label htmlFor="flag-orgs">Organization ids</Label>104                <Input id="flag-orgs" value={orgIds} onChange={(e) => setOrgIds(e.target.value)} placeholder="org_a, org_b" className="font-mono" />105              </Field>106            </div>107            <ResultMessage result={result && !result.ok ? result : null} />108            <DialogFooter>109              <Button type="button" variant="outline" onClick={() => setOpen(false)}>110                Cancel111              </Button>112              <Button type="submit" variant="primary" loading={pending}>113                Create flag114              </Button>115            </DialogFooter>116          </form>117        </DialogContent>118      </Dialog>119    </>120  );121}122