Skip to content

Commit 607af80

Browse files
committed
fix: always skip locked flags in delete loop to prevent accidental deletion on self-hosted
1 parent e17007e commit 607af80

File tree

1 file changed

+18
-11
lines changed

1 file changed

+18
-11
lines changed

apps/webapp/app/routes/admin.feature-flags.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,12 @@ export const action = async ({ request }: ActionFunctionArgs) => {
124124

125125
const validatedFlags = validationResult.data as Record<string, unknown>;
126126
const controlTypes = getAllFlagControlTypes();
127-
const lockedKeys = isManagedCloud ? GLOBAL_LOCKED_FLAGS : [];
128-
const editableKeys = Object.keys(controlTypes).filter(
129-
(key) => !lockedKeys.includes(key)
130-
);
127+
const catalogKeys = Object.keys(controlTypes);
131128

132129
const keysToDelete: string[] = [];
133130
const upsertOps: ReturnType<typeof prisma.featureFlag.upsert>[] = [];
134131

135-
for (const key of editableKeys) {
132+
for (const key of catalogKeys) {
136133
if (key in validatedFlags) {
137134
upsertOps.push(
138135
prisma.featureFlag.upsert({
@@ -142,7 +139,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
142139
})
143140
);
144141
} else {
145-
keysToDelete.push(key);
142+
// On cloud, never delete locked flags (they're not in the payload
143+
// because the UI doesn't include them). Locally, delete everything
144+
// the user didn't include - full control.
145+
const isProtected = isManagedCloud && GLOBAL_LOCKED_FLAGS.includes(key);
146+
if (!isProtected) {
147+
keysToDelete.push(key);
148+
}
146149
}
147150
}
148151

@@ -219,8 +222,10 @@ export default function AdminFeatureFlagsRoute() {
219222
});
220223
};
221224

222-
const sortedFlagKeys = Object.keys(controlTypes as Record<string, FlagControlType>).sort();
225+
const typedControlTypes = controlTypes as Record<string, FlagControlType>;
226+
const typedResolvedDefaults = resolvedDefaults as Record<string, string>;
223227
const allFlags = (globalFlags ?? {}) as Record<string, unknown>;
228+
const sortedFlagKeys = Object.keys(typedControlTypes).sort();
224229
const workerGroupMap = new Map((workerGroups as WorkerGroup[]).map((wg) => [wg.id, wg.name]));
225230

226231
const resolveWorkerGroupDisplay = (id: string) => {
@@ -253,7 +258,7 @@ export default function AdminFeatureFlagsRoute() {
253258

254259
<div className="flex flex-col gap-1.5">
255260
{sortedFlagKeys.map((key) => {
256-
const control = (controlTypes as Record<string, FlagControlType>)[key];
261+
const control = typedControlTypes[key];
257262
const locked = isLocked(key);
258263

259264
if (locked) {
@@ -262,7 +267,7 @@ export default function AdminFeatureFlagsRoute() {
262267
key={key}
263268
flagKey={key}
264269
value={allFlags[key]}
265-
resolvedDefault={(resolvedDefaults as Record<string, string>)[key]}
270+
resolvedDefault={typedResolvedDefaults[key]}
266271
workerGroupName={workerGroupName as string | undefined}
267272
/>
268273
);
@@ -295,7 +300,9 @@ export default function AdminFeatureFlagsRoute() {
295300
? isWorkerGroup
296301
? resolveWorkerGroupDisplay(values[key] as string)
297302
: `value: ${String(values[key])}`
298-
: "not set"}
303+
: typedResolvedDefaults[key]
304+
? `${typedResolvedDefaults[key]} (from env)`
305+
: "not set"}
299306
</div>
300307
</div>
301308

@@ -390,7 +397,7 @@ export default function AdminFeatureFlagsRoute() {
390397
onOpenChange={setConfirmOpen}
391398
initialValues={initialValues}
392399
newValues={values}
393-
controlTypes={controlTypes as Record<string, FlagControlType>}
400+
controlTypes={typedControlTypes}
394401
lockedKeys={unlocked ? [] : GLOBAL_LOCKED_FLAGS}
395402
onConfirm={handleSave}
396403
isSaving={isSaving}

0 commit comments

Comments
 (0)