TypeScript 97.5%
SQL 1.4%
Python 0.8%
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