From bd21d9bfd6d772a214f55c8e6459364d85100d7b Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 16:22:08 -0400 Subject: [PATCH 01/11] initial work --- packages/api-v4/src/regions/types.ts | 4 +- .../RegionSelect/RegionMultiSelect.tsx | 2 +- .../components/RegionSelect/RegionOption.tsx | 31 +++--- .../components/RegionSelect/RegionSelect.tsx | 99 ++++++++----------- .../RegionSelect/RegionSelect.utils.tsx | 68 ++++++++++++- .../manager/src/utilities/formatRegion.ts | 19 +++- 6 files changed, 144 insertions(+), 79 deletions(-) diff --git a/packages/api-v4/src/regions/types.ts b/packages/api-v4/src/regions/types.ts index 30329f4e8f8..5df1994d2d9 100644 --- a/packages/api-v4/src/regions/types.ts +++ b/packages/api-v4/src/regions/types.ts @@ -47,6 +47,6 @@ export interface RegionAvailability { region: string; } -type ContinentCode = keyof typeof COUNTRY_CODE_TO_CONTINENT_CODE; +type CountryCode = keyof typeof COUNTRY_CODE_TO_CONTINENT_CODE; -export type Country = Lowercase; +export type Country = Lowercase; diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx index 2c69a74d0d5..6b77ff478fe 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx @@ -148,7 +148,7 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { return ( diff --git a/packages/manager/src/components/RegionSelect/RegionOption.tsx b/packages/manager/src/components/RegionSelect/RegionOption.tsx index 33c4a1b36f9..6df3fc81d75 100644 --- a/packages/manager/src/components/RegionSelect/RegionOption.tsx +++ b/packages/manager/src/components/RegionSelect/RegionOption.tsx @@ -13,34 +13,35 @@ import { StyledListItem, sxDistributedRegionIcon, } from './RegionSelect.styles'; -import { RegionSelectOption } from './RegionSelect.types'; +import type { Region } from '@linode/api-v4'; import type { ListItemComponentsPropsOverrides } from '@mui/material/ListItem'; -type Props = { +interface Props { + disabledReason?: string; displayDistributedRegionIcon?: boolean; - option: RegionSelectOption; props: React.HTMLAttributes; + region: Region; selected?: boolean; -}; +} export const RegionOption = ({ + disabledReason, displayDistributedRegionIcon, - option, props, + region, selected, }: Props) => { const { className, onClick } = props; - const { data, disabledProps, label, value } = option; - const isRegionDisabled = Boolean(disabledProps?.disabled); - const isRegionDisabledReason = disabledProps?.reason; + const isRegionDisabled = Boolean(disabledReason); + const isRegionDisabledReason = disabledReason; return ( @@ -72,9 +73,9 @@ export const RegionOption = ({ <> - + - {label} + {region.label} ({region.id}) {displayDistributedRegionIcon && (  (This region is a distributed region.) @@ -84,7 +85,7 @@ export const RegionOption = ({ {isRegionDisabledReason} )} - {selected && } + {selected && } {displayDistributedRegionIcon && ( } diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.tsx index 745857911a1..bbbdd4ba60e 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.tsx @@ -7,6 +7,7 @@ import { Flag } from 'src/components/Flag'; import { Link } from 'src/components/Link'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { useAllAccountAvailabilitiesQuery } from 'src/queries/account/availability'; +import { getRegionCountryGroup } from 'src/utilities/formatRegion'; import { RegionOption } from './RegionOption'; import { @@ -15,12 +16,12 @@ import { StyledFlagContainer, sxDistributedRegionIcon, } from './RegionSelect.styles'; -import { getRegionOptions, getSelectedRegionById } from './RegionSelect.utils'; +import { + getRegionOptionsv2, + isRegionOptionUnavailable, +} from './RegionSelect.utils'; -import type { - RegionSelectOption, - RegionSelectProps, -} from './RegionSelect.types'; +import type { RegionSelectProps } from './RegionSelect.types'; /** * A specific select for regions. @@ -55,62 +56,40 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { isLoading: accountAvailabilityLoading, } = useAllAccountAvailabilitiesQuery(); - const regionFromSelectedId: RegionSelectOption | null = - getSelectedRegionById({ - accountAvailabilityData: accountAvailability, - currentCapability, - regions, - selectedRegionId: selectedId ?? '', - }) ?? null; - - const [selectedRegion, setSelectedRegion] = React.useState< - RegionSelectOption | null | undefined - >(regionFromSelectedId); - - const handleRegionChange = (selection: RegionSelectOption | null) => { - setSelectedRegion(selection); - handleSelection(selection?.value || ''); - }; + const regionOptions = getRegionOptionsv2({ + currentCapability, + regionFilter, + regions, + }); - React.useEffect(() => { - if (selectedId) { - setSelectedRegion(regionFromSelectedId); - } else { - // We need to reset the state when create types change - setSelectedRegion(null); - } - }, [selectedId, regions]); + const selectedRegion = regionOptions.find((r) => r.id === selectedId) ?? null; - const options = React.useMemo( - () => - getRegionOptions({ - accountAvailabilityData: accountAvailability, - currentCapability, - handleDisabledRegion, - regionFilter, - regions, - }), - [ - accountAvailability, - currentCapability, - handleDisabledRegion, - regions, - regionFilter, - ] + const disabledRegions = regionOptions.reduce>( + (acc, region) => { + const disabledInfo = handleDisabledRegion?.(region); + if (disabledInfo?.disabled) { + acc[region.id] = disabledInfo.reason; + } + if ( + isRegionOptionUnavailable({ + accountAvailabilityData: accountAvailability, + currentCapability, + region, + }) + ) { + acc[region.id] = + 'This region is currently unavailable. For help, open a support ticket.'; + } + return acc; + }, + {} ); return ( - Boolean(option.disabledProps?.disabled) - } - isOptionEqualToValue={( - option: RegionSelectOption, - { value }: RegionSelectOption - ) => option.value === value} - onChange={(_, selectedOption: RegionSelectOption) => { - handleRegionChange(selectedOption); + onChange={(_, selectedOption) => { + handleSelection(selectedOption!.id); }} renderOption={(props, option) => { return ( @@ -120,9 +99,10 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { (option.site_type === 'distributed' || option.site_type === 'edge') } - key={option.value} - option={option} + disabledReason={disabledRegions[option.id]} + key={option.id} props={props} + region={option} /> ); }} @@ -147,7 +127,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { required, startAdornment: selectedRegion && ( - + ), }, @@ -159,13 +139,14 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { disableClearable={!isClearable} disabled={disabled} errorText={errorText} - groupBy={(option: RegionSelectOption) => option.data.region} + getOptionDisabled={(option) => disabledRegions[option.id]} + groupBy={(option) => getRegionCountryGroup(option)} helperText={helperText} label={label ?? 'Region'} loading={accountAvailabilityLoading} loadingText="Loading regions..." noOptionsText="No results" - options={options} + options={regionOptions} placeholder="Select a Region" value={selectedRegion} /> diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx index 41c782d9e57..b9614c43937 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx @@ -1,4 +1,8 @@ -import { CONTINENT_CODE_TO_CONTINENT } from '@linode/api-v4'; +import { + Capabilities, + CONTINENT_CODE_TO_CONTINENT, + RegionSite, +} from '@linode/api-v4'; import { getRegionCountryGroup, @@ -121,6 +125,68 @@ export const getRegionOptions = ({ }); }; +interface RegionSelectOptionsOptions { + regions: Region[]; + currentCapability: Capabilities | undefined; + regionFilter?: RegionSite; +} + +export const getRegionOptionsv2 = ({ + regions, + currentCapability, + regionFilter, +}: RegionSelectOptionsOptions) => { + return regions + .filter((region) => { + let keepRegion = true; + if (currentCapability) { + keepRegion = region.capabilities.includes(currentCapability); + } + if (regionFilter) { + keepRegion = region.site_type === regionFilter; + } + return keepRegion; + }) + .sort((region1, region2) => { + const region1Group = getRegionCountryGroup(region1); + const region2Group = getRegionCountryGroup(region2); + + // North America group comes first + if ( + region1Group === 'North America' && + region2Group !== 'North America' + ) { + return -1; + } + if (region1Group !== NORTH_AMERICA && region2Group === NORTH_AMERICA) { + return 1; + } + + // Rest of the regions are sorted alphabetically + if (region1Group < region2Group) { + return -1; + } + if (region1Group > region2Group) { + return 1; + } + + // Then we group by country + if (region1.country < region2.country) { + return 1; + } + if (region1.country > region2.country) { + return -1; + } + + // If regions are in the same group or country, sort alphabetically by label + if (region1.label < region2.label) { + return -1; + } + + return 1; + }); +}; + /** * Util to map a region ID to an OptionType object. * diff --git a/packages/manager/src/utilities/formatRegion.ts b/packages/manager/src/utilities/formatRegion.ts index f1c2317c2a9..b3f585da1a1 100644 --- a/packages/manager/src/utilities/formatRegion.ts +++ b/packages/manager/src/utilities/formatRegion.ts @@ -23,13 +23,30 @@ export const getRegionCountryGroup = (region: Region | undefined) => { } const continentCode = - COUNTRY_CODE_TO_CONTINENT_CODE[region.country.toUpperCase() as Country]; + COUNTRY_CODE_TO_CONTINENT_CODE[ + region.country.toUpperCase() as Uppercase + ]; return continentCode ? CONTINENT_CODE_TO_CONTINENT[continentCode] ?? 'Other' : 'Other'; }; +export const getRegionCountryGroupByCountryCode = (countryCode: string | undefined) => { + if (!countryCode) { + return 'Other'; + } + + const continentCode = + COUNTRY_CODE_TO_CONTINENT_CODE[ + countryCode.toUpperCase() as Uppercase + ]; + + return continentCode + ? CONTINENT_CODE_TO_CONTINENT[continentCode] ?? 'Other' + : 'Other'; +} + export const getSelectedRegion = ( regions: Region[], selectedRegionId: string From afd98e260f81cc486b8142a8c7b15aaff06a0d9c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 17:19:37 -0400 Subject: [PATCH 02/11] save more work --- .../RegionMultiSelect.stories.tsx | 6 +- .../RegionSelect/RegionMultiSelect.test.tsx | 15 +- .../RegionSelect/RegionMultiSelect.tsx | 110 ++++------ .../components/RegionSelect/RegionOption.tsx | 11 +- .../components/RegionSelect/RegionSelect.tsx | 55 ++--- .../RegionSelect/RegionSelect.types.ts | 33 +-- .../RegionSelect/RegionSelect.utils.test.tsx | 193 ++---------------- .../RegionSelect/RegionSelect.utils.tsx | 185 ++--------------- .../AccessKeyRegions/AccessKeyRegions.tsx | 8 +- .../AccessKeyRegions/SelectedRegionsList.tsx | 4 +- .../PlacementGroupsCreateDrawer.tsx | 55 ++--- 11 files changed, 160 insertions(+), 515 deletions(-) diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx index ed4af598942..0abdb665b0f 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx @@ -8,10 +8,10 @@ import { sortByString } from 'src/utilities/sort-by'; import { RegionMultiSelect } from './RegionMultiSelect'; import type { RegionMultiSelectProps } from './RegionSelect.types'; +import type { Region } from '@linode/api-v4'; import type { Meta, StoryObj } from '@storybook/react'; -import type { RegionSelectOption } from 'src/components/RegionSelect/RegionSelect.types'; -const sortRegionOptions = (a: RegionSelectOption, b: RegionSelectOption) => { +const sortRegionOptions = (a: Region, b: Region) => { return sortByString(a.label, b.label, 'asc'); }; @@ -30,7 +30,7 @@ export const Default: StoryObj = { diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx index 9a055bb41ad..12a07021719 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx @@ -1,3 +1,4 @@ +import { Region } from '@linode/api-v4'; import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; @@ -6,8 +7,6 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { RegionMultiSelect } from './RegionMultiSelect'; -import type { RegionSelectOption } from 'src/components/RegionSelect/RegionSelect.types'; - const regions = regionFactory.buildList(1, { id: 'us-east', label: 'Newark, NJ', @@ -23,7 +22,7 @@ const regionsAtlanta = regionFactory.buildList(1, { }); interface SelectedRegionsProps { onRemove: (region: string) => void; - selectedRegions: RegionSelectOption[]; + selectedRegions: Region[]; } const SelectedRegionsList = ({ onRemove, @@ -33,7 +32,7 @@ const SelectedRegionsList = ({ {selectedRegions.map((region, index) => (
  • {region.label} - +
  • ))} @@ -46,7 +45,7 @@ describe('RegionMultiSelect', () => { renderWithTheme( @@ -59,7 +58,7 @@ describe('RegionMultiSelect', () => { renderWithTheme( @@ -87,7 +86,7 @@ describe('RegionMultiSelect', () => { renderWithTheme( @@ -121,7 +120,7 @@ describe('RegionMultiSelect', () => { /> )} currentCapability="Block Storage" - handleSelection={mockHandleSelection} + onChange={mockHandleSelection} regions={[...regionsNewark, ...regionsAtlanta]} selectedIds={[]} /> diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx index 6b77ff478fe..c502d1b15c7 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx @@ -1,12 +1,13 @@ +import { Region } from '@linode/api-v4'; import CloseIcon from '@mui/icons-material/Close'; -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; -import { StyledListItem } from 'src/components/Autocomplete/Autocomplete.styles'; import { Box } from 'src/components/Box'; import { Chip } from 'src/components/Chip'; import { Flag } from 'src/components/Flag'; import { useAllAccountAvailabilitiesQuery } from 'src/queries/account/availability'; +import { getRegionCountryGroup } from 'src/utilities/formatRegion'; import { RegionOption } from './RegionOption'; import { @@ -15,19 +16,20 @@ import { } from './RegionSelect.styles'; import { getRegionOptions, - getSelectedRegionsByIds, + isRegionOptionUnavailable, } from './RegionSelect.utils'; import type { + DisableRegionOption, RegionMultiSelectProps, - RegionSelectOption, } from './RegionSelect.types'; +import { StyledListItem } from '../Autocomplete/Autocomplete.styles'; interface LabelComponentProps { - selection: RegionSelectOption; + region: Region; } -const SelectedRegion = ({ selection }: LabelComponentProps) => { +const SelectedRegion = ({ region }: LabelComponentProps) => { return ( { transform: 'scale(0.8)', })} > - + - {selection.label} + {region.label} ); }; @@ -55,7 +57,7 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { currentCapability, disabled, errorText, - handleSelection, + onChange, helperText, isClearable, label, @@ -72,74 +74,44 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { isLoading: accountAvailabilityLoading, } = useAllAccountAvailabilitiesQuery(); - const [selectedRegions, setSelectedRegions] = useState( - getSelectedRegionsByIds({ - accountAvailabilityData: accountAvailability, - currentCapability, - regions, - selectedRegionIds: selectedIds ?? [], - }) + const regionOptions = getRegionOptions({ currentCapability, regions }); + + const selectedRegions = regionOptions.filter((r) => + selectedIds.includes(r.id) ); - const handleRegionChange = (selection: RegionSelectOption[]) => { - setSelectedRegions(selection); + const handleRemoveOption = (regionToRemove: string) => { + onChange(selectedIds.filter((value) => value !== regionToRemove)); }; - useEffect(() => { - setSelectedRegions( - getSelectedRegionsByIds({ + const disabledRegions = regionOptions.reduce< + Record + >((acc, region) => { + if ( + isRegionOptionUnavailable({ accountAvailabilityData: accountAvailability, currentCapability, - regions, - selectedRegionIds: selectedIds ?? [], + region, }) - ); - }, [selectedIds, accountAvailability, currentCapability, regions]); - - const options = useMemo( - () => - getRegionOptions({ - accountAvailabilityData: accountAvailability, - currentCapability, - regions, - }), - [accountAvailability, currentCapability, regions] - ); - - const handleRemoveOption = (regionToRemove: string) => { - const updatedSelectedOptions = selectedRegions.filter( - (option) => option.value !== regionToRemove - ); - const updatedSelectedIds = updatedSelectedOptions.map( - (region) => region.value - ); - setSelectedRegions(updatedSelectedOptions); - handleSelection(updatedSelectedIds); - }; + ) { + acc[region.id] = { + reason: + 'This region is currently unavailable. For help, open a support ticket.', + }; + } + return acc; + }, {}); return ( <> - Boolean(option.disabledProps?.disabled) + getOptionDisabled={(option) => Boolean(disabledRegions[option.id])} + onChange={(_, selectedOptions) => + onChange(selectedOptions.map((region) => region.id)) } - groupBy={(option: RegionSelectOption) => { - return option?.data?.region; - }} - isOptionEqualToValue={( - option: RegionSelectOption, - value: RegionSelectOption - ) => option.value === value.value} - onChange={(_, selectedOption) => - handleRegionChange(selectedOption as RegionSelectOption[]) - } - onClose={() => { - const selectedIds = selectedRegions.map((region) => region.value); - handleSelection(selectedIds); - }} renderOption={(props, option, { selected }) => { - if (!option.data) { + if (!option.site_type) { // Render options like "Select All / Deselect All " return {option.label}; } @@ -147,10 +119,11 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { // Render regular options return ( ); }} @@ -158,11 +131,11 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { return tagValue.map((option, index) => ( } key={index} - label={} - onDelete={() => handleRemoveOption(option.value)} + label={} + onDelete={() => handleRemoveOption(option.id)} /> )); }} @@ -184,11 +157,12 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { disableClearable={!isClearable} disabled={disabled} errorText={errorText} + groupBy={(option) => getRegionCountryGroup(option)} label={label ?? 'Regions'} loading={accountAvailabilityLoading} multiple noOptionsText="No results" - options={options} + options={regionOptions} placeholder={placeholder ?? 'Select Regions'} value={selectedRegions} /> diff --git a/packages/manager/src/components/RegionSelect/RegionOption.tsx b/packages/manager/src/components/RegionSelect/RegionOption.tsx index 6df3fc81d75..45cae51ff39 100644 --- a/packages/manager/src/components/RegionSelect/RegionOption.tsx +++ b/packages/manager/src/components/RegionSelect/RegionOption.tsx @@ -14,11 +14,12 @@ import { sxDistributedRegionIcon, } from './RegionSelect.styles'; +import type { DisableRegionOption } from './RegionSelect.types'; import type { Region } from '@linode/api-v4'; import type { ListItemComponentsPropsOverrides } from '@mui/material/ListItem'; interface Props { - disabledReason?: string; + disabledOptions?: DisableRegionOption; displayDistributedRegionIcon?: boolean; props: React.HTMLAttributes; region: Region; @@ -26,22 +27,22 @@ interface Props { } export const RegionOption = ({ - disabledReason, + disabledOptions, displayDistributedRegionIcon, props, region, selected, }: Props) => { const { className, onClick } = props; - const isRegionDisabled = Boolean(disabledReason); - const isRegionDisabledReason = disabledReason; + const isRegionDisabled = Boolean(disabledOptions); + const isRegionDisabledReason = disabledOptions?.reason; return ( { const { currentCapability, disabled, + disabledRegions: disabledRegionsFromProps, errorText, - handleDisabledRegion, handleSelection, helperText, isClearable, @@ -56,7 +59,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { isLoading: accountAvailabilityLoading, } = useAllAccountAvailabilitiesQuery(); - const regionOptions = getRegionOptionsv2({ + const regionOptions = getRegionOptions({ currentCapability, regionFilter, regions, @@ -64,26 +67,26 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { const selectedRegion = regionOptions.find((r) => r.id === selectedId) ?? null; - const disabledRegions = regionOptions.reduce>( - (acc, region) => { - const disabledInfo = handleDisabledRegion?.(region); - if (disabledInfo?.disabled) { - acc[region.id] = disabledInfo.reason; - } - if ( - isRegionOptionUnavailable({ - accountAvailabilityData: accountAvailability, - currentCapability, - region, - }) - ) { - acc[region.id] = - 'This region is currently unavailable. For help, open a support ticket.'; - } - return acc; - }, - {} - ); + const disabledRegions = regionOptions.reduce< + Record + >((acc, region) => { + if (disabledRegionsFromProps?.[region.id]) { + acc[region.id] = disabledRegionsFromProps[region.id]; + } + if ( + isRegionOptionUnavailable({ + accountAvailabilityData: accountAvailability, + currentCapability, + region, + }) + ) { + acc[region.id] = { + reason: + 'This region is currently unavailable. For help, open a support ticket.', + }; + } + return acc; + }, {}); return ( @@ -99,7 +102,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { (option.site_type === 'distributed' || option.site_type === 'edge') } - disabledReason={disabledRegions[option.id]} + disabledOptions={disabledRegions[option.id]} key={option.id} props={props} region={option} @@ -139,7 +142,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { disableClearable={!isClearable} disabled={disabled} errorText={errorText} - getOptionDisabled={(option) => disabledRegions[option.id]} + getOptionDisabled={(option) => Boolean(disabledRegions[option.id])} groupBy={(option) => getRegionCountryGroup(option)} helperText={helperText} label={label ?? 'Region'} diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts index e7a37e6c9dd..45b3fcf1a3b 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts +++ b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts @@ -3,30 +3,19 @@ import React from 'react'; import type { AccountAvailability, Capabilities, - Country, Region, RegionSite, } from '@linode/api-v4'; import type { EnhancedAutocompleteProps } from 'src/components/Autocomplete/Autocomplete'; -export interface RegionSelectOption { - data: { - country: Country; - region: string; - }; - disabledProps?: { - disabled: boolean; - reason?: JSX.Element | string; - tooltipWidth?: number; - }; - label: string; - site_type: RegionSite; - value: string; +export interface DisableRegionOption { + reason: JSX.Element | string; + tooltipWidth?: number; } export interface RegionSelectProps extends Omit< - EnhancedAutocompleteProps, + EnhancedAutocompleteProps, 'label' | 'onChange' | 'options' > { /** @@ -37,9 +26,7 @@ export interface RegionSelectProps * See `ImageUpload.tsx` for an example of a RegionSelect with an undefined `currentCapability` - there is no capability associated with Images yet. */ currentCapability: Capabilities | undefined; - handleDisabledRegion?: ( - region: Region - ) => RegionSelectOption['disabledProps']; + disabledRegions?: Record; handleSelection: (id: string) => void; helperText?: string; isClearable?: boolean; @@ -55,22 +42,22 @@ export interface RegionSelectProps export interface RegionMultiSelectProps extends Omit< - EnhancedAutocompleteProps, + EnhancedAutocompleteProps, 'label' | 'onChange' | 'options' > { SelectedRegionsList?: React.ComponentType<{ onRemove: (region: string) => void; - selectedRegions: RegionSelectOption[]; + selectedRegions: Region[]; }>; currentCapability: Capabilities | undefined; - handleSelection: (ids: string[]) => void; + onChange: (ids: string[]) => void; helperText?: string; isClearable?: boolean; label?: string; regions: Region[]; required?: boolean; selectedIds: string[]; - sortRegionOptions?: (a: RegionSelectOption, b: RegionSelectOption) => number; + sortRegionOptions?: (a: Region, b: Region) => number; tooltipText?: string; width?: number; } @@ -80,7 +67,7 @@ export interface RegionOptionAvailability { currentCapability: Capabilities | undefined; handleDisabledRegion?: ( region: Region - ) => RegionSelectOption['disabledProps']; + ) => DisableRegionOption; } export interface GetRegionOptions extends RegionOptionAvailability { diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx index d7c05901120..3c43ac96592 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx @@ -2,12 +2,9 @@ import { accountAvailabilityFactory, regionFactory } from 'src/factories'; import { getRegionOptions, - getSelectedRegionById, - getSelectedRegionsByIds, isRegionOptionUnavailable, } from './RegionSelect.utils'; -import type { RegionSelectOption } from './RegionSelect.types'; import type { Region } from '@linode/api-v4'; const accountAvailabilityData = [ @@ -56,37 +53,22 @@ const distributedRegions = [ }), ]; -const expectedRegions: RegionSelectOption[] = [ - { - data: { - country: 'us', - region: 'North America', - }, - disabledProps: { - disabled: false, - }, +const expectedRegions: Region[] = [ + regionFactory.build({ label: 'US Location (us-1)', site_type: 'core', - value: 'us-1', - }, - { - data: { country: 'ca', region: 'North America' }, - disabledProps: { - disabled: false, - }, + id: 'us-1', + }), + regionFactory.build({ label: 'CA Location (ca-1)', site_type: 'core', - value: 'ca-1', - }, - { - data: { country: 'jp', region: 'Asia' }, - disabledProps: { - disabled: false, - }, + id: 'ca-1', + }), + regionFactory.build({ label: 'JP Location (jp-1)', site_type: 'core', - value: 'jp-1', - }, + id: 'jp-1', + }), ]; const expectedDistributedRegions = [ @@ -114,7 +96,6 @@ describe('getRegionOptions', () => { it('should return an empty array if no regions are provided', () => { const regions: Region[] = []; const result = getRegionOptions({ - accountAvailabilityData, currentCapability: 'Linodes', regions, }); @@ -123,8 +104,7 @@ describe('getRegionOptions', () => { }); it('should return a sorted array of OptionType objects with North America first', () => { - const result: RegionSelectOption[] = getRegionOptions({ - accountAvailabilityData, + const result = getRegionOptions({ currentCapability: 'Linodes', regions, }); @@ -143,8 +123,7 @@ describe('getRegionOptions', () => { }), ]; - const result: RegionSelectOption[] = getRegionOptions({ - accountAvailabilityData, + const result = getRegionOptions({ currentCapability: 'Linodes', regions: regionsToFilter, }); @@ -153,8 +132,7 @@ describe('getRegionOptions', () => { }); it('should filter out distributed regions if regionFilter is core', () => { - const result: RegionSelectOption[] = getRegionOptions({ - accountAvailabilityData, + const result = getRegionOptions({ currentCapability: 'Linodes', regionFilter: 'core', regions: distributedRegions, @@ -164,8 +142,7 @@ describe('getRegionOptions', () => { }); it('should filter out core regions if regionFilter is "distributed"', () => { - const result: RegionSelectOption[] = getRegionOptions({ - accountAvailabilityData, + const result = getRegionOptions({ currentCapability: 'Linodes', regionFilter: 'distributed', regions: distributedRegions, @@ -177,8 +154,7 @@ describe('getRegionOptions', () => { it('should not filter out any regions if regionFilter is undefined', () => { const regions = [...expectedDistributedRegions, ...expectedRegions]; - const result: RegionSelectOption[] = getRegionOptions({ - accountAvailabilityData, + const result = getRegionOptions({ currentCapability: 'Linodes', regionFilter: undefined, regions: distributedRegions, @@ -186,85 +162,6 @@ describe('getRegionOptions', () => { expect(result).toEqual(regions); }); - - it('should have its option disabled if the region is unavailable', () => { - const _regions = [ - ...regions, - regionFactory.build({ - capabilities: ['Linodes'], - country: 'us', - id: 'ap-south', - label: 'US Location 2', - }), - ]; - - const result: RegionSelectOption[] = getRegionOptions({ - accountAvailabilityData, - currentCapability: 'Linodes', - regions: _regions, - }); - - const unavailableRegion = result.find( - (region) => region.value === 'ap-south' - ); - - expect(unavailableRegion?.disabledProps?.disabled).toBe(true); - }); - - it('should have its option disabled if `handleDisabledRegion` is passed', () => { - const result: RegionSelectOption[] = getRegionOptions({ - accountAvailabilityData, - currentCapability: 'Linodes', - handleDisabledRegion: (region) => ({ - ...region, - disabled: true, - }), - regions, - }); - - const unavailableRegion = result.find((region) => region.value === 'us-1'); - - expect(unavailableRegion?.disabledProps?.disabled).toBe(true); - }); -}); - -describe('getSelectedRegionById', () => { - it('should return the correct OptionType for a selected region', () => { - const selectedRegionId = 'us-1'; - - const result = getSelectedRegionById({ - accountAvailabilityData, - currentCapability: 'Linodes', - regions, - selectedRegionId, - }); - - // Expected result - const expected = { - data: { - country: 'us', - region: 'North America', - }, - label: 'US Location (us-1)', - site_type: 'core', - value: 'us-1', - }; - - expect(result).toEqual(expected); - }); - - it('should return undefined for an unknown region', () => { - const selectedRegionId = 'unknown'; - - const result = getSelectedRegionById({ - accountAvailabilityData, - currentCapability: 'Linodes', - regions, - selectedRegionId, - }); - - expect(result).toBeUndefined(); - }); }); describe('getRegionOptionAvailability', () => { @@ -293,63 +190,3 @@ describe('getRegionOptionAvailability', () => { }); }); -describe('getSelectedRegionsByIds', () => { - it('should return an array of RegionSelectOptions for the given selectedRegionIds', () => { - const selectedRegionIds = ['us-1', 'ca-1']; - - const result = getSelectedRegionsByIds({ - accountAvailabilityData, - currentCapability: 'Linodes', - regions, - selectedRegionIds, - }); - - const expected = [ - { - data: { - country: 'us', - region: 'North America', - }, - label: 'US Location (us-1)', - site_type: 'core', - value: 'us-1', - }, - { - data: { - country: 'ca', - region: 'North America', - }, - label: 'CA Location (ca-1)', - site_type: 'core', - value: 'ca-1', - }, - ]; - - expect(result).toEqual(expected); - }); - - it('should exclude regions that are not found in the regions array', () => { - const selectedRegionIds = ['us-1', 'non-existent-region']; - - const result = getSelectedRegionsByIds({ - accountAvailabilityData, - currentCapability: 'Linodes', - regions, - selectedRegionIds, - }); - - const expected = [ - { - data: { - country: 'us', - region: 'North America', - }, - label: 'US Location (us-1)', - site_type: 'core', - value: 'us-1', - }, - ]; - - expect(result).toEqual(expected); - }); -}); diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx index b9614c43937..1c417cb7d6d 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx @@ -1,20 +1,13 @@ import { - Capabilities, CONTINENT_CODE_TO_CONTINENT, + Capabilities, RegionSite, } from '@linode/api-v4'; -import { - getRegionCountryGroup, - getSelectedRegion, -} from 'src/utilities/formatRegion'; +import { getRegionCountryGroup } from 'src/utilities/formatRegion'; import type { GetRegionOptionAvailability, - GetRegionOptions, - GetSelectedRegionById, - GetSelectedRegionsByIdsArgs, - RegionSelectOption, SupportedDistributedRegionTypes, } from './RegionSelect.types'; import type { AccountAvailability, Region } from '@linode/api-v4'; @@ -22,130 +15,29 @@ import type { LinodeCreateType } from 'src/features/Linodes/LinodesCreate/types' const NORTH_AMERICA = CONTINENT_CODE_TO_CONTINENT.NA; -/** - * Returns an array of OptionType objects for use in the RegionSelect component. - * Handles the disabled state of each region based on the user's account availability or an optional custom handler. - * Regions are sorted alphabetically by region, with North America first. - * - * @returns An array of RegionSelectOption objects - */ -export const getRegionOptions = ({ - accountAvailabilityData, - currentCapability, - handleDisabledRegion, - regionFilter, - regions, -}: GetRegionOptions): RegionSelectOption[] => { - const filteredRegionsByCapability = currentCapability - ? regions.filter((region) => - region.capabilities.includes(currentCapability) - ) - : regions; - - const filteredRegionsByCapabilityAndSiteType = regionFilter - ? filteredRegionsByCapability.filter( - (region) => region.site_type === regionFilter - ) - : filteredRegionsByCapability; - - const isRegionUnavailable = (region: Region) => - isRegionOptionUnavailable({ - accountAvailabilityData, - currentCapability, - region, - }); - - return filteredRegionsByCapabilityAndSiteType - .map((region: Region) => { - const group = getRegionCountryGroup(region); - - // The region availability is the first check we run, regardless of the handleDisabledRegion function. - // This check always runs, and if the region is unavailable, the region will be disabled. - const disabledProps = isRegionUnavailable(region) - ? { - disabled: true, - reason: - 'This region is currently unavailable. For help, open a support ticket.', - tooltipWidth: 250, - } - : handleDisabledRegion?.(region)?.disabled - ? handleDisabledRegion(region) - : { - disabled: false, - }; - - return { - data: { - country: region.country, - region: group, - }, - disabledProps, - label: `${region.label} (${region.id})`, - site_type: region.site_type, - value: region.id, - }; - }) - .sort((region1, region2) => { - // North America group comes first - if ( - region1.data.region === NORTH_AMERICA && - region2.data.region !== NORTH_AMERICA - ) { - return -1; - } - if ( - region1.data.region !== NORTH_AMERICA && - region2.data.region === NORTH_AMERICA - ) { - return 1; - } - - // Rest of the regions are sorted alphabetically - if (region1.data.region < region2.data.region) { - return -1; - } - if (region1.data.region > region2.data.region) { - return 1; - } - - // Then we group by country - if (region1.data.country < region2.data.country) { - return 1; - } - if (region1.data.country > region2.data.country) { - return -1; - } - - // If regions are in the same group or country, sort alphabetically by label - if (region1.label < region2.label) { - return -1; - } - - return 1; - }); -}; - interface RegionSelectOptionsOptions { - regions: Region[]; currentCapability: Capabilities | undefined; regionFilter?: RegionSite; + regions: Region[]; } -export const getRegionOptionsv2 = ({ - regions, +export const getRegionOptions = ({ currentCapability, regionFilter, + regions, }: RegionSelectOptionsOptions) => { return regions .filter((region) => { - let keepRegion = true; - if (currentCapability) { - keepRegion = region.capabilities.includes(currentCapability); + if ( + currentCapability && + !region.capabilities.includes(currentCapability) + ) { + return false; } - if (regionFilter) { - keepRegion = region.site_type === regionFilter; + if (regionFilter && region.site_type !== regionFilter) { + return false; } - return keepRegion; + return true; }) .sort((region1, region2) => { const region1Group = getRegionCountryGroup(region1); @@ -187,34 +79,6 @@ export const getRegionOptionsv2 = ({ }); }; -/** - * Util to map a region ID to an OptionType object. - * - * @returns an RegionSelectOption object for the currently selected region. - */ -export const getSelectedRegionById = ({ - regions, - selectedRegionId, -}: GetSelectedRegionById): RegionSelectOption | undefined => { - const selectedRegion = getSelectedRegion(regions, selectedRegionId); - - if (!selectedRegion) { - return undefined; - } - - const group = getRegionCountryGroup(selectedRegion); - - return { - data: { - country: selectedRegion?.country, - region: group, - }, - label: `${selectedRegion.label} (${selectedRegion.id})`, - site_type: selectedRegion.site_type, - value: selectedRegion.id, - }; -}; - /** * Util to determine if a region is available to the user for a given capability. * @@ -243,29 +107,6 @@ export const isRegionOptionUnavailable = ({ return regionWithUnavailability.unavailable.includes(currentCapability); }; -/** - * This utility function takes an array of region IDs and returns an array of corresponding RegionSelectOption objects. - * - * @returns An array of RegionSelectOption objects corresponding to the selected region IDs. - */ -export const getSelectedRegionsByIds = ({ - accountAvailabilityData, - currentCapability, - regions, - selectedRegionIds, -}: GetSelectedRegionsByIdsArgs): RegionSelectOption[] => { - return selectedRegionIds - .map((selectedRegionId) => - getSelectedRegionById({ - accountAvailabilityData, - currentCapability, - regions, - selectedRegionId, - }) - ) - .filter((region): region is RegionSelectOption => !!region); -}; - /** * Util to determine whether a create type has support for distributed regions. * diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/AccessKeyRegions.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/AccessKeyRegions.tsx index 4a8a89be6c4..f145eaee71d 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/AccessKeyRegions.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/AccessKeyRegions.tsx @@ -4,7 +4,7 @@ import { RegionMultiSelect } from 'src/components/RegionSelect/RegionMultiSelect import { useRegionsQuery } from 'src/queries/regions/regions'; import { sortByString } from 'src/utilities/sort-by'; -import type { RegionSelectOption } from 'src/components/RegionSelect/RegionSelect.types'; +import type { Region } from '@linode/api-v4'; interface Props { disabled?: boolean; @@ -15,7 +15,7 @@ interface Props { selectedRegion: string[]; } -const sortRegionOptions = (a: RegionSelectOption, b: RegionSelectOption) => { +const sortRegionOptions = (a: Region, b: Region) => { return sortByString(a.label, b.label, 'asc'); }; @@ -29,9 +29,7 @@ export const AccessKeyRegions = (props: Props) => { return ( { - onChange(ids); - }} + onChange={onChange} currentCapability="Object Storage" disabled={disabled} errorText={errorText} diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx index 197dc7e5122..ecc69907885 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx @@ -8,11 +8,11 @@ import { RemovableSelectionsList, } from 'src/components/RemovableSelectionsList/RemovableSelectionsList'; -import type { RegionSelectOption } from 'src/components/RegionSelect/RegionSelect.types'; +import type { Region } from '@linode/api-v4'; interface SelectedRegionsProps { onRemove: (region: string) => void; - selectedRegions: RegionSelectOption[]; + selectedRegions: Region[]; } interface LabelComponentProps { diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index ab4e75fcf48..f0a21b20753 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -34,6 +34,7 @@ import { import type { PlacementGroupsCreateDrawerProps } from './types'; import type { CreatePlacementGroupPayload, Region } from '@linode/api-v4'; import type { FormikHelpers } from 'formik'; +import type { DisableRegionOption } from 'src/components/RegionSelect/RegionSelect.types'; export const PlacementGroupsCreateDrawer = ( props: PlacementGroupsCreateDrawerProps @@ -136,6 +137,34 @@ export const PlacementGroupsCreateDrawer = ( selectedRegion )}`; + const disabledRegions = regions?.reduce>( + (acc, region) => { + const isRegionAtCapacity = hasRegionReachedPlacementGroupCapacity({ + allPlacementGroups: allPlacementGroupsInRegion, + region, + }); + if (isRegionAtCapacity) { + acc[region.id] = { + reason: ( + <> + + You’ve reached the limit of placement groups you can create in + this region. + + + {MAXIMUM_NUMBER_OF_PLACEMENT_GROUPS_IN_REGION}{' '} + {getMaxPGsPerCustomer(region)} + + + ), + tooltipWidth: 300, + }; + } + return acc; + }, + {} + ); + return ( { - const isRegionAtCapacity = hasRegionReachedPlacementGroupCapacity( - { - allPlacementGroups: allPlacementGroupsInRegion, - region, - } - ); - - return { - disabled: isRegionAtCapacity, - reason: ( - <> - - You’ve reached the limit of placement groups you can - create in this region. - - - {MAXIMUM_NUMBER_OF_PLACEMENT_GROUPS_IN_REGION}{' '} - {getMaxPGsPerCustomer(region)} - - - ), - tooltipWidth: 300, - }; - }} handleSelection={(selection) => { handleRegionSelect(selection); }} currentCapability="Placement Group" + disabledRegions={disabledRegions} helperText={values.region && pgRegionLimitHelperText} regions={regions ?? []} selectedId={selectedRegionId ?? values.region} From 7ce262e4ad089d5972d1b6f76bed4b7255301f11 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 17:27:28 -0400 Subject: [PATCH 03/11] save progress --- .../src/components/RegionSelect/RegionMultiSelect.tsx | 2 +- .../components/RegionSelect/RegionSelect.stories.tsx | 4 ++-- .../src/components/RegionSelect/RegionSelect.test.tsx | 4 ++-- .../src/components/RegionSelect/RegionSelect.tsx | 10 +++++----- .../src/components/RegionSelect/RegionSelect.types.ts | 6 +++--- .../components/SelectRegionPanel/SelectRegionPanel.tsx | 10 +++++----- .../src/features/CloudPulse/shared/RegionSelect.tsx | 6 ++---- .../Databases/DatabaseCreate/DatabaseCreate.tsx | 6 ++---- packages/manager/src/features/Images/ImageUpload.tsx | 4 ++-- .../Kubernetes/CreateCluster/CreateCluster.tsx | 6 ++---- .../features/Linodes/MigrateLinode/ConfigureForm.tsx | 4 ++-- .../ObjectStorage/BucketLanding/BucketRegions.tsx | 4 ++-- .../ObjectStorage/BucketLanding/ClusterSelect.tsx | 4 ++-- .../PlacementGroups/PlacementGroupsCreateDrawer.tsx | 6 ++---- .../VPCCreate/FormComponents/VPCTopSectionContent.tsx | 4 ++-- .../src/features/VPCs/VPCLanding/VPCEditDrawer.tsx | 4 ++-- packages/manager/src/features/Volumes/VolumeCreate.tsx | 4 ++-- 17 files changed, 40 insertions(+), 48 deletions(-) diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx index c502d1b15c7..37808a5eb9d 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx @@ -106,7 +106,6 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { <> Boolean(disabledRegions[option.id])} onChange={(_, selectedOptions) => onChange(selectedOptions.map((region) => region.id)) } @@ -157,6 +156,7 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { disableClearable={!isClearable} disabled={disabled} errorText={errorText} + getOptionDisabled={(option) => Boolean(disabledRegions[option.id])} groupBy={(option) => getRegionCountryGroup(option)} label={label ?? 'Regions'} loading={accountAvailabilityLoading} diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx index fc7b5707a6d..14d5a40bb8b 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx @@ -17,7 +17,7 @@ export const Default: StoryObj = { setOpen(false)} + onChange={() => setOpen(false)} open={open} /> @@ -38,7 +38,7 @@ const meta: Meta = { label: 'Region', regions, required: true, - selectedId: regions[2].id, + value: regions[2].id, }, component: RegionSelect, title: 'Components/Selects/Region Select', diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx index 13fe9614d57..dbab54f7316 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx @@ -15,13 +15,13 @@ describe('RegionSelect', () => { currentCapability: 'Linodes', disabled: false, errorText: '', - handleSelection: vi.fn(), + onChange: vi.fn(), helperText: '', isClearable: false, label: '', regions, required: false, - selectedId: '', + value: '', tooltipText: '', width: 100, }; diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.tsx index a0ae12a6ad5..1a1335fa59b 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.tsx @@ -41,16 +41,16 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { disabled, disabledRegions: disabledRegionsFromProps, errorText, - handleSelection, helperText, isClearable, label, + onChange, regionFilter, regions, required, - selectedId, showDistributedRegionIconHelperText, tooltipText, + value, width, } = props; @@ -65,7 +65,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { regions, }); - const selectedRegion = regionOptions.find((r) => r.id === selectedId) ?? null; + const selectedRegion = regionOptions.find((r) => r.id === value) ?? null; const disabledRegions = regionOptions.reduce< Record @@ -91,8 +91,8 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { return ( { - handleSelection(selectedOption!.id); + onChange={(_, region) => { + onChange(region!); }} renderOption={(props, option) => { return ( diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts index 45b3fcf1a3b..726e10c597b 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts +++ b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts @@ -16,7 +16,7 @@ export interface DisableRegionOption { export interface RegionSelectProps extends Omit< EnhancedAutocompleteProps, - 'label' | 'onChange' | 'options' + 'label' | 'onChange' | 'options' | 'value' > { /** * The specified capability to filter the regions on. Any region that does not have the `currentCapability` will not appear in the RegionSelect dropdown. @@ -27,16 +27,16 @@ export interface RegionSelectProps */ currentCapability: Capabilities | undefined; disabledRegions?: Record; - handleSelection: (id: string) => void; helperText?: string; isClearable?: boolean; label?: string; + onChange: (region: Region) => void; regionFilter?: RegionSite; regions: Region[]; required?: boolean; - selectedId: null | string; showDistributedRegionIconHelperText?: boolean; tooltipText?: string; + value: null | string; width?: number; } diff --git a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx index 1fbe47fa423..bcc436048e3 100644 --- a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx +++ b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx @@ -147,17 +147,17 @@ export const SelectRegionPanel = (props: SelectRegionPanelProps) => { ) : null} handleSelection(region.id)} regionFilter={hideDistributedRegions ? 'core' : undefined} regions={regions ?? []} - selectedId={selectedId || null} - showDistributedRegionIconHelperText={ - showDistributedRegionIconHelperText - } + value={selectedId || null} {...RegionSelectProps} /> {showClonePriceWarning && ( diff --git a/packages/manager/src/features/CloudPulse/shared/RegionSelect.tsx b/packages/manager/src/features/CloudPulse/shared/RegionSelect.tsx index b60ba23564f..a3a2bd7224d 100644 --- a/packages/manager/src/features/CloudPulse/shared/RegionSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/RegionSelect.tsx @@ -20,16 +20,14 @@ export const CloudPulseRegionSelect = React.memo( return ( { - setRegion(value); - }} currentCapability={undefined} fullWidth isClearable={false} label="" noMarginTop + onChange={(region) => setRegion(region.id)} regions={regions ? regions : []} - selectedId={null} + value={null} /> ); } diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 15b7feec0b2..3e7184584c3 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -496,13 +496,11 @@ const DatabaseCreate = () => { - setFieldValue('region', selected) - } currentCapability="Managed Databases" errorText={errors.region} + onChange={(region) => setFieldValue('region', region.id)} regions={regionsData} - selectedId={values.region} + value={values.region} /> diff --git a/packages/manager/src/features/Images/ImageUpload.tsx b/packages/manager/src/features/Images/ImageUpload.tsx index 704a0bce61a..7c1ddf4f5f2 100644 --- a/packages/manager/src/features/Images/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImageUpload.tsx @@ -257,12 +257,12 @@ export const ImageUpload = () => { }} currentCapability={undefined} errorText={fieldState.error?.message} - handleSelection={field.onChange} + onChange={field.onChange} helperText="For fastest initial upload, select the region that is geographically closest to you. Once uploaded, you will be able to deploy the image to other regions." label="Region" regionFilter="core" // Images service will not be supported for Gecko Beta regions={regions ?? []} - selectedId={field.value ?? null} + value={field.value ?? null} /> )} control={form.control} diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index aaeb389f057..9124efd264b 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -220,17 +220,15 @@ export const CreateCluster = () => { - setSelectedRegionID(regionID) - } textFieldProps={{ helperText: , helperTextPosition: 'top', }} currentCapability="Kubernetes" errorText={errorMap.region} + onChange={(region) => setSelectedRegionID(region.id)} regions={regionsData} - selectedId={selectedId} + value={selectedId} /> diff --git a/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx b/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx index a8f091afc80..a28fcd731ef 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx @@ -191,9 +191,9 @@ export const ConfigureForm = React.memo((props: Props) => { }} currentCapability="Linodes" errorText={errorText} - handleSelection={handleSelectRegion} label="New Region" - selectedId={selectedRegion} + onChange={(region) => handleSelectRegion(region.id)} + value={selectedRegion} /> {shouldDisplayPriceComparison && selectedRegion && ( { currentCapability="Object Storage" disabled={disabled} errorText={errorText} - handleSelection={(id) => onChange(id)} isClearable={false} label="Region" onBlur={onBlur} + onChange={(region) => onChange(region.id)} placeholder="Select a Region" regions={regions ?? []} required={required} - selectedId={selectedRegion} + value={selectedRegion} /> ); }; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx index 5de99f11411..2850e1abb92 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx @@ -48,14 +48,14 @@ export const ClusterSelect: React.FC = (props) => { data-qa-select-cluster disabled={disabled} errorText={errorText} - handleSelection={(id) => onChange(id)} isClearable={false} label="Region" onBlur={onBlur} + onChange={(region) => onChange(region.id)} placeholder="Select a Region" regions={regionOptions ?? []} required={required} - selectedId={selectedCluster} + value={selectedCluster} /> ); }; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index f0a21b20753..45545b9f01f 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -214,15 +214,13 @@ export const PlacementGroupsCreateDrawer = ( disabled={ Boolean(selectedRegionId) || disabledPlacementGroupCreateButton } - handleSelection={(selection) => { - handleRegionSelect(selection); - }} currentCapability="Placement Group" disabledRegions={disabledRegions} helperText={values.region && pgRegionLimitHelperText} + onChange={(region) => handleRegionSelect(region.id)} regions={regions ?? []} - selectedId={selectedRegionId ?? values.region} tooltipText="Only Linode data center regions that support placement groups are listed." + value={selectedRegionId ?? values.region} /> )} { currentCapability="VPCs" disabled={isDrawer ? true : disabled} errorText={errors.region} - handleSelection={(region: string) => onChangeField('region', region)} isClearable + onChange={(region) => onChangeField('region', region.id)} regions={regions} - selectedId={values.region} + value={values.region} /> ) => diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx index 83de5473f36..cf8e8cf148b 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx @@ -120,10 +120,10 @@ export const VPCEditDrawer = (props: Props) => { currentCapability="VPCs" disabled // the Region field will not be editable during beta errorText={(regionsError && regionsError[0].reason) || undefined} - handleSelection={() => null} + onChange={() => null} helperText={REGION_HELPER_TEXT} regions={regionsData} - selectedId={vpc?.region ?? null} + value={vpc?.region ?? null} /> )} { /> { + onChange={(value) => { setFieldValue('region', value); setFieldValue('linode_id', null); }} @@ -314,7 +314,7 @@ export const VolumeCreate = () => { label="Region" onBlur={handleBlur} regions={regions ?? []} - selectedId={values.region} + value={values.region} width={400} /> {renderSelectTooltip( From 6878a460bd32a89ff9ea188794155baa9d408c83 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 17:43:35 -0400 Subject: [PATCH 04/11] fix issues with multi-select --- .../RegionSelect/RegionMultiSelect.tsx | 18 ++++++++++++------ packages/manager/src/utilities/formatRegion.ts | 15 --------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx index 37808a5eb9d..0dd6c1e4987 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx @@ -9,6 +9,7 @@ import { Flag } from 'src/components/Flag'; import { useAllAccountAvailabilitiesQuery } from 'src/queries/account/availability'; import { getRegionCountryGroup } from 'src/utilities/formatRegion'; +import { StyledListItem } from '../Autocomplete/Autocomplete.styles'; import { RegionOption } from './RegionOption'; import { StyledAutocompleteContainer, @@ -23,7 +24,6 @@ import type { DisableRegionOption, RegionMultiSelectProps, } from './RegionSelect.types'; -import { StyledListItem } from '../Autocomplete/Autocomplete.styles'; interface LabelComponentProps { region: Region; @@ -57,10 +57,10 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { currentCapability, disabled, errorText, - onChange, helperText, isClearable, label, + onChange, placeholder, regions, required, @@ -106,23 +106,30 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { <> { + if (!option.site_type) { + // Render empty group for "Select All / Deselect All" + return ''; + } + return getRegionCountryGroup(option); + }} onChange={(_, selectedOptions) => onChange(selectedOptions.map((region) => region.id)) } renderOption={(props, option, { selected }) => { if (!option.site_type) { - // Render options like "Select All / Deselect All " + // Render options like "Select All / Deselect All" return {option.label}; } // Render regular options return ( ); }} @@ -157,7 +164,6 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { disabled={disabled} errorText={errorText} getOptionDisabled={(option) => Boolean(disabledRegions[option.id])} - groupBy={(option) => getRegionCountryGroup(option)} label={label ?? 'Regions'} loading={accountAvailabilityLoading} multiple diff --git a/packages/manager/src/utilities/formatRegion.ts b/packages/manager/src/utilities/formatRegion.ts index b3f585da1a1..3b8f92c95b6 100644 --- a/packages/manager/src/utilities/formatRegion.ts +++ b/packages/manager/src/utilities/formatRegion.ts @@ -32,21 +32,6 @@ export const getRegionCountryGroup = (region: Region | undefined) => { : 'Other'; }; -export const getRegionCountryGroupByCountryCode = (countryCode: string | undefined) => { - if (!countryCode) { - return 'Other'; - } - - const continentCode = - COUNTRY_CODE_TO_CONTINENT_CODE[ - countryCode.toUpperCase() as Uppercase - ]; - - return continentCode - ? CONTINENT_CODE_TO_CONTINENT[continentCode] ?? 'Other' - : 'Other'; -} - export const getSelectedRegion = ( regions: Region[], selectedRegionId: string From 8e8a99233e8654d0ab943e385046ef114cda171c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 18:02:30 -0400 Subject: [PATCH 05/11] more work --- .../RegionSelect/RegionMultiSelect.test.tsx | 9 +- .../RegionSelect/RegionSelect.utils.test.tsx | 241 ++++++++++-------- 2 files changed, 137 insertions(+), 113 deletions(-) diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx index 12a07021719..654040f540d 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx @@ -54,7 +54,8 @@ describe('RegionMultiSelect', () => { screen.getByRole('combobox', { name: 'Regions' }); }); - it('should be able to select all the regions correctly', () => { + // Re-enable if we allow RegionMultiSelect to be uncontrolled + it.skip('should be able to select all the regions correctly', () => { renderWithTheme( { ).toHaveAttribute('aria-selected', 'true'); }); - it('should be able to deselect all the regions', () => { + // Re-enable if we allow RegionMultiSelect to be uncontrolled + it.skip('should be able to deselect all the regions', () => { renderWithTheme( { ).toHaveAttribute('aria-selected', 'false'); }); - it('should render selected regions correctly', () => { + // Re-enable if we allow RegionMultiSelect to be uncontrolled + it.skip('should render selected regions correctly', () => { renderWithTheme( ( diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx index 3c43ac96592..22ba45e2dc8 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx @@ -5,165 +5,187 @@ import { isRegionOptionUnavailable, } from './RegionSelect.utils'; -import type { Region } from '@linode/api-v4'; - -const accountAvailabilityData = [ - accountAvailabilityFactory.build({ - region: 'ap-south', - unavailable: ['Linodes'], - }), -]; - -const regions: Region[] = [ - regionFactory.build({ - capabilities: ['Linodes'], - country: 'us', - id: 'us-1', - label: 'US Location', - }), - regionFactory.build({ - capabilities: ['Linodes'], - country: 'ca', - id: 'ca-1', - label: 'CA Location', - }), - regionFactory.build({ - capabilities: ['Linodes'], - country: 'jp', - id: 'jp-1', - label: 'JP Location', - }), -]; - -const distributedRegions = [ - ...regions, - regionFactory.build({ - capabilities: ['Linodes'], - country: 'us', - id: 'us-den-10', - label: 'Gecko Distributed Region Test', - site_type: 'distributed', - }), - regionFactory.build({ - capabilities: ['Linodes'], - country: 'us', - id: 'us-den-11', - label: 'Gecko Distributed Region Test 2', - site_type: 'distributed', - }), -]; - -const expectedRegions: Region[] = [ - regionFactory.build({ - label: 'US Location (us-1)', - site_type: 'core', - id: 'us-1', - }), - regionFactory.build({ - label: 'CA Location (ca-1)', - site_type: 'core', - id: 'ca-1', - }), - regionFactory.build({ - label: 'JP Location (jp-1)', - site_type: 'core', - id: 'jp-1', - }), -]; - -const expectedDistributedRegions = [ - { - data: { country: 'us', region: 'North America' }, - disabledProps: { - disabled: false, - }, - label: 'Gecko Distributed Region Test (us-den-10)', - site_type: 'distributed', - value: 'us-den-10', - }, - { - data: { country: 'us', region: 'North America' }, - disabledProps: { - disabled: false, - }, - label: 'Gecko Distributed Region Test 2 (us-den-11)', - site_type: 'distributed', - value: 'us-den-11', - }, -]; - describe('getRegionOptions', () => { it('should return an empty array if no regions are provided', () => { - const regions: Region[] = []; const result = getRegionOptions({ currentCapability: 'Linodes', - regions, + regions: [], }); expect(result).toEqual([]); }); - it('should return a sorted array of OptionType objects with North America first', () => { + it('should return a sorted array of regions with North America first', () => { + const regions = [ + regionFactory.build({ + capabilities: ['Linodes'], + country: 'jp', + id: 'jp-1', + label: 'JP Location', + }), + regionFactory.build({ + capabilities: ['Linodes'], + country: 'us', + id: 'us-1', + label: 'US Location', + }), + regionFactory.build({ + capabilities: ['Linodes'], + country: 'ca', + id: 'ca-1', + label: 'CA Location', + }), + ]; + const result = getRegionOptions({ currentCapability: 'Linodes', regions, }); - expect(result).toEqual(expectedRegions); + expect(result).toEqual([ + regionFactory.build({ + capabilities: ['Linodes'], + country: 'us', + id: 'us-1', + label: 'US Location', + }), + regionFactory.build({ + capabilities: ['Linodes'], + country: 'ca', + id: 'ca-1', + label: 'CA Location', + }), + regionFactory.build({ + capabilities: ['Linodes'], + country: 'jp', + id: 'jp-1', + label: 'JP Location', + }), + ]); }); it('should filter out regions that do not have the currentCapability if currentCapability is provided', () => { - const regionsToFilter: Region[] = [ - ...regions, + const distributedRegions = [ regionFactory.build({ - capabilities: ['Object Storage'], - country: 'pe', - id: 'peru-1', - label: 'Peru Location', + capabilities: ['Linodes'], + country: 'us', + id: 'us-den-10', + label: 'Gecko Distributed Region Test', + site_type: 'distributed', + }), + regionFactory.build({ + capabilities: [], + country: 'us', + id: 'us-den-11', + label: 'Gecko Distributed Region Test 2', + site_type: 'distributed', }), ]; const result = getRegionOptions({ currentCapability: 'Linodes', - regions: regionsToFilter, + regions: distributedRegions, }); - expect(result).toEqual(expectedRegions); + expect(result).toEqual([ + regionFactory.build({ + capabilities: ['Linodes'], + country: 'us', + id: 'us-den-10', + label: 'Gecko Distributed Region Test', + site_type: 'distributed', + }), + ]); }); it('should filter out distributed regions if regionFilter is core', () => { + const regions = [ + regionFactory.build({ + id: 'us-1', + label: 'US Site 1', + site_type: 'distributed', + }), + regionFactory.build({ + id: 'us-2', + label: 'US Site 2', + site_type: 'core', + }), + ]; + const result = getRegionOptions({ - currentCapability: 'Linodes', + currentCapability: undefined, regionFilter: 'core', - regions: distributedRegions, + regions, }); - expect(result).toEqual(expectedRegions); + expect(result).toEqual([ + regionFactory.build({ + id: 'us-2', + label: 'US Site 2', + site_type: 'core', + }), + ]); }); it('should filter out core regions if regionFilter is "distributed"', () => { + const regions = [ + regionFactory.build({ + id: 'us-1', + label: 'US Site 1', + site_type: 'distributed', + }), + regionFactory.build({ + id: 'us-2', + label: 'US Site 2', + site_type: 'core', + }), + ]; + const result = getRegionOptions({ - currentCapability: 'Linodes', + currentCapability: undefined, regionFilter: 'distributed', - regions: distributedRegions, + regions, }); - expect(result).toEqual(expectedDistributedRegions); + expect(result).toEqual([ + regionFactory.build({ + id: 'us-1', + label: 'US Site 1', + site_type: 'distributed', + }), + ]); }); it('should not filter out any regions if regionFilter is undefined', () => { - const regions = [...expectedDistributedRegions, ...expectedRegions]; - + const regions = [ + regionFactory.build({ + id: 'us-1', + label: 'US Site 1', + site_type: 'distributed', + }), + regionFactory.build({ + id: 'us-2', + label: 'US Site 2', + site_type: 'core', + }), + ]; const result = getRegionOptions({ - currentCapability: 'Linodes', + currentCapability: undefined, regionFilter: undefined, - regions: distributedRegions, + regions, }); expect(result).toEqual(regions); }); }); +const accountAvailabilityData = [ + accountAvailabilityFactory.build({ + region: 'ap-south', + unavailable: ['Linodes'], + }), +]; + describe('getRegionOptionAvailability', () => { it('should return true if the region is not available', () => { const result = isRegionOptionUnavailable({ @@ -189,4 +211,3 @@ describe('getRegionOptionAvailability', () => { expect(result).toBe(false); }); }); - From e7498a7319d6215971052e67051cc385cc62ddf6 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 18:26:27 -0400 Subject: [PATCH 06/11] clean up more --- .../components/RegionSelect/RegionOption.tsx | 5 ++- .../components/RegionSelect/RegionSelect.tsx | 45 +++++++------------ .../RegionSelect/RegionSelect.types.ts | 41 +++++++---------- 3 files changed, 36 insertions(+), 55 deletions(-) diff --git a/packages/manager/src/components/RegionSelect/RegionOption.tsx b/packages/manager/src/components/RegionSelect/RegionOption.tsx index 45cae51ff39..76d3e7a873c 100644 --- a/packages/manager/src/components/RegionSelect/RegionOption.tsx +++ b/packages/manager/src/components/RegionSelect/RegionOption.tsx @@ -20,7 +20,6 @@ import type { ListItemComponentsPropsOverrides } from '@mui/material/ListItem'; interface Props { disabledOptions?: DisableRegionOption; - displayDistributedRegionIcon?: boolean; props: React.HTMLAttributes; region: Region; selected?: boolean; @@ -28,7 +27,6 @@ interface Props { export const RegionOption = ({ disabledOptions, - displayDistributedRegionIcon, props, region, selected, @@ -37,6 +35,9 @@ export const RegionOption = ({ const isRegionDisabled = Boolean(disabledOptions); const isRegionDisabledReason = disabledOptions?.reason; + const displayDistributedRegionIcon = + region.site_type === 'edge' || region.site_type === 'distributed'; + return ( { return ( { - onChange(region!); - }} - renderOption={(props, option) => { - return ( - - ); - }} + renderOption={(props, region) => ( + + )} sx={(theme) => ({ [theme.breakpoints.up('md')]: { width: '416px', @@ -117,16 +106,15 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { textFieldProps={{ ...props.textFieldProps, InputProps: { - endAdornment: regionFilter !== 'core' && - (selectedRegion?.site_type === 'distributed' || - selectedRegion?.site_type === 'edge') && ( - } - status="other" - sxTooltipIcon={sxDistributedRegionIcon} - text="This region is a distributed region." - /> - ), + endAdornment: (selectedRegion?.site_type === 'distributed' || + selectedRegion?.site_type === 'edge') && ( + } + status="other" + sxTooltipIcon={sxDistributedRegionIcon} + text="This region is a distributed region." + /> + ), required, startAdornment: selectedRegion && ( @@ -149,6 +137,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { loading={accountAvailabilityLoading} loadingText="Loading regions..." noOptionsText="No results" + onChange={(_, region) => onChange(region!)} options={regionOptions} placeholder="Select a Region" value={selectedRegion} diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts index 726e10c597b..f08ebb7bdc5 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts +++ b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts @@ -9,7 +9,15 @@ import type { import type { EnhancedAutocompleteProps } from 'src/components/Autocomplete/Autocomplete'; export interface DisableRegionOption { + /** + * The reason the region option is disabled. + * This is shown to the user as a tooltip. + */ reason: JSX.Element | string; + /** + * An optional minWith applied to the tooltip + * @default 215 + */ tooltipWidth?: number; } @@ -26,6 +34,9 @@ export interface RegionSelectProps * See `ImageUpload.tsx` for an example of a RegionSelect with an undefined `currentCapability` - there is no capability associated with Images yet. */ currentCapability: Capabilities | undefined; + /** + * A key/value object for disabling regions by their ID. + */ disabledRegions?: Record; helperText?: string; isClearable?: boolean; @@ -36,6 +47,9 @@ export interface RegionSelectProps required?: boolean; showDistributedRegionIconHelperText?: boolean; tooltipText?: string; + /** + * The ID of the selected region. + */ value: null | string; width?: number; } @@ -50,10 +64,10 @@ export interface RegionMultiSelectProps selectedRegions: Region[]; }>; currentCapability: Capabilities | undefined; - onChange: (ids: string[]) => void; helperText?: string; isClearable?: boolean; label?: string; + onChange: (ids: string[]) => void; regions: Region[]; required?: boolean; selectedIds: string[]; @@ -62,33 +76,10 @@ export interface RegionMultiSelectProps width?: number; } -export interface RegionOptionAvailability { +export interface GetRegionOptionAvailability { accountAvailabilityData: AccountAvailability[] | undefined; currentCapability: Capabilities | undefined; - handleDisabledRegion?: ( - region: Region - ) => DisableRegionOption; -} - -export interface GetRegionOptions extends RegionOptionAvailability { - regionFilter?: RegionSite; - regions: Region[]; -} - -export interface GetSelectedRegionById extends RegionOptionAvailability { - regions: Region[]; - selectedRegionId: string; -} - -export interface GetRegionOptionAvailability extends RegionOptionAvailability { region: Region; } -export interface GetSelectedRegionsByIdsArgs { - accountAvailabilityData: AccountAvailability[] | undefined; - currentCapability: Capabilities | undefined; - regions: Region[]; - selectedRegionIds: string[]; -} - export type SupportedDistributedRegionTypes = 'Distributions' | 'StackScripts'; From 408328612485a0128bc9739dbf437506460353c2 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 18:31:48 -0400 Subject: [PATCH 07/11] fix key error and fix parity with prod --- packages/manager/src/components/RegionSelect/RegionOption.tsx | 1 - packages/manager/src/components/RegionSelect/RegionSelect.tsx | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/components/RegionSelect/RegionOption.tsx b/packages/manager/src/components/RegionSelect/RegionOption.tsx index 76d3e7a873c..d5d87778acd 100644 --- a/packages/manager/src/components/RegionSelect/RegionOption.tsx +++ b/packages/manager/src/components/RegionSelect/RegionOption.tsx @@ -56,7 +56,6 @@ export const RegionOption = ({ enterDelay={200} enterNextDelay={200} enterTouchDelay={200} - key={region.id} > { renderOption={(props, region) => ( @@ -131,6 +132,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { disabled={disabled} errorText={errorText} getOptionDisabled={(option) => Boolean(disabledRegions[option.id])} + getOptionLabel={(region) => `${region.label} (${region.id})`} groupBy={(option) => getRegionCountryGroup(option)} helperText={helperText} label={label ?? 'Region'} From e2f1ff2ac61805bc0684d8f1cfbbb61767b13898 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 22:34:32 -0400 Subject: [PATCH 08/11] fix volume form --- packages/manager/src/features/Volumes/VolumeCreate.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Volumes/VolumeCreate.tsx b/packages/manager/src/features/Volumes/VolumeCreate.tsx index 85c8ed3c622..149e32e8931 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate.tsx @@ -303,8 +303,8 @@ export const VolumeCreate = () => { /> { - setFieldValue('region', value); + onChange={(region) => { + setFieldValue('region', region?.id ?? null); setFieldValue('linode_id', null); }} currentCapability="Block Storage" From 3b753411ce92f4ce9998b85b0310b17f66e474f9 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Jun 2024 23:29:33 -0400 Subject: [PATCH 09/11] fix typesafety with clearable --- .../RegionSelect/RegionSelect.stories.tsx | 7 +------ .../RegionSelect/RegionSelect.test.tsx | 1 - .../components/RegionSelect/RegionSelect.tsx | 19 ++++++++++++------- .../RegionSelect/RegionSelect.types.ts | 11 +++++------ .../SelectRegionPanel/SelectRegionPanel.tsx | 5 +++-- .../CloudPulse/shared/RegionSelect.tsx | 4 ++-- .../DatabaseCreate/DatabaseCreate.tsx | 3 ++- .../src/features/Images/ImageUpload.tsx | 3 ++- .../CreateCluster/CreateCluster.tsx | 3 ++- .../Linodes/MigrateLinode/ConfigureForm.tsx | 3 ++- .../BucketLanding/BucketRegions.tsx | 4 ++-- .../BucketLanding/ClusterSelect.tsx | 4 ++-- .../PlacementGroupsCreateDrawer.tsx | 3 ++- .../FormComponents/VPCTopSectionContent.tsx | 3 +-- .../src/features/Volumes/VolumeCreate.tsx | 3 +-- 15 files changed, 39 insertions(+), 37 deletions(-) diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx index 14d5a40bb8b..66e3e5f9ef9 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx @@ -15,11 +15,7 @@ export const Default: StoryObj = { return ( - setOpen(false)} - open={open} - /> + setOpen(false)} open={open} /> ); }; @@ -34,7 +30,6 @@ const meta: Meta = { disabled: false, errorText: '', helperText: '', - isClearable: false, label: 'Region', regions, required: true, diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx index dbab54f7316..bd97e2a64a1 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx @@ -17,7 +17,6 @@ describe('RegionSelect', () => { errorText: '', onChange: vi.fn(), helperText: '', - isClearable: false, label: '', regions, required: false, diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.tsx index f7c87e9d715..6c1fbef0d0c 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.tsx @@ -25,6 +25,7 @@ import type { DisableRegionOption, RegionSelectProps, } from './RegionSelect.types'; +import type { Region } from '@linode/api-v4'; /** * A specific select for regions. @@ -35,14 +36,18 @@ import type { * * We do not display the selected check mark for single selects. */ -export const RegionSelect = React.memo((props: RegionSelectProps) => { +export const RegionSelect = < + DisableClearable extends boolean | undefined = undefined +>( + props: RegionSelectProps +) => { const { currentCapability, + disableClearable, disabled, disabledRegions: disabledRegionsFromProps, errorText, helperText, - isClearable, label, onChange, regionFilter, @@ -90,7 +95,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { return ( - renderOption={(props, region) => ( { autoHighlight clearOnBlur data-testid="region-select" - disableClearable={!isClearable} + disableClearable={disableClearable} disabled={disabled} errorText={errorText} getOptionDisabled={(option) => Boolean(disabledRegions[option.id])} @@ -139,10 +144,10 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { loading={accountAvailabilityLoading} loadingText="Loading regions..." noOptionsText="No results" - onChange={(_, region) => onChange(region!)} + onChange={onChange} options={regionOptions} placeholder="Select a Region" - value={selectedRegion} + value={selectedRegion as Region} /> {showDistributedRegionIconHelperText && ( // @TODO Gecko Beta: Add docs link @@ -165,4 +170,4 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { )} ); -}); +}; diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts index f08ebb7bdc5..d2dfbe23f96 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts +++ b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts @@ -21,10 +21,11 @@ export interface DisableRegionOption { tooltipWidth?: number; } -export interface RegionSelectProps - extends Omit< - EnhancedAutocompleteProps, - 'label' | 'onChange' | 'options' | 'value' +export interface RegionSelectProps< + DisableClearable extends boolean | undefined = undefined +> extends Omit< + EnhancedAutocompleteProps, + 'label' | 'options' | 'value' > { /** * The specified capability to filter the regions on. Any region that does not have the `currentCapability` will not appear in the RegionSelect dropdown. @@ -39,9 +40,7 @@ export interface RegionSelectProps */ disabledRegions?: Record; helperText?: string; - isClearable?: boolean; label?: string; - onChange: (region: Region) => void; regionFilter?: RegionSite; regions: Region[]; required?: boolean; diff --git a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx index bcc436048e3..3d230d03b77 100644 --- a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx +++ b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx @@ -30,7 +30,7 @@ import { RegionSelectProps } from '../RegionSelect/RegionSelect.types'; import type { LinodeCreateType } from 'src/features/Linodes/LinodesCreate/types'; interface SelectRegionPanelProps { - RegionSelectProps?: Partial; + RegionSelectProps?: Partial>; currentCapability: Capabilities; disabled?: boolean; error?: string; @@ -151,10 +151,11 @@ export const SelectRegionPanel = (props: SelectRegionPanelProps) => { showDistributedRegionIconHelperText } currentCapability={currentCapability} + disableClearable disabled={disabled} errorText={error} helperText={helperText} - onChange={(region) => handleSelection(region.id)} + onChange={(e, region) => handleSelection(region.id)} regionFilter={hideDistributedRegions ? 'core' : undefined} regions={regions ?? []} value={selectedId || null} diff --git a/packages/manager/src/features/CloudPulse/shared/RegionSelect.tsx b/packages/manager/src/features/CloudPulse/shared/RegionSelect.tsx index a3a2bd7224d..176f4bc37a8 100644 --- a/packages/manager/src/features/CloudPulse/shared/RegionSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/RegionSelect.tsx @@ -21,11 +21,11 @@ export const CloudPulseRegionSelect = React.memo( return ( setRegion(region.id)} + onChange={(e, region) => setRegion(region.id)} regions={regions ? regions : []} value={null} /> diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 3e7184584c3..36a598f3b4e 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -497,8 +497,9 @@ const DatabaseCreate = () => { setFieldValue('region', region.id)} + onChange={(e, region) => setFieldValue('region', region.id)} regions={regionsData} value={values.region} /> diff --git a/packages/manager/src/features/Images/ImageUpload.tsx b/packages/manager/src/features/Images/ImageUpload.tsx index 7c1ddf4f5f2..3a329828ffd 100644 --- a/packages/manager/src/features/Images/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImageUpload.tsx @@ -256,10 +256,11 @@ export const ImageUpload = () => { onBlur: field.onBlur, }} currentCapability={undefined} + disableClearable errorText={fieldState.error?.message} - onChange={field.onChange} helperText="For fastest initial upload, select the region that is geographically closest to you. Once uploaded, you will be able to deploy the image to other regions." label="Region" + onChange={(e, region) => field.onChange(region.id)} regionFilter="core" // Images service will not be supported for Gecko Beta regions={regions ?? []} value={field.value ?? null} diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 9124efd264b..7c38f687f8e 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -225,8 +225,9 @@ export const CreateCluster = () => { helperTextPosition: 'top', }} currentCapability="Kubernetes" + disableClearable errorText={errorMap.region} - onChange={(region) => setSelectedRegionID(region.id)} + onChange={(e, region) => setSelectedRegionID(region.id)} regions={regionsData} value={selectedId} /> diff --git a/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx b/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx index a28fcd731ef..b10784efb62 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.tsx @@ -190,9 +190,10 @@ export const ConfigureForm = React.memo((props: Props) => { helperText, }} currentCapability="Linodes" + disableClearable errorText={errorText} label="New Region" - onChange={(region) => handleSelectRegion(region.id)} + onChange={(e, region) => handleSelectRegion(region.id)} value={selectedRegion} /> {shouldDisplayPriceComparison && selectedRegion && ( diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.tsx index dff749bd710..a4fbb9b9152 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.tsx @@ -23,12 +23,12 @@ export const BucketRegions = (props: Props) => { return ( onChange(region.id)} + onChange={(e, region) => onChange(region.id)} placeholder="Select a Region" regions={regions ?? []} required={required} diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx index 2850e1abb92..21d431ff761 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx @@ -46,12 +46,12 @@ export const ClusterSelect: React.FC = (props) => { onChange(region.id)} + onChange={(e, region) => onChange(region.id)} placeholder="Select a Region" regions={regionOptions ?? []} required={required} diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 45545b9f01f..cfaef030495 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -215,9 +215,10 @@ export const PlacementGroupsCreateDrawer = ( Boolean(selectedRegionId) || disabledPlacementGroupCreateButton } currentCapability="Placement Group" + disableClearable disabledRegions={disabledRegions} helperText={values.region && pgRegionLimitHelperText} - onChange={(region) => handleRegionSelect(region.id)} + onChange={(e, region) => handleRegionSelect(region.id)} regions={regions ?? []} tooltipText="Only Linode data center regions that support placement groups are listed." value={selectedRegionId ?? values.region} diff --git a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx index 161757eb314..11bd0655a1f 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx @@ -58,8 +58,7 @@ export const VPCTopSectionContent = (props: Props) => { currentCapability="VPCs" disabled={isDrawer ? true : disabled} errorText={errors.region} - isClearable - onChange={(region) => onChangeField('region', region.id)} + onChange={(e, region) => onChangeField('region', region?.id ?? '')} regions={regions} value={values.region} /> diff --git a/packages/manager/src/features/Volumes/VolumeCreate.tsx b/packages/manager/src/features/Volumes/VolumeCreate.tsx index 149e32e8931..d1cca18ad0d 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate.tsx @@ -303,14 +303,13 @@ export const VolumeCreate = () => { /> { + onChange={(e, region) => { setFieldValue('region', region?.id ?? null); setFieldValue('linode_id', null); }} currentCapability="Block Storage" disabled={doesNotHavePermission} errorText={touched.region ? errors.region : undefined} - isClearable label="Region" onBlur={handleBlur} regions={regions ?? []} From f6d211d327f8f746259beaa5fe623cac011b3617 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 7 Jun 2024 12:03:31 -0400 Subject: [PATCH 10/11] fix storybook --- .../src/components/RegionSelect/RegionSelect.stories.tsx | 9 ++++++--- .../AccessKeyRegions/SelectedRegionsList.tsx | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx index 66e3e5f9ef9..45e9aa8a17d 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.stories.tsx @@ -1,3 +1,4 @@ +import { useArgs } from '@storybook/preview-api'; import React from 'react'; import { regions } from 'src/__data__/regionsData'; @@ -11,11 +12,13 @@ import type { Meta, StoryObj } from '@storybook/react'; export const Default: StoryObj = { render: (args) => { const SelectWrapper = () => { - const [open, setOpen] = React.useState(false); - + const [_, updateArgs] = useArgs(); return ( - setOpen(false)} open={open} /> + updateArgs({ value: region?.id })} + /> ); }; diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx index ecc69907885..480258a1ebc 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx @@ -29,7 +29,7 @@ const SelectedRegion = ({ selection }: LabelComponentProps) => { }} > - + {selection.label} @@ -41,7 +41,10 @@ export const SelectedRegionsList = ({ selectedRegions, }: SelectedRegionsProps) => { const handleRemove = (item: RemovableItem) => { - onRemove(item.value); + const regionId = selectedRegions.find((r) => r.label === item.label)?.id; + if (regionId) { + onRemove(regionId); + } }; return ( From 25631ac9ae10e6200ad67f5aa236097240ee0f0f Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 7 Jun 2024 12:34:33 -0400 Subject: [PATCH 11/11] fix up the obj multi-select --- .../RegionSelect/RegionMultiSelect.test.tsx | 84 +++++++------------ .../RegionSelect/RegionMultiSelect.tsx | 2 +- .../RemovableSelectionsList.tsx | 8 +- .../AccessKeyRegions/SelectedRegionsList.tsx | 11 +-- 4 files changed, 40 insertions(+), 65 deletions(-) diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx index 654040f540d..ea64ef5929b 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx @@ -7,19 +7,16 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { RegionMultiSelect } from './RegionMultiSelect'; -const regions = regionFactory.buildList(1, { +const regionNewark = regionFactory.build({ id: 'us-east', label: 'Newark, NJ', }); -const regionsNewark = regionFactory.buildList(1, { - id: 'us-east', - label: 'Newark, NJ', -}); -const regionsAtlanta = regionFactory.buildList(1, { +const regionAtlanta = regionFactory.build({ id: 'us-southeast', label: 'Atlanta, GA', }); + interface SelectedRegionsProps { onRemove: (region: string) => void; selectedRegions: Region[]; @@ -31,7 +28,7 @@ const SelectedRegionsList = ({
      {selectedRegions.map((region, index) => (
    • - {region.label} + {region.label} ({region.id})
    • ))} @@ -46,7 +43,7 @@ describe('RegionMultiSelect', () => { ); @@ -54,13 +51,13 @@ describe('RegionMultiSelect', () => { screen.getByRole('combobox', { name: 'Regions' }); }); - // Re-enable if we allow RegionMultiSelect to be uncontrolled - it.skip('should be able to select all the regions correctly', () => { + it('should be able to select all the regions correctly', () => { + const onChange = vi.fn(); renderWithTheme( ); @@ -70,27 +67,17 @@ describe('RegionMultiSelect', () => { fireEvent.click(screen.getByRole('option', { name: 'Select All' })); - // Check if all the option is selected - expect( - screen.getByRole('option', { - name: 'Newark, NJ (us-east)', - }) - ).toHaveAttribute('aria-selected', 'true'); - expect( - screen.getByRole('option', { - name: 'Newark, NJ (us-east)', - }) - ).toHaveAttribute('aria-selected', 'true'); + expect(onChange).toHaveBeenCalledWith([regionAtlanta.id, regionNewark.id]); }); - // Re-enable if we allow RegionMultiSelect to be uncontrolled - it.skip('should be able to deselect all the regions', () => { + it('should be able to deselect all the regions', () => { + const onChange = vi.fn(); renderWithTheme( ); @@ -99,21 +86,10 @@ describe('RegionMultiSelect', () => { fireEvent.click(screen.getByRole('option', { name: 'Deselect All' })); - // Check if all the option is deselected selected - expect( - screen.getByRole('option', { - name: 'Newark, NJ (us-east)', - }) - ).toHaveAttribute('aria-selected', 'false'); - expect( - screen.getByRole('option', { - name: 'Newark, NJ (us-east)', - }) - ).toHaveAttribute('aria-selected', 'false'); + expect(onChange).toHaveBeenCalledWith([]); }); - // Re-enable if we allow RegionMultiSelect to be uncontrolled - it.skip('should render selected regions correctly', () => { + it('should render selected regions correctly', () => { renderWithTheme( ( @@ -124,29 +100,33 @@ describe('RegionMultiSelect', () => { )} currentCapability="Block Storage" onChange={mockHandleSelection} - regions={[...regionsNewark, ...regionsAtlanta]} - selectedIds={[]} + regions={[regionNewark, regionAtlanta]} + selectedIds={[regionNewark.id]} /> ); // Open the dropdown fireEvent.click(screen.getByRole('button', { name: 'Open' })); - fireEvent.click(screen.getByRole('option', { name: 'Select All' })); - - // Close the dropdown - fireEvent.click(screen.getByRole('button', { name: 'Close' })); - - // Check if all the options are rendered + // Check Newark chip shows becaused it is selected expect( screen.getByRole('listitem', { - name: 'Newark, NJ (us-east)', + name: 'Newark, NJ', }) ).toBeInTheDocument(); + + // Newark is selected expect( - screen.getByRole('listitem', { + screen.getByRole('option', { name: 'Newark, NJ (us-east)', }) - ).toBeInTheDocument(); + ).toHaveAttribute('aria-selected', 'true'); + + // Atlanta is not selected + expect( + screen.getByRole('option', { + name: 'Atlanta, GA (us-southeast)', + }) + ).toHaveAttribute('aria-selected', 'false'); }); }); diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx index 0dd6c1e4987..4ba6d5879a7 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx @@ -46,7 +46,7 @@ const SelectedRegion = ({ region }: LabelComponentProps) => { > - {region.label} + {region.label} ({region.id}) ); }; diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx index db01e6a6f9f..34e14dc58a6 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx @@ -22,7 +22,7 @@ export type RemovableItem = { // as 'any' because we do not know what types they could be. // Trying to type them as 'unknown' led to type errors. [key: string]: any; - id: number; + id: number | string; label: string; }; @@ -117,9 +117,9 @@ export const RemovableSelectionsList = ( // used to determine when to display a box-shadow to indicate scrollability const listRef = React.useRef(null); const [listHeight, setListHeight] = React.useState(0); - const [removingItemId, setRemovingItemId] = React.useState( - null - ); + const [removingItemId, setRemovingItemId] = React.useState< + null | number | string + >(null); const [isRemoving, setIsRemoving] = React.useState(false); React.useEffect(() => { diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx index 480258a1ebc..50ff7ccd614 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList.tsx @@ -31,7 +31,7 @@ const SelectedRegion = ({ selection }: LabelComponentProps) => { - {selection.label} + {selection.label} ({selection.id}) ); }; @@ -41,17 +41,12 @@ export const SelectedRegionsList = ({ selectedRegions, }: SelectedRegionsProps) => { const handleRemove = (item: RemovableItem) => { - const regionId = selectedRegions.find((r) => r.label === item.label)?.id; - if (regionId) { - onRemove(regionId); - } + onRemove(item.id as string); }; return ( { - return { ...item, id: index }; - })} + selectionData={selectedRegions} LabelComponent={SelectedRegion} headerText="" noDataText=""