Repository navigation
fix(clusters): explain the two saves a region change takes instead of failing the update - #1849
dawsontoth wants to merge 2 commits into
Conversation
…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.
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 region names the check compares. Refs #1311 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
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.
| const regionSwap = describeRegionSwap({ | ||
| currentRegionNames, | ||
| selectedRegionNames: data.regionPlans.map(regionPlan => regionPlan.regionName), | ||
| singleRegionTier: !selectedPlan?.priceUsd || !!selectedPlan.allowedRegionIds?.length, | ||
| }); |
There was a problem hiding this comment.
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.
| 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), | |
| }); |
⊙ 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 matches a cluster's regions by name and 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 current
stagein a real browser (Playwright, local dev server, mocked API): on/#/<org>/<cluster>/editwith the cluster in US, choosing Europe left the submit button enabled with no message, and submitting sentregionPlans: [{ regionId: 'eu-1', … }], the exact update the server refuses.💡 Solution
On an edit, the form now compares the selected region names 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.
🔧 Changes
src/features/clusters/upsert/lib/describeRegionSwap.ts(new) —regionNamesOfPlansresolves a cluster's plans to region names through the catalog, falling back to the name a plan carries, anddescribeRegionSwapreturns the guidance, or null when any current region is kept, when nothing is chosen yet, or when there are no current regions (creating). It has a separate message for a single-region tier.src/features/clusters/upsert/ClusterForm.tsx— a newcurrentRegionNamesprop, and the zod refinement adds the guidance as an issue onregionPlans.0.regionNamefor hosted clusters.src/features/clusters/upsert/index.tsx— derivescurrentRegionNamesfrom the edited cluster's server plans (not the form draft, which a billing redirect can restore) and passes it in; it is empty when creating.src/features/clusters/DESIGN.md— records the server rule, that the client mirrors only the certain refusal, and the gap below.Not covered: the server also counts shrinking a kept region (a smaller latency/distribution tier of the same region) as leaving it, 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.
Stacked on #1842 (base
claude/1275-region-removal-keeps-siblings); retarget tostageonce that merges. PR #1780 (custom regions) rewritesrefineZodandindex.tsxto work on region ids, so the refinement call and the prop will need to be carried over when it rebases; the helper itself works on names and should survive unchanged.✅ Verification
Route: component tests driving the real
ClusterFormin jsdom (Radix selects opened and picked through clicks), unit tests for the helper, and a live browser check through the real edit route.src/features/clusters/upsert/ClusterForm.test.tsx— four new cases under "ClusterForm editing regions": swapping US for Europe shows the guidance and disables submit (Investigate and improve UI process for changing cluster region #1311); adding a region and keeping US clears it and re-enables submit; the free tier gets the cannot-move-in-place message; a new cluster never sees it. The harness gainedclusterId/currentRegionNamesoptions.src/features/clusters/upsert/lib/describeRegionSwap.test.ts(new) — plan-to-name resolution (catalog first, the plan's own name for a retired region, self-hosted plans skipped and each null and message branch.ctx.addIssuecall disabled, the three positive component cases go red; the new-cluster case stays green.npx vitest run src/features/clusters/upsert→ 75 passed (exit 0). Full gate via the pre-commit hook: vitest 398 files / 3682 tests passed,oxlintclean,dprint check --stagedclean, commitlint ok;npx tsc -bexit 0.index.tsxderivation, since the fixture cluster's plans carry only aregionId.Cross-model review: two rounds, codex (graded) and gemini. Fixed from round 1: the multi-region message told the user to "remove the others" after the first save, when the regions left by then are the ones they kept; and the derivation in
index.tsxhad no test, so it moved into the helper with one. Rejected: explicit import extensions (this repo's imports are extensionless), the allocation of mapping at most 50 rows on each validation of the create form (the same refinement already loops over them), and treating a missingselectedPlanas multi-region (with no plan the form hides the add-region button, so the single-region message is the consistent one). The Cursor leg could not fetch over SSH and the Harper domain adjudicator failed authentication, so outside findings were triaged by hand.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 (rewrites the same refinement and props for region ids)
Complexity: medium
Review-Coverage: authored=claude; ran=codex,gemini; blocked=cursor-composer(no-receipt),domain(auth); declined=cursor-grok,cursor-kimi,cursor-muse; rounds=2; full=1 @ c7381f3
Review-Attention: read ~3m (raised: degraded review) @ c7381f3