import { useFetcher } from "@remix-run/react"; import { useEffect, useState } from "react"; import stableStringify from "json-stable-stringify"; import { json } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { LockClosedIcon } from "@heroicons/react/20/solid"; import { prisma } from "~/db.server"; import { env } from "~/env.server"; import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; import { FEATURE_FLAG, GLOBAL_LOCKED_FLAGS, type FlagControlType, getAllFlagControlTypes, validatePartialFeatureFlags, } from "~/v3/featureFlags"; import { flags as getGlobalFlags } from "~/v3/featureFlags.server"; import { featuresForRequest } from "~/features.server"; import { Button } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { CheckboxWithLabel } from "~/components/primitives/Checkbox"; import { Dialog, DialogContent, DialogHeader, DialogDescription, DialogFooter, } from "~/components/primitives/Dialog"; import { cn } from "~/utils/cn"; import { UNSET_VALUE, BooleanControl, EnumControl, NumberControl, StringControl, WorkerGroupControl, type WorkerGroup, } from "~/components/admin/FlagControls"; export const loader = dashboardLoader( { authorization: { requireSuper: true } }, async ({ request }) => { const [globalFlags, workerGroups] = await Promise.all([ getGlobalFlags(), prisma.workerInstanceGroup.findMany({ select: { id: true, name: true }, orderBy: { name: "asc" }, }), ]); const controlTypes = getAllFlagControlTypes(); // Resolve env-based defaults for locked flags const resolvedDefaults: Record = { [FEATURE_FLAG.taskEventRepository]: env.EVENT_REPOSITORY_DEFAULT_STORE, }; // Look up worker group name if the flag is set const workerGroupId = (globalFlags as Record)?.[ FEATURE_FLAG.defaultWorkerInstanceGroupId ]; const workerGroupName = typeof workerGroupId === "string" ? workerGroups.find((wg) => wg.id === workerGroupId)?.name : undefined; const { isManagedCloud } = featuresForRequest(request); return typedjson({ globalFlags, controlTypes, resolvedDefaults, workerGroupName, workerGroups, isManagedCloud, }); } ); export const action = dashboardAction( { authorization: { requireSuper: true } }, async ({ request }) => { let body: unknown; try { body = await request.json(); } catch { return json({ error: "Invalid JSON body" }, { status: 400 }); } const payloadSchema = z.object({ flags: z.record(z.unknown()) }); const parsed = payloadSchema.safeParse(body); if (!parsed.success) { return json({ error: "Invalid payload" }, { status: 400 }); } const { isManagedCloud } = featuresForRequest(request); // On managed cloud, reject if payload includes locked flags if (isManagedCloud) { const lockedInPayload = Object.keys(parsed.data.flags).filter((key) => GLOBAL_LOCKED_FLAGS.includes(key) ); if (lockedInPayload.length > 0) { return json( { error: `Cannot modify locked flags: ${lockedInPayload.join(", ")}` }, { status: 400 } ); } } const validationResult = validatePartialFeatureFlags(parsed.data.flags); if (!validationResult.success) { return json( { error: "Invalid feature flags", details: validationResult.error.issues }, { status: 400 } ); } const validatedFlags = validationResult.data as Record; const controlTypes = getAllFlagControlTypes(); const catalogKeys = Object.keys(controlTypes); const keysToDelete: string[] = []; const upsertOps: ReturnType[] = []; for (const key of catalogKeys) { if (key in validatedFlags) { upsertOps.push( prisma.featureFlag.upsert({ where: { key }, create: { key, value: validatedFlags[key] as any }, update: { value: validatedFlags[key] as any }, }) ); } else { // On cloud, never delete locked flags (they're not in the payload // because the UI doesn't include them). Locally, delete everything // the user didn't include - full control. const isProtected = isManagedCloud && GLOBAL_LOCKED_FLAGS.includes(key); if (!isProtected) { keysToDelete.push(key); } } } await prisma.$transaction([ ...upsertOps, ...(keysToDelete.length > 0 ? [prisma.featureFlag.deleteMany({ where: { key: { in: keysToDelete } } })] : []), ]); return json({ success: true }); } ); export default function AdminFeatureFlagsRoute() { const { globalFlags, controlTypes, resolvedDefaults, workerGroupName, workerGroups, isManagedCloud, } = useTypedLoaderData(); const saveFetcher = useFetcher<{ success?: boolean; error?: string }>(); const [values, setValues] = useState>({}); const [initialValues, setInitialValues] = useState>({}); const [saveError, setSaveError] = useState(null); const [confirmOpen, setConfirmOpen] = useState(false); const [unlocked, setUnlocked] = useState(false); const isLocked = (key: string) => !unlocked && GLOBAL_LOCKED_FLAGS.includes(key); useEffect(() => { const loaded = (globalFlags ?? {}) as Record; // Only track editable flags in state const editable: Record = {}; for (const [key, value] of Object.entries(loaded)) { if (!isLocked(key)) { editable[key] = value; } } setValues({ ...editable }); setInitialValues({ ...editable }); }, [globalFlags, unlocked]); useEffect(() => { if (saveFetcher.data?.success) { setSaveError(null); setConfirmOpen(false); } else if (saveFetcher.data?.error) { setSaveError(saveFetcher.data.error); } }, [saveFetcher.data]); const isDirty = stableStringify(values) !== stableStringify(initialValues); const isSaving = saveFetcher.state === "submitting"; const setFlagValue = (key: string, value: unknown) => { setValues((prev) => ({ ...prev, [key]: value })); }; const unsetFlag = (key: string) => { setValues((prev) => { const next = { ...prev }; delete next[key]; return next; }); }; const handleSave = () => { saveFetcher.submit(JSON.stringify({ flags: values }), { method: "POST", encType: "application/json", }); }; const typedControlTypes = controlTypes as Record; const typedResolvedDefaults = resolvedDefaults as Record; const allFlags = (globalFlags ?? {}) as Record; const sortedFlagKeys = Object.keys(typedControlTypes).sort(); const workerGroupMap = new Map((workerGroups as WorkerGroup[]).map((wg) => [wg.id, wg.name])); const resolveWorkerGroupDisplay = (id: string) => { const name = workerGroupMap.get(id); return name ? `${name} (${id.slice(0, 8)}...)` : id; }; return (
These are global feature flags that affect every organization on this instance. Changing values here is a dangerous operation and should rarely be done - prefer org-level overrides where possible. Org-level overrides take precedence; when a flag isn't set, each consumer uses its own default.
{sortedFlagKeys.map((key) => { const control = typedControlTypes[key]; const locked = isLocked(key); if (locked) { return ( ); } const isSet = key in values; const isWorkerGroup = key === FEATURE_FLAG.defaultWorkerInstanceGroupId; return (
{isWorkerGroup ? "defaultWorkerInstanceGroup" : key}
{isSet ? isWorkerGroup ? resolveWorkerGroupDisplay(values[key] as string) : `value: ${String(values[key])}` : typedResolvedDefaults[key] ? `${typedResolvedDefaults[key]} (from env)` : "not set"}
{isWorkerGroup ? ( { if (val === UNSET_VALUE) { unsetFlag(key); } else { setFlagValue(key, val); } }} dimmed={!isSet} /> ) : ( <> {control.type === "boolean" && ( setFlagValue(key, val)} dimmed={!isSet} /> )} {control.type === "enum" && ( { if (val === UNSET_VALUE) { unsetFlag(key); } else { setFlagValue(key, val); } }} dimmed={!isSet} /> )} {control.type === "number" && ( { if (val === undefined) { unsetFlag(key); } else { setFlagValue(key, val); } }} dimmed={!isSet} /> )} {control.type === "string" && ( { if (val === "") { unsetFlag(key); } else { setFlagValue(key, val); } }} dimmed={!isSet} /> )} )}
); })}
{saveError && {saveError}}
{isDirty && ( )}
); } // --- Locked Flag Row --- function LockedFlagRow({ flagKey, value, resolvedDefault, workerGroupName, }: { flagKey: string; value: unknown; resolvedDefault: string | undefined; workerGroupName: string | undefined; }) { const isSet = value !== undefined; const isWorkerGroup = flagKey === FEATURE_FLAG.defaultWorkerInstanceGroupId; let displayValue: string; if (isSet) { if (isWorkerGroup && workerGroupName) { displayValue = `${workerGroupName} (${String(value).slice(0, 8)}...)`; } else { displayValue = String(value); } } else if (resolvedDefault) { displayValue = `${resolvedDefault} (from env)`; } else { displayValue = "not set (required)"; } return (
{isWorkerGroup ? "defaultWorkerInstanceGroup" : flagKey}
{displayValue}
); } // --- Confirmation Dialog with Diff --- function ConfirmDialog({ open, onOpenChange, initialValues, newValues, controlTypes, lockedKeys, onConfirm, isSaving, saveError, }: { open: boolean; onOpenChange: (open: boolean) => void; initialValues: Record; newValues: Record; controlTypes: Record; lockedKeys: readonly string[]; onConfirm: () => void; isSaving: boolean; saveError: string | null; }) { const editableKeys = Object.keys(controlTypes) .filter((key) => !lockedKeys.includes(key)) .sort(); type Change = | { key: string; type: "added"; newVal: string } | { key: string; type: "removed"; oldVal: string } | { key: string; type: "changed"; oldVal: string; newVal: string }; const changes = editableKeys.flatMap((key) => { const wasSet = key in initialValues; const isSet = key in newValues; const oldVal = initialValues[key]; const newVal = newValues[key]; if (!wasSet && !isSet) return []; if (wasSet && isSet && stableStringify(oldVal) === stableStringify(newVal)) return []; if (!wasSet && isSet) { return [{ key, type: "added" as const, newVal: String(newVal) }]; } if (wasSet && !isSet) { return [{ key, type: "removed" as const, oldVal: String(oldVal) }]; } return [ { key, type: "changed" as const, oldVal: String(oldVal), newVal: String(newVal), }, ]; }); return ( Confirm feature flag changes These changes affect all organizations globally. Please review carefully.
{changes.length === 0 ? (

No changes to apply.

) : ( changes.map((change) => (
{change.key}
{change.type === "added" && (
+ {change.newVal}
)} {change.type === "removed" && (
- {change.oldVal} (unset)
)} {change.type === "changed" && ( <>
- {change.oldVal}
+ {change.newVal}
)}
)) )}
{saveError && {saveError}}
); }