chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import stableStringify from "json-stable-stringify";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "~/components/primitives/Dialog";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { LockClosedIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { FEATURE_FLAG, ORG_LOCKED_FLAGS, type FlagControlType } from "~/v3/featureFlags";
|
||||
import {
|
||||
UNSET_VALUE,
|
||||
BooleanControl,
|
||||
EnumControl,
|
||||
NumberControl,
|
||||
StringControl,
|
||||
WorkerGroupControl,
|
||||
type WorkerGroup,
|
||||
} from "./FlagControls";
|
||||
|
||||
type LoaderData = {
|
||||
org: { id: string; title: string; slug: string };
|
||||
orgFlags: Record<string, unknown>;
|
||||
globalFlags: Record<string, unknown>;
|
||||
controlTypes: Record<string, FlagControlType>;
|
||||
workerGroupName?: string;
|
||||
workerGroups?: WorkerGroup[];
|
||||
isManagedCloud?: boolean;
|
||||
};
|
||||
|
||||
type ActionData = {
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type FeatureFlagsDialogProps = {
|
||||
orgId: string | null;
|
||||
orgTitle: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export function FeatureFlagsDialog({
|
||||
orgId,
|
||||
orgTitle,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: FeatureFlagsDialogProps) {
|
||||
const loadFetcher = useFetcher<LoaderData>();
|
||||
const saveFetcher = useFetcher<ActionData>();
|
||||
|
||||
const [overrides, setOverrides] = useState<Record<string, unknown>>({});
|
||||
const [initialOverrides, setInitialOverrides] = useState<Record<string, unknown>>({});
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [unlocked, setUnlocked] = useState(false);
|
||||
|
||||
const isLocked = (key: string) => !unlocked && ORG_LOCKED_FLAGS.includes(key);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && orgId) {
|
||||
setSaveError(null);
|
||||
setOverrides({});
|
||||
setInitialOverrides({});
|
||||
loadFetcher.load(`/admin/api/v2/orgs/${orgId}/feature-flags`);
|
||||
}
|
||||
}, [open, orgId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadFetcher.data) {
|
||||
const loaded = loadFetcher.data.orgFlags ?? {};
|
||||
setOverrides({ ...loaded });
|
||||
setInitialOverrides({ ...loaded });
|
||||
}
|
||||
}, [loadFetcher.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (saveFetcher.data?.success) {
|
||||
onOpenChange(false);
|
||||
} else if (saveFetcher.data?.error) {
|
||||
setSaveError(saveFetcher.data.error);
|
||||
}
|
||||
}, [saveFetcher.data]);
|
||||
|
||||
const isDirty = stableStringify(overrides) !== stableStringify(initialOverrides);
|
||||
|
||||
const setFlagValue = (key: string, value: unknown) => {
|
||||
setOverrides((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const unsetFlag = (key: string) => {
|
||||
setOverrides((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[key];
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!orgId) return;
|
||||
const body = Object.keys(overrides).length === 0 ? null : overrides;
|
||||
saveFetcher.submit(JSON.stringify(body), {
|
||||
method: "POST",
|
||||
action: `/admin/api/v2/orgs/${orgId}/feature-flags`,
|
||||
encType: "application/json",
|
||||
});
|
||||
};
|
||||
|
||||
const data = loadFetcher.data;
|
||||
const isLoading = loadFetcher.state === "loading";
|
||||
const isSaving = saveFetcher.state === "submitting";
|
||||
|
||||
const jsonPreview =
|
||||
Object.keys(overrides).length === 0 ? "null" : JSON.stringify(overrides, null, 2);
|
||||
|
||||
const sortedFlagKeys = data ? Object.keys(data.controlTypes).sort() : [];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>Feature flags - {orgTitle}</DialogHeader>
|
||||
<DialogDescription>
|
||||
Org-level overrides. Unset flags inherit from global defaults.
|
||||
</DialogDescription>
|
||||
|
||||
{data && (
|
||||
<div className={data.isManagedCloud ? "cursor-not-allowed" : undefined}>
|
||||
<CheckboxWithLabel
|
||||
variant="simple/small"
|
||||
label={
|
||||
data.isManagedCloud
|
||||
? "Unlock read-only flags (only in unmanaged cloud)"
|
||||
: "Unlock read-only flags"
|
||||
}
|
||||
defaultChecked={unlocked}
|
||||
onChange={setUnlocked}
|
||||
disabled={data.isManagedCloud}
|
||||
className={data.isManagedCloud ? "pointer-events-none" : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-h-[60vh] overflow-y-auto">
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center text-sm text-text-dimmed">Loading flags...</div>
|
||||
) : data ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{sortedFlagKeys.map((key) => {
|
||||
const control = data.controlTypes[key];
|
||||
const locked = isLocked(key);
|
||||
const globalValue = data.globalFlags[key as keyof typeof data.globalFlags];
|
||||
const isWorkerGroup = key === FEATURE_FLAG.defaultWorkerInstanceGroupId;
|
||||
const globalDisplay =
|
||||
isWorkerGroup && data.workerGroupName && globalValue !== undefined
|
||||
? `${data.workerGroupName} (${String(globalValue).slice(0, 8)}...)`
|
||||
: globalValue !== undefined
|
||||
? String(globalValue)
|
||||
: "unset";
|
||||
|
||||
if (locked) {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between rounded-md border border-transparent bg-background-hover px-3 py-2.5"
|
||||
title="Global-level setting - not editable per org"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm text-text-dimmed">{key}</div>
|
||||
<div className="text-xs text-text-dimmed">global: {globalDisplay}</div>
|
||||
</div>
|
||||
<LockClosedIcon className="size-4 text-text-faint" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isOverridden = key in overrides;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-md border px-3 py-2.5",
|
||||
isOverridden
|
||||
? "border-indigo-500/20 bg-indigo-500/5"
|
||||
: "border-transparent bg-background-hover"
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className={cn(
|
||||
"truncate text-sm",
|
||||
isOverridden ? "text-text-bright" : "text-text-dimmed"
|
||||
)}
|
||||
>
|
||||
{key}
|
||||
</div>
|
||||
<div className="text-xs text-text-dimmed">global: {globalDisplay}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="minimal/small"
|
||||
onClick={() => unsetFlag(key)}
|
||||
className={cn(!isOverridden && "invisible")}
|
||||
>
|
||||
unset
|
||||
</Button>
|
||||
|
||||
{isWorkerGroup && data.workerGroups ? (
|
||||
<WorkerGroupControl
|
||||
value={isOverridden ? (overrides[key] as string) : undefined}
|
||||
workerGroups={data.workerGroups as WorkerGroup[]}
|
||||
onChange={(val) => {
|
||||
if (val === UNSET_VALUE) {
|
||||
unsetFlag(key);
|
||||
} else {
|
||||
setFlagValue(key, val);
|
||||
}
|
||||
}}
|
||||
dimmed={!isOverridden}
|
||||
/>
|
||||
) : control.type === "boolean" ? (
|
||||
<BooleanControl
|
||||
value={isOverridden ? (overrides[key] as boolean) : undefined}
|
||||
onChange={(val) => setFlagValue(key, val)}
|
||||
dimmed={!isOverridden}
|
||||
/>
|
||||
) : control.type === "enum" ? (
|
||||
<EnumControl
|
||||
value={isOverridden ? (overrides[key] as string) : undefined}
|
||||
options={control.options}
|
||||
onChange={(val) => {
|
||||
if (val === UNSET_VALUE) {
|
||||
unsetFlag(key);
|
||||
} else {
|
||||
setFlagValue(key, val);
|
||||
}
|
||||
}}
|
||||
dimmed={!isOverridden}
|
||||
/>
|
||||
) : control.type === "number" ? (
|
||||
<NumberControl
|
||||
value={isOverridden ? (overrides[key] as number) : undefined}
|
||||
min={control.min}
|
||||
max={control.max}
|
||||
onChange={(val) => {
|
||||
if (val === undefined) {
|
||||
unsetFlag(key);
|
||||
} else {
|
||||
setFlagValue(key, val);
|
||||
}
|
||||
}}
|
||||
dimmed={!isOverridden}
|
||||
/>
|
||||
) : control.type === "string" ? (
|
||||
<StringControl
|
||||
value={isOverridden ? (overrides[key] as string) : ""}
|
||||
onChange={(val) => {
|
||||
if (val === "") {
|
||||
unsetFlag(key);
|
||||
} else {
|
||||
setFlagValue(key, val);
|
||||
}
|
||||
}}
|
||||
dimmed={!isOverridden}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{data && (
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-xs text-text-dimmed hover:text-text-bright">
|
||||
Preview JSON
|
||||
</summary>
|
||||
<pre className="mt-1 max-h-40 overflow-auto rounded bg-background-bright p-2 text-xs text-text-dimmed">
|
||||
{jsonPreview}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{saveError && <Callout variant="error">{saveError}</Callout>}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="tertiary/small" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary/small" onClick={handleSave} disabled={!isDirty || isSaving}>
|
||||
{isSaving ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user