From a57167b6b59af818ae0287433e2d3366079b1a5a Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 29 May 2026 16:51:31 -0400 Subject: [PATCH] fix(cells) Hide US2 in customer facing dropdowns Currently US2 is hidden, and only visible to users who already have membership there. However, in the future US2 will become visible, but we still don't want it to be listed as a unique storage location in the UI. Instead we will be mapping 'us' to us2/us1 serverside. These changes update the org-create view and relocation import views to use a common set of helpers to create select options for regions. I took the time to trim out some redundant formatters and expand test coverage as well. Refs INFRENG-331 --- static/app/utils/regions/index.spec.tsx | 95 +++++++++++++++++++ static/app/utils/regions/index.tsx | 50 +++++++--- static/app/views/organizationCreate/index.tsx | 6 +- static/app/views/relocation/getStarted.tsx | 18 +--- static/app/views/relocation/relocation.tsx | 21 ++-- static/gsAdmin/components/forkCustomer.tsx | 8 +- 6 files changed, 154 insertions(+), 44 deletions(-) create mode 100644 static/app/utils/regions/index.spec.tsx diff --git a/static/app/utils/regions/index.spec.tsx b/static/app/utils/regions/index.spec.tsx new file mode 100644 index 000000000000..964ceb893e6b --- /dev/null +++ b/static/app/utils/regions/index.spec.tsx @@ -0,0 +1,95 @@ +import {ConfigStore} from 'sentry/stores/configStore'; +import type {Config} from 'sentry/types/system'; +import {getRegionUrlOptions, getRegionNameOptions} from 'sentry/utils/regions'; + +describe('getRegionUrlOptions', () => { + let configstate: Config; + + beforeEach(() => { + configstate = ConfigStore.getState(); + }); + + afterEach(() => { + ConfigStore.loadInitialData(configstate); + }); + + it('filters out excluded names', () => { + ConfigStore.set('regions', [ + {name: 'us', url: 'https://us.sentry.io'}, + {name: 'de', url: 'https://de.sentry.io'}, + {name: 'ja', url: 'https://ja.sentry.io'}, + ]); + + const res = getRegionUrlOptions([ + {name: 'us', url: 'https://us.sentry.io', displayName: 'us'}, + ]); + expect(res).toHaveLength(2); + expect(res[0]).toEqual({ + value: 'https://de.sentry.io', + label: 'πŸ‡ͺπŸ‡Ί European Union (EU)', + }); + expect(res[1]).toEqual({value: 'https://ja.sentry.io', label: ' ja'}); + + // Excluding the only included option = empty set. + const none = getRegionUrlOptions( + [{name: 'us', url: 'https://us.sentry.io', displayName: 'us'}], + ['us'] + ); + expect(none).toHaveLength(0); + }); + + it('limits to only parameter', () => { + ConfigStore.set('regions', [ + {name: 'us', url: 'https://us.sentry.io'}, + {name: 'de', url: 'https://de.sentry.io'}, + {name: 'ja', url: 'https://ja.sentry.io'}, + ]); + + const res = getRegionUrlOptions([], ['us']); + expect(res).toHaveLength(1); + expect(res[0]).toEqual({ + value: 'https://us.sentry.io', + label: 'πŸ‡ΊπŸ‡Έ United States of America (US)', + }); + }); + + it('always excludes US2', () => { + ConfigStore.set('regions', [ + {name: 'us', url: 'https://us.sentry.io'}, + {name: 'us2', url: 'https://us2.sentry.io'}, + {name: 'de', url: 'https://de.sentry.io'}, + {name: 'ja', url: 'https://ja.sentry.io'}, + ]); + + const res = getRegionUrlOptions(); + expect(res).toHaveLength(3); + res.forEach(item => expect(item.value).not.toContain('us2')); + }); +}); + +describe('getRegionNameOptions', () => { + let configstate: Config; + + beforeEach(() => { + configstate = ConfigStore.getState(); + }); + + afterEach(() => { + ConfigStore.loadInitialData(configstate); + }); + it('always excludes US2', () => { + ConfigStore.set('regions', [ + {name: 'us', url: 'https://us.sentry.io'}, + {name: 'us2', url: 'https://us2.sentry.io'}, + {name: 'de', url: 'https://de.sentry.io'}, + {name: 'ja', url: 'https://ja.sentry.io'}, + ]); + + const res = getRegionNameOptions(); + expect(res).toHaveLength(3); + + expect(res[0]).toEqual({value: 'us', label: 'πŸ‡ΊπŸ‡Έ United States of America (US)'}); + + res.forEach(item => expect(item.label).not.toContain('us2')); + }); +}); diff --git a/static/app/utils/regions/index.tsx b/static/app/utils/regions/index.tsx index 5bbebea42af3..a7c794019970 100644 --- a/static/app/utils/regions/index.tsx +++ b/static/app/utils/regions/index.tsx @@ -1,5 +1,6 @@ import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; +import type {SelectValue} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import type {Region} from 'sentry/types/system'; @@ -36,7 +37,7 @@ export function getRegionDataFromOrganization( ): RegionData | undefined { const {regionUrl} = organization.links; - const regions = ConfigStore.get('regions') ?? []; + const regions = getRegions(); const region = regions.find(value => { return value.url === regionUrl; @@ -58,32 +59,53 @@ export function getRegions(): Region[] { return ConfigStore.get('regions') ?? []; } -export function getRegionChoices(exclude: RegionData[] = []): Array<[string, string]> { +/** + * Get a list of choice tuples with (url, display name) + */ +export function getRegionUrlOptions( + exclude: RegionData[] = [], + only: string[] = [] +): Array> { const regions = getRegions(); const excludedRegionNames = exclude.map(region => region.name); return regions .filter(region => { - return !excludedRegionNames.includes(region.name); + if ( + excludedRegionNames.includes(region.name) || + (only.length > 0 && !only.includes(region.name)) || + CUSTOMER_HIDDEN_REGIONS.has(region.name) + ) { + return false; + } + return true; }) .map(region => { const {url} = region; - return [ - url, - `${getRegionFlagIndicator(region) || ''} ${getRegionDisplayName(region)}`, - ]; + return { + value: url, + label: `${getRegionFlagIndicator(region) || ''} ${getRegionDisplayName(region)}`, + }; }); } -export function getRegionNameChoices(): Array<[string, string]> { +// TODO(cells) Rework/remove this once Region -> Locality config changes are completed. +const CUSTOMER_HIDDEN_REGIONS = new Set(['us2']); + +/** + * Create a list of Choice tuples with (name, display name) + */ +export function getRegionNameOptions(): Array> { const regions = getRegions(); - return regions.map(region => { - return [ - region.name, - `${getRegionFlagIndicator(region) || ''} ${getRegionDisplayName(region)}`, - ]; - }); + return regions + .filter(region => !CUSTOMER_HIDDEN_REGIONS.has(region.name)) + .map(region => { + return { + value: region.name, + label: `${getRegionFlagIndicator(region) || ''} ${getRegionDisplayName(region)}`, + }; + }); } export function shouldDisplayRegions(): boolean { diff --git a/static/app/views/organizationCreate/index.tsx b/static/app/views/organizationCreate/index.tsx index f46520447dd8..dda6cab25a48 100644 --- a/static/app/views/organizationCreate/index.tsx +++ b/static/app/views/organizationCreate/index.tsx @@ -16,7 +16,7 @@ import {t, tct} from 'sentry/locale'; import {getOverride} from 'sentry/overrideRegistry'; import {ConfigStore} from 'sentry/stores/configStore'; import type {OrganizationSummary} from 'sentry/types/organization'; -import {getRegionNameChoices, shouldDisplayRegions} from 'sentry/utils/regions'; +import {getRegionNameOptions, shouldDisplayRegions} from 'sentry/utils/regions'; import {testableWindowLocation} from 'sentry/utils/testableWindowLocation'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useApi} from 'sentry/utils/useApi'; @@ -34,7 +34,7 @@ function OrganizationCreate() { const privacyUrl = ConfigStore.get('privacyUrl'); const isSelfHosted = ConfigStore.get('isSelfHosted'); const relocationUrl = normalizeUrl('/relocation/'); - const regionChoices = getRegionNameChoices(); + const regionOptions = getRegionNameOptions(); const client = useApi(); const hasDataConsent = @@ -119,7 +119,7 @@ function OrganizationCreate() { "Choose where to store your organization's data. Please note, you won't be able to change locations once your organization has been created. [learnMore:Learn More]", {learnMore: } )} - choices={regionChoices} + options={regionOptions} inline={false} stacked required diff --git a/static/app/views/relocation/getStarted.tsx b/static/app/views/relocation/getStarted.tsx index 899ea5bbeaed..f4551f669db3 100644 --- a/static/app/views/relocation/getStarted.tsx +++ b/static/app/views/relocation/getStarted.tsx @@ -8,6 +8,7 @@ import {Select} from '@sentry/scraps/select'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; +import {getRegionUrlOptions} from 'sentry/utils/regions'; import {useApi} from 'sentry/utils/useApi'; import {ContinueButton} from 'sentry/views/relocation/components/continueButton'; import {StepHeading} from 'sentry/views/relocation/components/stepHeading'; @@ -18,17 +19,6 @@ const PROMO_CODE_ERROR_MSG = t( 'That promotional code has already been claimed, does not have enough remaining uses, is no longer valid, or never existed.' ); -// Best-effort region name prettification. -function prettyRegionName(name: string): string { - if (name === 'de') { - return 'πŸ‡ͺπŸ‡Ί European Union (EU)'; - } - if (name === 'us') { - return 'πŸ‡ΊπŸ‡Έ United States of America (US)'; - } - return name; -} - export function GetStarted({ relocationState, onUpdateRelocationState, @@ -38,9 +28,7 @@ export function GetStarted({ const {orgSlugs, regionUrl, promoCode} = relocationState; const [showPromoCode, setShowPromoCode] = useState(!!promoCode); const selectableRegions = ConfigStore.get('relocationConfig')?.selectableRegions || []; - const regions = ConfigStore.get('regions').filter(region => - selectableRegions.includes(region.name) - ); + const regionOptions = getRegionUrlOptions([], selectableRegions); const handleContinue = async (event: any) => { event.preventDefault(); @@ -92,7 +80,7 @@ export function GetStarted({ name="region" aria-label={t('region')} placeholder="Select Location" - options={regions.map(r => ({label: prettyRegionName(r.name), value: r.url}))} + options={regionOptions} onChange={(opt: any) => { onUpdateRelocationState({regionUrl: opt.value}); }} diff --git a/static/app/views/relocation/relocation.tsx b/static/app/views/relocation/relocation.tsx index 5976ed56c76a..597ce4c8023f 100644 --- a/static/app/views/relocation/relocation.tsx +++ b/static/app/views/relocation/relocation.tsx @@ -12,7 +12,7 @@ import {Redirect} from 'sentry/components/redirect'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; import {IconArrow} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {ConfigStore} from 'sentry/stores/configStore'; +import {getRegionUrlOptions} from 'sentry/utils/regions'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useApi} from 'sentry/utils/useApi'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -76,7 +76,7 @@ export function RelocationOnboarding() { const stepObj = onboardingSteps.find(({id}) => stepId === id); const stepIndex = onboardingSteps.findIndex(({id}) => stepId === id); const api = useApi(); - const regions = ConfigStore.get('regions'); + const regionOptions = getRegionUrlOptions(); const [existingRelocationState, setExistingRelocationState] = useState( LoadingState.FETCHING ); @@ -95,10 +95,10 @@ export function RelocationOnboarding() { const fetchExistingRelocation = useCallback(() => { setExistingRelocationState(LoadingState.FETCHING); return Promise.all( - regions.map(region => + regionOptions.map(option => api.requestPromise('/relocations/', { method: 'GET', - host: region.url, + host: option.value, }) ) ) @@ -142,7 +142,7 @@ export function RelocationOnboarding() { setExistingRelocation(''); setExistingRelocationState(LoadingState.ERROR); }); - }, [api, navigate, regions, relocationState, stepId]); + }, [api, navigate, regionOptions, relocationState, stepId]); useEffect(() => { fetchExistingRelocation(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -151,17 +151,20 @@ export function RelocationOnboarding() { const fetchPublicKeys = useCallback(() => { setPublicKeysState(LoadingState.FETCHING); return Promise.all( - regions.map(region => + regionOptions.map(option => api.requestPromise('/publickeys/relocations/', { method: 'GET', - host: region.url, + host: option.value, }) ) ) .then(responses => { setPublicKeys( new Map( - regions.map((region, index) => [region.url, responses[index].public_key]) + regionOptions.map((option, index) => [ + option.value, + responses[index].public_key, + ]) ) ); setPublicKeysState(LoadingState.FETCHED); @@ -170,7 +173,7 @@ export function RelocationOnboarding() { setPublicKeys(new Map()); setPublicKeysState(LoadingState.ERROR); }); - }, [api, regions]); + }, [api, regionOptions]); useEffect(() => { fetchPublicKeys(); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/static/gsAdmin/components/forkCustomer.tsx b/static/gsAdmin/components/forkCustomer.tsx index 644658f09529..f85a6e54c73b 100644 --- a/static/gsAdmin/components/forkCustomer.tsx +++ b/static/gsAdmin/components/forkCustomer.tsx @@ -4,7 +4,7 @@ import {Client} from 'sentry/api'; import {SelectField} from 'sentry/components/forms/fields/selectField'; import type {Organization} from 'sentry/types/organization'; import { - getRegionChoices, + getRegionUrlOptions, getRegionDataFromOrganization, getRegions, } from 'sentry/utils/regions'; @@ -65,14 +65,16 @@ class ForkCustomerActionImpl extends Component { render() { const {organization} = this.props; const currentRegionData = getRegionDataFromOrganization(organization); - const regionChoices = getRegionChoices(currentRegionData ? [currentRegionData] : []); + const regionOptions = getRegionUrlOptions( + currentRegionData ? [currentRegionData] : [] + ); return (