diff --git a/packages/api-v4/src/regions/types.ts b/packages/api-v4/src/regions/types.ts index 23204247879..84871a6a6b0 100644 --- a/packages/api-v4/src/regions/types.ts +++ b/packages/api-v4/src/regions/types.ts @@ -24,6 +24,8 @@ export interface DNSResolvers { export type RegionStatus = 'ok' | 'outage'; +export type RegionSite = 'core' | 'edge'; + export interface Region { id: string; label: string; @@ -33,6 +35,7 @@ export interface Region { maximum_vms_per_pg: number; status: RegionStatus; resolvers: DNSResolvers; + site_type: RegionSite; } export interface RegionAvailability { diff --git a/packages/manager/.changeset/pr-10194-upcoming-features-1708456171781.md b/packages/manager/.changeset/pr-10194-upcoming-features-1708456171781.md new file mode 100644 index 00000000000..7e255b7427c --- /dev/null +++ b/packages/manager/.changeset/pr-10194-upcoming-features-1708456171781.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update Region Select for edge sites ([#10194](https://github.com/linode/manager/pull/10194)) diff --git a/packages/manager/src/__data__/regionsData.ts b/packages/manager/src/__data__/regionsData.ts index c8afbb336d6..6e44c8dd739 100644 --- a/packages/manager/src/__data__/regionsData.ts +++ b/packages/manager/src/__data__/regionsData.ts @@ -24,6 +24,7 @@ export const regions: Region[] = [ ipv6: '2400:8904::f03c:91ff:fea5:659, 2400:8904::f03c:91ff:fea5:9282, 2400:8904::f03c:91ff:fea5:b9b3, 2400:8904::f03c:91ff:fea5:925a, 2400:8904::f03c:91ff:fea5:22cb, 2400:8904::f03c:91ff:fea5:227a, 2400:8904::f03c:91ff:fea5:924c, 2400:8904::f03c:91ff:fea5:f7e2, 2400:8904::f03c:91ff:fea5:2205, 2400:8904::f03c:91ff:fea5:9207', }, + site_type: 'core', status: 'ok', }, { @@ -48,6 +49,7 @@ export const regions: Region[] = [ ipv6: '2600:3c04::f03c:91ff:fea9:f63, 2600:3c04::f03c:91ff:fea9:f6d, 2600:3c04::f03c:91ff:fea9:f80, 2600:3c04::f03c:91ff:fea9:f0f, 2600:3c04::f03c:91ff:fea9:f99, 2600:3c04::f03c:91ff:fea9:fbd, 2600:3c04::f03c:91ff:fea9:fdd, 2600:3c04::f03c:91ff:fea9:fe2, 2600:3c04::f03c:91ff:fea9:f68, 2600:3c04::f03c:91ff:fea9:f4a', }, + site_type: 'core', status: 'ok', }, { @@ -72,6 +74,7 @@ export const regions: Region[] = [ ipv6: '2400:8907::f03c:92ff:fe6e:ec8, 2400:8907::f03c:92ff:fe6e:98e4, 2400:8907::f03c:92ff:fe6e:1c58, 2400:8907::f03c:92ff:fe6e:c299, 2400:8907::f03c:92ff:fe6e:c210, 2400:8907::f03c:92ff:fe6e:c219, 2400:8907::f03c:92ff:fe6e:1c5c, 2400:8907::f03c:92ff:fe6e:c24e, 2400:8907::f03c:92ff:fe6e:e6b, 2400:8907::f03c:92ff:fe6e:e3d', }, + site_type: 'core', status: 'ok', }, { @@ -98,6 +101,7 @@ export const regions: Region[] = [ ipv6: '2600:3c05::f03c:93ff:feb6:43b6, 2600:3c05::f03c:93ff:feb6:4365, 2600:3c05::f03c:93ff:feb6:43c2, 2600:3c05::f03c:93ff:feb6:e441, 2600:3c05::f03c:93ff:feb6:94ef, 2600:3c05::f03c:93ff:feb6:94ba, 2600:3c05::f03c:93ff:feb6:94a8, 2600:3c05::f03c:93ff:feb6:9413, 2600:3c05::f03c:93ff:feb6:9443, 2600:3c05::f03c:93ff:feb6:94e0', }, + site_type: 'core', status: 'ok', }, { @@ -124,6 +128,7 @@ export const regions: Region[] = [ ipv6: '2600:3c06::f03c:93ff:fed0:e5fc, 2600:3c06::f03c:93ff:fed0:e54b, 2600:3c06::f03c:93ff:fed0:e572, 2600:3c06::f03c:93ff:fed0:e530, 2600:3c06::f03c:93ff:fed0:e597, 2600:3c06::f03c:93ff:fed0:e511, 2600:3c06::f03c:93ff:fed0:e5f2, 2600:3c06::f03c:93ff:fed0:e5bf, 2600:3c06::f03c:93ff:fed0:e529, 2600:3c06::f03c:93ff:fed0:e5a3', }, + site_type: 'core', status: 'ok', }, { @@ -150,6 +155,7 @@ export const regions: Region[] = [ ipv6: '2600:3c07::f03c:93ff:fef2:2e63, 2600:3c07::f03c:93ff:fef2:2ec7, 2600:3c07::f03c:93ff:fef2:0dee, 2600:3c07::f03c:93ff:fef2:0d25, 2600:3c07::f03c:93ff:fef2:0de0, 2600:3c07::f03c:93ff:fef2:2e29, 2600:3c07::f03c:93ff:fef2:0dda, 2600:3c07::f03c:93ff:fef2:0d82, 2600:3c07::f03c:93ff:fef2:b3ac, 2600:3c07::f03c:93ff:fef2:b3a8', }, + site_type: 'core', status: 'ok', }, { @@ -175,6 +181,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0a::f03c:93ff:fe54:c6da, 2600:3c0a::f03c:93ff:fe54:c691, 2600:3c0a::f03c:93ff:fe54:c68d, 2600:3c0a::f03c:93ff:fe54:c61e, 2600:3c0a::f03c:93ff:fe54:c653, 2600:3c0a::f03c:93ff:fe54:c64c, 2600:3c0a::f03c:93ff:fe54:c68a, 2600:3c0a::f03c:93ff:fe54:c697, 2600:3c0a::f03c:93ff:fe54:c60f, 2600:3c0a::f03c:93ff:fe54:c6a0', }, + site_type: 'core', status: 'ok', }, { @@ -200,6 +207,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0d::f03c:93ff:fe3d:51cb, 2600:3c0d::f03c:93ff:fe3d:51a7, 2600:3c0d::f03c:93ff:fe3d:51a9, 2600:3c0d::f03c:93ff:fe3d:5119, 2600:3c0d::f03c:93ff:fe3d:51fe, 2600:3c0d::f03c:93ff:fe3d:517c, 2600:3c0d::f03c:93ff:fe3d:5144, 2600:3c0d::f03c:93ff:fe3d:5170, 2600:3c0d::f03c:93ff:fe3d:51cc, 2600:3c0d::f03c:93ff:fe3d:516c', }, + site_type: 'core', status: 'ok', }, { @@ -225,6 +233,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0e::f03c:93ff:fe9d:2d10, 2600:3c0e::f03c:93ff:fe9d:2d89, 2600:3c0e::f03c:93ff:fe9d:2d79, 2600:3c0e::f03c:93ff:fe9d:2d96, 2600:3c0e::f03c:93ff:fe9d:2da5, 2600:3c0e::f03c:93ff:fe9d:2d34, 2600:3c0e::f03c:93ff:fe9d:2d68, 2600:3c0e::f03c:93ff:fe9d:2d17, 2600:3c0e::f03c:93ff:fe9d:2d45, 2600:3c0e::f03c:93ff:fe9d:2d5c', }, + site_type: 'core', status: 'ok', }, { @@ -250,6 +259,7 @@ export const regions: Region[] = [ ipv6: '2600:3c09::f03c:93ff:fea9:4dbe, 2600:3c09::f03c:93ff:fea9:4d63, 2600:3c09::f03c:93ff:fea9:4dce, 2600:3c09::f03c:93ff:fea9:4dbb, 2600:3c09::f03c:93ff:fea9:4d99, 2600:3c09::f03c:93ff:fea9:4d26, 2600:3c09::f03c:93ff:fea9:4de0, 2600:3c09::f03c:93ff:fea9:4d69, 2600:3c09::f03c:93ff:fea9:4dbf, 2600:3c09::f03c:93ff:fea9:4da6', }, + site_type: 'core', status: 'ok', }, { @@ -275,6 +285,7 @@ export const regions: Region[] = [ ipv6: '2600:3c08::f03c:93ff:fe7c:1135, 2600:3c08::f03c:93ff:fe7c:11f8, 2600:3c08::f03c:93ff:fe7c:11d2, 2600:3c08::f03c:93ff:fe7c:11a7, 2600:3c08::f03c:93ff:fe7c:11ad, 2600:3c08::f03c:93ff:fe7c:110a, 2600:3c08::f03c:93ff:fe7c:11f9, 2600:3c08::f03c:93ff:fe7c:1137, 2600:3c08::f03c:93ff:fe7c:11db, 2600:3c08::f03c:93ff:fe7c:1164', }, + site_type: 'core', status: 'ok', }, { @@ -300,6 +311,7 @@ export const regions: Region[] = [ ipv6: '2400:8905::f03c:93ff:fe9d:b085, 2400:8905::f03c:93ff:fe9d:b012, 2400:8905::f03c:93ff:fe9d:b09b, 2400:8905::f03c:93ff:fe9d:b0d8, 2400:8905::f03c:93ff:fe9d:259f, 2400:8905::f03c:93ff:fe9d:b006, 2400:8905::f03c:93ff:fe9d:b084, 2400:8905::f03c:93ff:fe9d:b0ce, 2400:8905::f03c:93ff:fe9d:25ea, 2400:8905::f03c:93ff:fe9d:b086', }, + site_type: 'core', status: 'ok', }, { @@ -325,6 +337,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0b::f03c:93ff:feba:d513, 2600:3c0b::f03c:93ff:feba:d5c3, 2600:3c0b::f03c:93ff:feba:d597, 2600:3c0b::f03c:93ff:feba:d5fb, 2600:3c0b::f03c:93ff:feba:d51f, 2600:3c0b::f03c:93ff:feba:d58e, 2600:3c0b::f03c:93ff:feba:d5d5, 2600:3c0b::f03c:93ff:feba:d534, 2600:3c0b::f03c:93ff:feba:d57c, 2600:3c0b::f03c:93ff:feba:d529', }, + site_type: 'core', status: 'ok', }, { @@ -350,6 +363,7 @@ export const regions: Region[] = [ ipv6: '2a01:7e04::f03c:93ff:fead:d31f, 2a01:7e04::f03c:93ff:fead:d37f, 2a01:7e04::f03c:93ff:fead:d30c, 2a01:7e04::f03c:93ff:fead:d318, 2a01:7e04::f03c:93ff:fead:d316, 2a01:7e04::f03c:93ff:fead:d339, 2a01:7e04::f03c:93ff:fead:d367, 2a01:7e04::f03c:93ff:fead:d395, 2a01:7e04::f03c:93ff:fead:d3d0, 2a01:7e04::f03c:93ff:fead:d38e', }, + site_type: 'core', status: 'ok', }, { @@ -375,6 +389,7 @@ export const regions: Region[] = [ ipv6: '2600:3c0c::f03c:93ff:feed:a90b, 2600:3c0c::f03c:93ff:feed:a9a5, 2600:3c0c::f03c:93ff:feed:a935, 2600:3c0c::f03c:93ff:feed:a930, 2600:3c0c::f03c:93ff:feed:a95c, 2600:3c0c::f03c:93ff:feed:a9ad, 2600:3c0c::f03c:93ff:feed:a9f2, 2600:3c0c::f03c:93ff:feed:a9ff, 2600:3c0c::f03c:93ff:feed:a9c8, 2600:3c0c::f03c:93ff:feed:a96b', }, + site_type: 'core', status: 'ok', }, { @@ -400,6 +415,7 @@ export const regions: Region[] = [ ipv6: '2a01:7e03::f03c:93ff:feb1:b789, 2a01:7e03::f03c:93ff:feb1:b717, 2a01:7e03::f03c:93ff:feb1:b707, 2a01:7e03::f03c:93ff:feb1:b7ab, 2a01:7e03::f03c:93ff:feb1:b7e2, 2a01:7e03::f03c:93ff:feb1:b709, 2a01:7e03::f03c:93ff:feb1:b7a6, 2a01:7e03::f03c:93ff:feb1:b750, 2a01:7e03::f03c:93ff:feb1:b76e, 2a01:7e03::f03c:93ff:feb1:b7a2', }, + site_type: 'core', status: 'ok', }, { @@ -424,6 +440,7 @@ export const regions: Region[] = [ ipv6: '2600:3c00::2, 2600:3c00::9, 2600:3c00::7, 2600:3c00::5, 2600:3c00::3, 2600:3c00::8, 2600:3c00::6, 2600:3c00::4, 2600:3c00::c, 2600:3c00::b', }, + site_type: 'core', status: 'ok', }, { @@ -447,6 +464,7 @@ export const regions: Region[] = [ ipv6: '2600:3c01::2, 2600:3c01::9, 2600:3c01::5, 2600:3c01::7, 2600:3c01::3, 2600:3c01::8, 2600:3c01::4, 2600:3c01::b, 2600:3c01::c, 2600:3c01::6', }, + site_type: 'core', status: 'ok', }, { @@ -473,6 +491,7 @@ export const regions: Region[] = [ ipv6: '2600:3c02::3, 2600:3c02::5, 2600:3c02::4, 2600:3c02::6, 2600:3c02::c, 2600:3c02::7, 2600:3c02::2, 2600:3c02::9, 2600:3c02::8, 2600:3c02::b', }, + site_type: 'core', status: 'ok', }, { @@ -500,6 +519,7 @@ export const regions: Region[] = [ ipv6: '2600:3c03::7, 2600:3c03::4, 2600:3c03::9, 2600:3c03::6, 2600:3c03::3, 2600:3c03::c, 2600:3c03::5, 2600:3c03::b, 2600:3c03::2, 2600:3c03::8', }, + site_type: 'core', status: 'ok', }, { @@ -524,6 +544,7 @@ export const regions: Region[] = [ ipv6: '2a01:7e00::9, 2a01:7e00::3, 2a01:7e00::c, 2a01:7e00::5, 2a01:7e00::6, 2a01:7e00::8, 2a01:7e00::b, 2a01:7e00::4, 2a01:7e00::7, 2a01:7e00::2', }, + site_type: 'core', status: 'ok', }, { @@ -550,6 +571,7 @@ export const regions: Region[] = [ ipv6: '2400:8901::5, 2400:8901::4, 2400:8901::b, 2400:8901::3, 2400:8901::9, 2400:8901::2, 2400:8901::8, 2400:8901::7, 2400:8901::c, 2400:8901::6', }, + site_type: 'core', status: 'ok', }, { @@ -576,6 +598,23 @@ export const regions: Region[] = [ ipv6: '2a01:7e01::5, 2a01:7e01::9, 2a01:7e01::7, 2a01:7e01::c, 2a01:7e01::2, 2a01:7e01::4, 2a01:7e01::3, 2a01:7e01::6, 2a01:7e01::b, 2a01:7e01::8', }, + site_type: 'core', + status: 'ok', + }, + { + capabilities: ['Linodes'], + country: 'us', + id: 'us-edgetest', + label: 'Gecko Edge Test', + maximum_pgs_per_customer: 5, + maximum_vms_per_pg: 10, + resolvers: { + ipv4: + '139.162.130.5, 139.162.131.5, 139.162.132.5, 139.162.133.5, 139.162.134.5, 139.162.135.5, 139.162.136.5, 139.162.137.5, 139.162.138.5, 139.162.139.5', + ipv6: + '2a01:7e01::5, 2a01:7e01::9, 2a01:7e01::7, 2a01:7e01::c, 2a01:7e01::2, 2a01:7e01::4, 2a01:7e01::3, 2a01:7e01::6, 2a01:7e01::b, 2a01:7e01::8', + }, + site_type: 'edge', status: 'ok', }, ]; diff --git a/packages/manager/src/assets/icons/entityIcons/edge-server.svg b/packages/manager/src/assets/icons/entityIcons/edge-server.svg new file mode 100644 index 00000000000..1ca05241491 --- /dev/null +++ b/packages/manager/src/assets/icons/entityIcons/edge-server.svg @@ -0,0 +1,7 @@ + + +edge-server + + + + diff --git a/packages/manager/src/components/Autocomplete/Autocomplete.tsx b/packages/manager/src/components/Autocomplete/Autocomplete.tsx index 3aa494f3c23..6d615d60e02 100644 --- a/packages/manager/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/manager/src/components/Autocomplete/Autocomplete.tsx @@ -113,6 +113,12 @@ export const Autocomplete = < InputProps={{ ...params.InputProps, ...textFieldProps?.InputProps, + endAdornment: ( + + {textFieldProps?.InputProps?.endAdornment} + {params.InputProps.endAdornment} + + ), }} /> )} diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx index 4b37ee42435..aa235200cb3 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx @@ -117,6 +117,11 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { /> ); }} + sx={(theme) => ({ + [theme.breakpoints.up('md')]: { + width: '416px', + }, + })} textFieldProps={{ InputProps: { required, diff --git a/packages/manager/src/components/RegionSelect/RegionOption.tsx b/packages/manager/src/components/RegionSelect/RegionOption.tsx index 38fe2feaccd..7186b1cc6c3 100644 --- a/packages/manager/src/components/RegionSelect/RegionOption.tsx +++ b/packages/manager/src/components/RegionSelect/RegionOption.tsx @@ -1,27 +1,36 @@ import { visuallyHidden } from '@mui/utils'; import React from 'react'; +import EdgeServer from 'src/assets/icons/entityIcons/edge-server.svg'; import { Box } from 'src/components/Box'; import { Flag } from 'src/components/Flag'; import { Link } from 'src/components/Link'; import { Tooltip } from 'src/components/Tooltip'; +import { TooltipIcon } from 'src/components/TooltipIcon'; import { SelectedIcon, StyledFlagContainer, StyledListItem, + sxEdgeIcon, } from './RegionSelect.styles'; import { RegionSelectOption } from './RegionSelect.types'; import type { ListItemComponentsPropsOverrides } from '@mui/material/ListItem'; type Props = { + displayEdgeServerIcon?: boolean; option: RegionSelectOption; props: React.HTMLAttributes; - selected: boolean; + selected?: boolean; }; -export const RegionOption = ({ option, props, selected }: Props) => { +export const RegionOption = ({ + displayEdgeServerIcon, + option, + props, + selected, +}: Props) => { const isDisabledMenuItem = option.unavailable; return ( @@ -78,6 +87,11 @@ export const RegionOption = ({ option, props, selected }: Props) => { {option.label} + {displayEdgeServerIcon && ( + +  (This region is an Edge site.) + + )} {isDisabledMenuItem && ( Disabled option - There may be limited capacity in this region. @@ -87,6 +101,14 @@ export const RegionOption = ({ option, props, selected }: Props) => { )} {selected && } + {displayEdgeServerIcon && ( + } + status="other" + sxTooltipIcon={sxEdgeIcon} + text="This region is an edge site." + /> + )} diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.styles.ts b/packages/manager/src/components/RegionSelect/RegionSelect.styles.ts index b71ef206b31..d4a72010e19 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.styles.ts +++ b/packages/manager/src/components/RegionSelect/RegionSelect.styles.ts @@ -20,8 +20,51 @@ export const StyledAutocompleteContainer = styled(Box, { marginTop: -8, }, }, + '& .MuiAutocomplete-root .MuiAutocomplete-inputRoot': { + paddingRight: 8, + }, + display: 'flex', + [theme.breakpoints.down('md')]: { + flexDirection: 'column', + }, })); +export const sxEdgeIcon = { + '& svg': { + color: 'inherit !important', + height: 21, + width: 24, + }, + '&:hover': { + color: 'inherit', + }, + color: 'inherit', + padding: 0, +}; + +export const StyledEdgeBox = styled(Box, { label: 'StyledEdgeBox' })( + ({ theme }) => ({ + '& svg': { + height: 21, + marginLeft: 8, + marginRight: 8, + width: 24, + }, + alignSelf: 'end', + color: 'inherit', + display: 'flex', + marginLeft: 8, + padding: '8px 0', + [theme.breakpoints.down('md')]: { + '& svg': { + marginLeft: 0, + }, + alignSelf: 'start', + marginLeft: 0, + }, + }) +); + export const StyledFlagContainer = styled('div', { label: 'RegionSelectFlagContainer', })(({ theme }) => ({ diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx index 33f6cd426f0..dc1ed580610 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx @@ -64,4 +64,22 @@ describe('RegionSelect', () => { ); expect(getByTestId('textfield-input')).toBeDisabled(); }); + + it('should render a Select component with edge region text', () => { + const newProps = { + ...props, + showGeckoHelperText: true, + }; + const { getByTestId } = renderWithTheme(); + expect(getByTestId('region-select-edge-text')).toBeInTheDocument(); + }); + + it('should render a Select component with no edge region text', () => { + const newProps = { + ...props, + showGeckoHelperText: false, + }; + const { queryByTestId } = renderWithTheme(); + expect(queryByTestId('region-select-edge-text')).not.toBeInTheDocument(); + }); }); diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.tsx index 683728688ef..c1ea6e51432 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.tsx @@ -1,13 +1,19 @@ +import { Typography } from '@mui/material'; import * as React from 'react'; +import EdgeServer from 'src/assets/icons/entityIcons/edge-server.svg'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Flag } from 'src/components/Flag'; +import { Link } from 'src/components/Link'; +import { TooltipIcon } from 'src/components/TooltipIcon'; import { useAccountAvailabilitiesQueryUnpaginated } from 'src/queries/accountAvailability'; import { RegionOption } from './RegionOption'; import { StyledAutocompleteContainer, + StyledEdgeBox, StyledFlagContainer, + sxEdgeIcon, } from './RegionSelect.styles'; import { getRegionOptions, getSelectedRegionById } from './RegionSelect.utils'; @@ -20,14 +26,17 @@ import type { * A specific select for regions. * * The RegionSelect automatically filters regions based on capability using its `currentCapability` prop. For example, if - * `currentCapability="VPCs"`, only regions that support VPCs will appear in the RegionSelect dropdown. There is no need to - * prefilter regions when passing them to the RegionSelect. See the description of `currentCapability` prop for more information. + * `currentCapability="VPCs"`, only regions that support VPCs will appear in the RegionSelect dropdown. Edge regions are filtered based on the `hideEdgeServers` prop. + * There is no need to pre-filter regions when passing them to the RegionSelect. See the description of `currentCapability` prop for more information. + * + * We do not display the selected check mark for single selects. */ export const RegionSelect = React.memo((props: RegionSelectProps) => { const { currentCapability, disabled, errorText, + geckoEnabled, handleSelection, helperText, isClearable, @@ -35,6 +44,7 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { regions, required, selectedId, + showGeckoHelperText, width, } = props; @@ -74,9 +84,10 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { getRegionOptions({ accountAvailabilityData: accountAvailability, currentCapability, + hideEdgeServers: !geckoEnabled, regions, }), - [accountAvailability, currentCapability, regions] + [accountAvailability, currentCapability, regions, geckoEnabled] ); return ( @@ -95,18 +106,34 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { handleRegionChange(null); } }} - renderOption={(props, option, { selected }) => { + renderOption={(props, option) => { return ( ); }} + sx={(theme) => ({ + [theme.breakpoints.up('md')]: { + width: '416px', + }, + })} textFieldProps={{ InputProps: { + endAdornment: geckoEnabled && + selectedRegion?.site_type === 'edge' && ( + } + status="other" + sxTooltipIcon={sxEdgeIcon} + text="This region is an Edge server." + /> + ), required, startAdornment: selectedRegion && ( @@ -132,6 +159,22 @@ export const RegionSelect = React.memo((props: RegionSelectProps) => { placeholder="Select a Region" value={selectedRegion} /> + {showGeckoHelperText && ( // @TODO Gecko MVP: Add docs link + + + + {' '} + Indicates an Edge server region.{' '} + + Learn more + + . + + + )} ); }); diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts index 90460892f2f..1d6b6b16073 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.types.ts +++ b/packages/manager/src/components/RegionSelect/RegionSelect.types.ts @@ -5,6 +5,7 @@ import type { Capabilities, Country, Region, + RegionSite, } from '@linode/api-v4'; import type { EnhancedAutocompleteProps } from 'src/components/Autocomplete/Autocomplete'; @@ -14,6 +15,7 @@ export interface RegionSelectOption { region: string; }; label: string; + site_type: RegionSite; unavailable: boolean; value: string; } @@ -31,6 +33,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; + geckoEnabled?: boolean; handleSelection: (id: string) => void; helperText?: string; isClearable?: boolean; @@ -38,6 +41,7 @@ export interface RegionSelectProps regions: Region[]; required?: boolean; selectedId: null | string; + showGeckoHelperText?: boolean; width?: number; } @@ -68,6 +72,7 @@ export interface RegionOptionAvailability { } export interface GetRegionOptions extends RegionOptionAvailability { + hideEdgeServers?: boolean; regions: Region[]; } @@ -86,3 +91,5 @@ export interface GetSelectedRegionsByIdsArgs { regions: Region[]; selectedRegionIds: string[]; } + +export type SupportedEdgeTypes = 'Distributions' | 'StackScripts'; diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx index f7a4b1c2acc..5b424acc1c7 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx @@ -38,6 +38,17 @@ const regions: Region[] = [ }), ]; +const regionsWithEdge = [ + ...regions, + regionFactory.build({ + capabilities: ['Linodes'], + country: 'us', + id: 'us-edge-1', + label: 'Gecko Edge Test', + site_type: 'edge', + }), +]; + const expectedRegions: RegionSelectOption[] = [ { data: { @@ -45,18 +56,21 @@ const expectedRegions: RegionSelectOption[] = [ region: 'North America', }, label: 'US Location (us-1)', + site_type: 'core', unavailable: false, value: 'us-1', }, { data: { country: 'ca', region: 'North America' }, label: 'CA Location (ca-1)', + site_type: 'core', unavailable: false, value: 'ca-1', }, { data: { country: 'jp', region: 'Asia' }, label: 'JP Location (jp-1)', + site_type: 'core', unavailable: false, value: 'jp-1', }, @@ -103,6 +117,39 @@ describe('getRegionOptions', () => { expect(result).toEqual(expectedRegions); }); + + it('should filter out edge regions if hideEdgeServers is true', () => { + const result: RegionSelectOption[] = getRegionOptions({ + accountAvailabilityData, + currentCapability: 'Linodes', + hideEdgeServers: true, + regions: regionsWithEdge, + }); + + expect(result).toEqual(expectedRegions); + }); + + it('should not filter out edge regions if hideEdgeServers is false', () => { + const expectedRegionsWithEdge = [ + { + data: { country: 'us', region: 'North America' }, + label: 'Gecko Edge Test (us-edge-1)', + site_type: 'edge', + unavailable: false, + value: 'us-edge-1', + }, + ...expectedRegions, + ]; + + const result: RegionSelectOption[] = getRegionOptions({ + accountAvailabilityData, + currentCapability: 'Linodes', + hideEdgeServers: false, + regions: regionsWithEdge, + }); + + expect(result).toEqual(expectedRegionsWithEdge); + }); }); describe('getSelectedRegionById', () => { @@ -123,6 +170,7 @@ describe('getSelectedRegionById', () => { region: 'North America', }, label: 'US Location (us-1)', + site_type: 'core', unavailable: false, value: 'us-1', }; @@ -188,6 +236,7 @@ describe('getSelectedRegionsByIds', () => { region: 'North America', }, label: 'US Location (us-1)', + site_type: 'core', unavailable: false, value: 'us-1', }, @@ -197,6 +246,7 @@ describe('getSelectedRegionsByIds', () => { region: 'North America', }, label: 'CA Location (ca-1)', + site_type: 'core', unavailable: false, value: 'ca-1', }, @@ -222,6 +272,7 @@ describe('getSelectedRegionsByIds', () => { region: 'North America', }, label: 'US Location (us-1)', + site_type: 'core', unavailable: false, value: 'us-1', }, diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.ts b/packages/manager/src/components/RegionSelect/RegionSelect.utils.ts index d5f54ca0304..597b7b1276d 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.ts +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.ts @@ -1,5 +1,6 @@ import { CONTINENT_CODE_TO_CONTINENT } from '@linode/api-v4'; +import { useFlags } from 'src/hooks/useFlags'; import { getRegionCountryGroup, getSelectedRegion, @@ -11,8 +12,10 @@ import type { GetSelectedRegionById, GetSelectedRegionsByIdsArgs, RegionSelectOption, + SupportedEdgeTypes, } from './RegionSelect.types'; import type { AccountAvailability, Region } from '@linode/api-v4'; +import type { LinodeCreateType } from 'src/features/Linodes/LinodesCreate/types'; const NORTH_AMERICA = CONTINENT_CODE_TO_CONTINENT.NA; @@ -25,15 +28,22 @@ const NORTH_AMERICA = CONTINENT_CODE_TO_CONTINENT.NA; export const getRegionOptions = ({ accountAvailabilityData, currentCapability, + hideEdgeServers = false, regions, }: GetRegionOptions): RegionSelectOption[] => { - const filteredRegions = currentCapability + const filteredRegionsByCapability = currentCapability ? regions.filter((region) => region.capabilities.includes(currentCapability) ) : regions; - return filteredRegions + const filteredRegionsByCapabilityAndSiteType = hideEdgeServers + ? filteredRegionsByCapability.filter( + (region) => region.site_type !== 'edge' + ) + : filteredRegionsByCapability; + + return filteredRegionsByCapabilityAndSiteType .map((region: Region) => { const group = getRegionCountryGroup(region); @@ -43,6 +53,7 @@ export const getRegionOptions = ({ region: group, }, label: `${region.label} (${region.id})`, + site_type: region.site_type, unavailable: getRegionOptionAvailability({ accountAvailabilityData, currentCapability, @@ -116,6 +127,7 @@ export const getSelectedRegionById = ({ region: group, }, label: `${selectedRegion.label} (${selectedRegion.id})`, + site_type: selectedRegion.site_type, unavailable: getRegionOptionAvailability({ accountAvailabilityData, currentCapability, @@ -175,3 +187,21 @@ export const getSelectedRegionsByIds = ({ ) .filter((region): region is RegionSelectOption => !!region); }; + +/** + * Util to determine whether a create type has support for edge regions. + * + * @returns a boolean indicating whether or not to enable gecko. + */ +export const useIsGeckoEnabled = (createType: LinodeCreateType) => { + const flags = useFlags(); + + const supportedEdgeTypes: SupportedEdgeTypes[] = [ + 'Distributions', + 'StackScripts', + ]; + return Boolean( + flags.gecko && + !supportedEdgeTypes.includes(createType as SupportedEdgeTypes) + ); +}; diff --git a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.test.tsx b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.test.tsx index 2a21db6cd10..ab77c8c36be 100644 --- a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.test.tsx +++ b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.test.tsx @@ -11,6 +11,7 @@ const pricingMocks = vi.hoisted(() => ({ })); const queryParamMocks = vi.hoisted(() => ({ + getQueryParamFromQueryString: vi.fn().mockReturnValue({}), getQueryParamsFromQueryString: vi.fn().mockReturnValue({}), })); @@ -20,6 +21,7 @@ vi.mock('src/utilities/pricing/linodes', () => ({ })); vi.mock('src/utilities/queryParams', () => ({ + getQueryParamFromQueryString: queryParamMocks.getQueryParamFromQueryString, getQueryParamsFromQueryString: queryParamMocks.getQueryParamsFromQueryString, })); diff --git a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx index 4db3bdde0ce..0d3db1b4740 100644 --- a/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx +++ b/packages/manager/src/components/SelectRegionPanel/SelectRegionPanel.tsx @@ -6,6 +6,7 @@ import { useLocation } from 'react-router-dom'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; +import { useIsGeckoEnabled } from 'src/components/RegionSelect/RegionSelect.utils'; import { RegionHelperText } from 'src/components/SelectRegionPanel/RegionHelperText'; import { Typography } from 'src/components/Typography'; import { CROSS_DATA_CENTER_CLONE_WARNING } from 'src/features/Linodes/LinodesCreate/constants'; @@ -22,6 +23,8 @@ import { Box } from '../Box'; import { DocsLink } from '../DocsLink/DocsLink'; import { Link } from '../Link'; +import type { LinodeCreateType } from 'src/features/Linodes/LinodesCreate/types'; + interface SelectRegionPanelProps { currentCapability: Capabilities; disabled?: boolean; @@ -72,6 +75,18 @@ export const SelectRegionPanel = (props: SelectRegionPanelProps) => { type, }); + const geckoEnabled = useIsGeckoEnabled(params.type as LinodeCreateType); + + const showGeckoHelperText = Boolean( + geckoEnabled && + currentCapability && + regions.find( + (region) => + region.site_type === 'edge' && + region.capabilities.includes(currentCapability) + ) + ); + if (props.regions.length === 0) { return null; } @@ -116,10 +131,12 @@ export const SelectRegionPanel = (props: SelectRegionPanelProps) => { currentCapability={currentCapability} disabled={disabled} errorText={error} + geckoEnabled={geckoEnabled} handleSelection={handleSelection} helperText={helperText} regions={regions} selectedId={selectedId || null} + showGeckoHelperText={showGeckoHelperText} /> {showClonePriceWarning && ( ({ maximum_pgs_per_customer: 5, maximum_vms_per_pg: 10, resolvers: resolverFactory.build(), + site_type: 'core', status: 'ok', }); @@ -42,6 +43,7 @@ export const regionWithDynamicPricingFactory = Factory.Sync.makeFactory( maximum_pgs_per_customer: 5, maximum_vms_per_pg: 10, resolvers: resolverFactory.build(), + site_type: 'core', status: 'ok', } ); diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index c972cf17c3f..8e03993b944 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -47,6 +47,7 @@ export interface Flags { databaseScaleUp: boolean; databases: boolean; firewallNodebalancer: boolean; + gecko: boolean; ipv6Sharing: boolean; linodeCloneUIChanges: boolean; linodeCreateWithFirewall: boolean; diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index 6773a912703..88717cc573e 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -21,6 +21,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { useEventsPollingActions } from 'src/queries/events/events'; import { useCreateImageMutation } from 'src/queries/images'; import { useGrants, useProfile } from 'src/queries/profile'; +import { useRegionsQuery } from 'src/queries/regions'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; @@ -99,6 +100,7 @@ export const CreateImageTab: React.FC = (props) => { const { data: profile } = useProfile(); const { data: grants } = useGrants(); const flags = useFlags(); + const { data: regions } = useRegionsQuery(); const { mutateAsync: createImage } = useCreateImageMutation(); @@ -236,6 +238,9 @@ export const CreateImageTab: React.FC = (props) => { const linodeError = hasErrorFor('linode_id'); const diskError = hasErrorFor('disk_id'); + const linodeIsNotInEdgeRegion = (linodeRegion: string) => + regions?.find((region) => region.id === linodeRegion)?.site_type !== 'edge'; + return ( {!canCreateImage ? ( @@ -251,7 +256,9 @@ export const CreateImageTab: React.FC = (props) => { - availableLinodesToImagize?.includes(linode.id) ?? true + (linodeIsNotInEdgeRegion(linode.region) && + availableLinodesToImagize?.includes(linode.id)) ?? + true } disabled={!canCreateImage} errorText={linodeError} diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx index 63f2f259c6c..8559fe6cb9c 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx @@ -39,11 +39,11 @@ import { RegionsProps } from 'src/containers/regions.container'; import { WithTypesProps } from 'src/containers/types.container'; import { WithLinodesProps } from 'src/containers/withLinodes.container'; import { EUAgreementCheckbox } from 'src/features/Account/Agreements/EUAgreementCheckbox'; -import { regionSupportsMetadata } from 'src/features/Linodes/LinodesCreate/utilities'; import { getMonthlyAndHourlyNodePricing, utoa, } from 'src/features/Linodes/LinodesCreate/utilities'; +import { regionSupportsMetadata } from 'src/features/Linodes/LinodesCreate/utilities'; import { SMTPRestrictionText } from 'src/features/Linodes/SMTPRestrictionText'; import { hasPlacementGroupReachedCapacity } from 'src/features/PlacementGroups/utils'; import { @@ -102,6 +102,7 @@ import { } from './types'; import type { Tab } from 'src/components/Tabs/TabLinkList'; +import type { LinodeCreateType } from 'src/features/Linodes/LinodesCreate/types'; export interface LinodeCreateProps { additionalIPv4RangesForVPC: ExtendedIP[]; @@ -205,7 +206,7 @@ export class LinodeCreate extends React.PureComponent< /** Get the query params as an object, excluding the "?" */ const queryParams = getQueryParamsFromQueryString(location.search); - const _tabs = [ + const _tabs: LinodeCreateType[] = [ 'Distributions', 'One-Click', 'StackScripts', diff --git a/packages/manager/src/features/Linodes/LinodesCreate/types.ts b/packages/manager/src/features/Linodes/LinodesCreate/types.ts index c7efd6d39bb..0907d08a61c 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/types.ts +++ b/packages/manager/src/features/Linodes/LinodesCreate/types.ts @@ -167,3 +167,11 @@ export type AllFormStateAndHandlers = BaseFormStateAndHandlers & CloneFormStateHandlers & StackScriptFormStateHandlers & BackupFormStateHandlers; + +export type LinodeCreateType = + | 'Backups' + | 'Clone Linode' + | 'Distributions' + | 'Images' + | 'One-Click' + | 'StackScripts'; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index dc0496d1fe3..8a577f9c373 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -667,6 +667,11 @@ export const handlers = [ label: 'metadata-test-region', region: 'eu-west', }); + const linodeInEdgeRegion = linodeFactory.build({ + image: 'edge-test-image', + label: 'edge-test-region', + region: 'us-southeast', + }); const onlineLinodes = linodeFactory.buildList(40, { backups: { enabled: false }, ipv4: ['000.000.000.000'], @@ -697,6 +702,7 @@ export const handlers = [ const linodes = [ metadataLinodeWithCompatibleImage, metadataLinodeWithCompatibleImageAndRegion, + linodeInEdgeRegion, ...onlineLinodes, linodeWithEligibleVolumes, ...offlineLinodes, diff --git a/packages/manager/src/utilities/regions.test.ts b/packages/manager/src/utilities/regions.test.ts index 99eee287b3d..c705afefcc4 100644 --- a/packages/manager/src/utilities/regions.test.ts +++ b/packages/manager/src/utilities/regions.test.ts @@ -13,6 +13,7 @@ describe('getRegionsByRegionId', () => { maximum_pgs_per_customer: 5, maximum_vms_per_pg: 10, resolvers: { ipv4: '', ipv6: '' }, + site_type: 'core', status: 'ok', }, { @@ -23,6 +24,7 @@ describe('getRegionsByRegionId', () => { maximum_pgs_per_customer: 5, maximum_vms_per_pg: 10, resolvers: { ipv4: '', ipv6: '' }, + site_type: 'core', status: 'ok', }, ]; @@ -36,6 +38,7 @@ describe('getRegionsByRegionId', () => { maximum_pgs_per_customer: 5, maximum_vms_per_pg: 10, resolvers: { ipv4: '', ipv6: '' }, + site_type: 'core', status: 'ok', }, 'us-southeast': { @@ -46,6 +49,7 @@ describe('getRegionsByRegionId', () => { maximum_pgs_per_customer: 5, maximum_vms_per_pg: 10, resolvers: { ipv4: '', ipv6: '' }, + site_type: 'core', status: 'ok', }, };