Skip to content

fix(clusters): explain the two saves a region change takes instead of failing the update - #1849

Draft
dawsontoth wants to merge 4 commits into
claude/1275-region-removal-keeps-siblingsfrom
claude/1311-guide-region-swap
Draft

dawsontoth wants to merge 4 commits into
claude/1275-region-removal-keeps-siblingsfrom
claude/1311-guide-region-swap

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

An otter explaining a two-step camp move on a slate

⊙ Problem

Changing the region of an existing cluster fails at the API with "Cannot delete all region plans from a cluster and add new region plan in the same update" (#1311). Central manager refuses any single update that leaves every current region while adding a new one, so a move takes two saves: add the new region and save, then remove the old one. The form gave no hint of that: a user who switched their only region from US to Europe went through payment review and then got the raw API error.

Reproduced on stage in a real browser before this rebase (Playwright, local dev server, mocked API): on /#/<org>/<cluster>/edit with the cluster in US, choosing Europe left the submit button enabled with no message, and submitting sent regionPlans: [{ regionId: 'eu-1', … }], the exact update the server refuses. #1780 does not change that path.

Stack: #1780 ← #1842 ← this PR ← #1851 ← #1870. Based on #1842's branch (claude/1275-region-removal-keeps-siblings); retarget as the PRs below it merge.

❓ Your call: the issue offers two directions, guide the user or run the two saves automatically. This PR guides and blocks. Running both saves for the user would make two infrastructure-changing, separately billed updates from one click and would have to wait for the first to finish (the server refuses updates to a cluster that is not RUNNING) — worth doing only if you want that behaviour as a product decision. Blocking also means that if central manager ever relaxes the rule, the form keeps refusing until Studio ships again; turning the block into a warning is a one-line change.

❓ Your call: a tier that allows a single region (the free tier, or any plan with allowedRegionIds) cannot take the two-step path, because the form will not hold a second region there. The guidance on such a tier says the cluster can't move in place and to create a new cluster in the target region. If support would rather be contacted, or there is another path, that is a one-string change.

💡 Solution

On an edit, the form now compares the regions selected with the regions the cluster runs in now. When none of the current regions is kept, the first region field shows the steps, for example "A cluster can't move out of all its current regions in one update. Keep US, add Europe as an additional region and save, then remove US once that update has finished.", and submitting stays blocked until a current region is kept. The check mirrors only the case the server is certain to refuse, so it never blocks an update the server would accept.

Regions compare the way central manager matches them. A catalog region matches by name, whatever its latency tier. A custom region (#1780's oreg-…) matches by id, since its name is free text: a custom region named "US" does not keep a cluster in the catalog's US. That is #1780's regionCohortKey, and it is how central-manager#892's update decides which current plans are deleted (src/resources/cluster/updateCluster.js:399-404) before refusing a delete-all-and-add update (:436-441). Before the rebase the check compared region names, which #1780's id-keyed form no longer holds.

🔧 Changes

Not covered: the server also counts shrinking a kept region as leaving it (a smaller latency tier of a catalog region, or fewer units of a custom one), so shrinking every current region while adding a new one is refused too. That combination is not mirrored here and still surfaces the server's error.

✅ Verification

Route: component tests driving the real ClusterForm in jsdom (Radix selects opened and picked through clicks, #1780's region lookup), unit tests for the helper.

Cross-model review on the rebased branch (one round, vs #1842's rebuilt branch): codex (graded), gemini and the Harper domain adjudicator all ran. The adjudicator found no case where the check blocks an update the server would accept, and kept five nits. Taken in a8e2443: the steps named only the first of several added regions, and two comments restated the code. Rejected: explicit import extensions (this repo's imports are extensionless), building the cohort list on a create (it fills inside the loop the refinement already runs, over at most 50 rows), and a segmented zod path (every other issue in this refinement, #1780's included, uses the dotted form, and the message lands on the field). Its open decisions (mirror the server rule, block rather than warn, the single-region advice, do-less via mapping the server error) are the calls above; the do-less option would let the update reach payment review and fail there, which is the report in #1311.

Before the rebase, three rounds (codex and gemini; the domain adjudicator ran in the third) fixed the multi-region message's "remove the others" and moved the plan derivation into the helper with a test. gemini-code-assist's suggestion to read a missing selectedPlan as a multi-region tier was declined: such a plan prices at 0, and at 0 the form hides Add Additional Region Usage, which 74b524a pins.

Closes #1311

🤖 Generated by Anthropic Claude Code (Claude Opus 5.5); posted via @dawsontoth.

🤖 Generated with Claude Code

Related PRs: #1842 overlaps (this branch is stacked on it), #1780 overlaps (base of the stack; the check uses its region ids, lookup and cohort key), #1851 overlaps (stacked on this branch)
Complexity: medium

Review-Coverage: authored=claude; ran=codex,gemini; adjudicated=domain; blocked=cursor-composer(no-receipt); declined=cursor-grok,cursor-kimi,cursor-muse; rounds=4; full=1 @ a8e2443

Review-Attention: read ~4m (decisions: client-mirror-of-cm-rule, blocking-vs-advisory, single-region-tier-advice, do-less-alternative) @ a8e2443

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces client-side validation and guidance to prevent users from attempting to move a cluster out of all its current regions in a single update, which is rejected by the central manager. It adds helper functions in describeRegionSwap.ts to detect invalid region swaps and display helpful guidance messages, integrates this validation into ClusterForm, and includes comprehensive tests. Feedback on the pull request points out a potential bug where an undefined selectedPlan could incorrectly flag a plan as a single-region tier, and suggests a fix to explicitly guard the check.

Comment on lines +172 to +176
const regionSwap = describeRegionSwap({
currentRegionNames,
selectedRegionNames: data.regionPlans.map(regionPlan => regionPlan.regionName),
singleRegionTier: !selectedPlan?.priceUsd || !!selectedPlan.allowedRegionIds?.length,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When selectedPlan is undefined (e.g., during initial load or if the selected deployment/performance descriptions are temporarily invalid), !selectedPlan?.priceUsd evaluates to true. This incorrectly flags the plan as a singleRegionTier, which can display a misleading "single-region tier" error message to the user instead of a generic or no error message.

We should explicitly guard this check to ensure selectedPlan is defined before determining if it is a single-region tier.

Suggested change
const regionSwap = describeRegionSwap({
currentRegionNames,
selectedRegionNames: data.regionPlans.map(regionPlan => regionPlan.regionName),
singleRegionTier: !selectedPlan?.priceUsd || !!selectedPlan.allowedRegionIds?.length,
});
const regionSwap = describeRegionSwap({
currentRegionNames,
selectedRegionNames: data.regionPlans.map(regionPlan => regionPlan.regionName),
singleRegionTier: !!selectedPlan && (!selectedPlan.priceUsd || !!selectedPlan.allowedRegionIds?.length),
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining. I applied the suggestion first and it made the guidance worse, so I took it back out.

When the selected tier isn't in the catalog, selectedPlan is undefined, so totalPrice is 0 (ClusterForm.tsx:301). At 0, ClusterRegions hides Add Additional Region Usage (ClusterRegions.tsx:47, "Free plans can only add a single region"). The form therefore already treats an unknown tier as single-region. With the suggestion, the message would say "add Europe as an additional region and save", and the form shows no control to do that. The current predicate matches the form, and a16e5c5 adds a test that pins the message and the missing button together.

A plan that is undefined only while data loads doesn't reach this message. The guidance appears only when the selection drops every current region, and an edit opens on the cluster's current regions.

🤖 Generated by Claude Code (Claude Opus 5.5); posted via @dawsontoth.

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 68.61% 10467 / 15255
🔵 Statements 68.76% 11167 / 16240
🔵 Functions 62.11% 2708 / 4360
🔵 Branches 63.56% 8048 / 12661
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/clusters/upsert/ClusterForm.tsx 60.57% 43.11% 62.96% 60.3% 134, 137-146, 151-155, 167-174, 181-185, 192-196, 199-203, 206-210, 228-232, 237-241, 298, 301-314, 318, 321, 324, 367, 378, 411, 430-439, 458-491, 495-575, 594-598, 602, 606
src/features/clusters/upsert/index.tsx 0% 0% 0% 0% 45-374
src/features/clusters/upsert/lib/describeRegionSwap.ts 100% 96% 100% 100%
Generated in workflow #2259 for commit a8e2443 by the Vitest Coverage Report Action

@dawsontoth
dawsontoth removed this pull request from stack #1907 October 12, 2026 17:25
dawsontoth and others added 3 commits October 12, 2026 14:05
…pdate fails

Central manager refuses a cluster update that leaves every region the cluster runs in while adding another ("Cannot delete all region plans from a cluster and add new region plan in the same update"). Changing an existing cluster's only region went all the way through payment review and then failed with that error.

Flag that selection on an edit, under the first region, with the steps: keep the current region, add the new one and save, then remove the old one. A tier that allows a single region cannot do that, so it says the cluster cannot move in place instead. Submitting stays blocked until a current region is kept.

Regions compare the way central manager matches them: a catalog region by name, whatever its latency tier, and a custom region by id (regionCohortKey), since its name is free text.

Refs #1311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With several current regions the guidance said "remove the others" after the first save, but the regions left by then are the ones the user kept. Name those instead, skip the work entirely when creating, and test how a cluster's plans resolve to the regions the check compares: a catalog plan by its region name, a custom region by its id.

Refs #1311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom the catalog

With no plan for the selected tier the price is 0, so the form hides Add Additional Region Usage. The guidance has to say to create a new cluster, not to add a region the form won't let the user add.

Refs #1311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth
dawsontoth force-pushed the claude/1311-guide-region-swap branch from a16e5c5 to 74b524a Compare October 12, 2026 18:05
With Europe and Canada chosen to replace US, the steps said to add Europe and left Canada out, so following them to the letter dropped a region. Name each region being added, and drop two comments that only restated the code.

Refs #1311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate and improve UI process for changing cluster region

1 participant