import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@agent-native/toolkit/ui/alert-dialog"; import { Badge } from "@agent-native/toolkit/ui/badge"; import { Button } from "@agent-native/toolkit/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger, } from "@agent-native/toolkit/ui/dropdown-menu"; import { Input } from "@agent-native/toolkit/ui/input"; import { Label } from "@agent-native/toolkit/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@agent-native/toolkit/ui/select"; import { Textarea } from "@agent-native/toolkit/ui/textarea"; import { IconCheck, IconDots, IconSettings, IconLoader2, IconUserCheck, } from "@tabler/icons-react"; import { useMemo, useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../components/ui/dialog.js"; import { useT } from "../i18n.js"; import { normalizeFeatureFlagPercentage, normalizeFeatureFlagRules, } from "./helpers.js"; import type { FeatureFlagMetadata, FeatureFlagRules, SetFeatureFlagInput, } from "./types.js"; function formatActor(actor: FeatureFlagRules["updatedBy"]): string | null { if (!actor) return null; if (typeof actor === "string") return actor; return actor.name ?? actor.email ?? null; } function formatWhen(value: number | null | undefined): string | null { if (!value) return null; const date = new Date(value); if (Number.isNaN(date.valueOf())) return null; return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", }).format(date); } function rolloutLabel( rules: FeatureFlagRules, t: ReturnType, ): string { if (rules.mode === "off") return t("featureFlags.off"); if (rules.mode === "on") return t("featureFlags.everyone"); const parts = [ rules.emails.length ? t("featureFlags.emailCount", { count: rules.emails.length }) : null, rules.orgIds.length ? t("featureFlags.organizationCount", { count: rules.orgIds.length }) : null, rules.percentage ? t("featureFlags.percentageRollout", { count: rules.percentage }) : null, ].filter(Boolean); return parts.join(" · ") || t("featureFlags.inherited"); } function audienceLabel( rules: FeatureFlagRules, t: ReturnType, ): string { if (rules.mode === "off") return t("featureFlags.off"); if (rules.mode === "on") return t("featureFlags.everyoneAudience"); return rolloutLabel(rules, t); } function modeLabel(rules: FeatureFlagRules, t: ReturnType) { if (rules.mode === "off") return t("featureFlags.off"); if (rules.mode === "on") return t("featureFlags.everyone"); return t("featureFlags.targeted"); } function listText(values: string[]): string { return values.join("\n"); } function parseList(value: string): string[] { return [ ...new Set( value .split(/[\n,]/) .map((entry) => entry.trim()) .filter(Boolean), ), ]; } function TargetingDialog({ flag, open, onOpenChange, onMutate, isPending, }: { flag: FeatureFlagMetadata; open: boolean; onOpenChange: (open: boolean) => void; onMutate: (input: SetFeatureFlagInput) => void; isPending?: boolean; }) { const t = useT(); const modeId = `feature-flag-${flag.key}-mode`; const emailsId = `feature-flag-${flag.key}-emails`; const orgIdsId = `feature-flag-${flag.key}-org-ids`; const percentageId = `feature-flag-${flag.key}-percentage`; const [mode, setMode] = useState(flag.rules.mode); const [emails, setEmails] = useState(() => listText(flag.rules.emails)); const [orgIds, setOrgIds] = useState(() => listText(flag.rules.orgIds)); const [percentage, setPercentage] = useState(String(flag.rules.percentage)); const save = () => { const nextPercentage = normalizeFeatureFlagPercentage(percentage); onMutate({ key: flag.key, operation: "replace-rules", rules: { version: 1, mode, emails: parseList(emails), orgIds: parseList(orgIds), percentage: nextPercentage, }, }); }; return ( {t("featureFlags.targetingTitle", { name: flag.displayName ?? flag.key, })} {t("featureFlags.targetingDescription")}
{mode === "rules" ? (