Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Made <RegionSelect /> more dynamic #8996

Merged
merged 10 commits into from
Apr 14, 2023
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

## [Unreleased]

### Changed:
- `<RegionSelect />` can now dynamically get country flag and group all countrys #8996

### Fixed:
- Clear the Kubernetes Delete Dialog when it is re-opened #9000

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const _SingleValue: React.FC<CombinedProps> = (props) => {
{props.children}
</components.SingleValue>
<span className={`${props.data.className} ${classes.icon}`}>
{props.data.flag && props.data.flag()}
{props.data.flag}
</span>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const useStyles = makeStyles((theme: Theme) => ({
}));

export interface RegionItem extends Item<string> {
flag: () => JSX.Element | null;
flag: JSX.Element | null;
country: string;
disabledMessage?: string | JSX.Element;
}
Expand Down Expand Up @@ -61,7 +61,7 @@ export const RegionOption: React.FC<CombinedProps> = (props) => {
justifyContent="flex-start"
spacing={2}
>
<Grid className="py0">{data.flag && data.flag()}</Grid>
<Grid className="py0">{data.flag}</Grid>
<Grid>{label} (Not available)</Grid>
</Grid>
</Tooltip>
Expand All @@ -73,7 +73,7 @@ export const RegionOption: React.FC<CombinedProps> = (props) => {
justifyContent="flex-start"
spacing={2}
>
<Grid className="py0">{data.flag && data.flag()}</Grid>
<Grid className="py0">{data.flag}</Grid>
<Grid>{label}</Grid>
</Grid>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import { regions } from 'src/__data__/regionsData';
import { getRegionOptions, getSelectedRegionById } from './RegionSelect';

const fakeRegion = { ...regions[0], country: 'NZ' };
const fakeRegion = { ...regions[0], country: 'fake iso code' };

describe('SelectRegionPanel', () => {
describe('helper functions', () => {
describe('getRegionOptions', () => {
it('should return a list of items grouped by country', () => {
const groupedRegions = getRegionOptions(regions);
const [r1, r2, r3] = groupedRegions;
expect(groupedRegions).toHaveLength(3);
expect(r1.options).toHaveLength(5);
expect(r2.options).toHaveLength(2);
expect(r3.options).toHaveLength(4);
});
describe('Region Select helper functions', () => {
describe('getRegionOptions', () => {
it('should return a list of items grouped by continent', () => {
const groupedRegions = getRegionOptions(regions);
const [r1, r2, r3] = groupedRegions;
expect(groupedRegions).toHaveLength(8);
expect(r1.options).toHaveLength(5);
expect(r2.options).toHaveLength(2);
expect(r3.options).toHaveLength(3);
});

it('should group unrecognized regions as Other', () => {
const groupedRegions = getRegionOptions([fakeRegion]);
expect(groupedRegions).toHaveLength(1);
expect(groupedRegions[0]).toHaveProperty('label', 'Other');
});
it('should group unrecognized regions as Other', () => {
const groupedRegions = getRegionOptions([fakeRegion]);
expect(
groupedRegions.find((group) => group.label === 'Other')
).toBeDefined();
});
});

describe('getSelectedRegionById', () => {
it('should return the matching Item from a list of GroupedItems', () => {
const groupedRegions = getRegionOptions(regions);
const selectedID = regions[1].id;
expect(
getSelectedRegionById(selectedID, groupedRegions)
).toHaveProperty('value', regions[1].id);
});
describe('getSelectedRegionById', () => {
it('should return the matching Item from a list of GroupedItems', () => {
const groupedRegions = getRegionOptions(regions);
const selectedID = regions[1].id;
expect(getSelectedRegionById(selectedID, groupedRegions)).toHaveProperty(
'value',
regions[1].id
);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { Region } from '@linode/api-v4/lib/regions';
import AU from 'flag-icons/flags/4x3/au.svg';
import CA from 'flag-icons/flags/4x3/ca.svg';
import DE from 'flag-icons/flags/4x3/de.svg';
import UK from 'flag-icons/flags/4x3/gb.svg';
import IN from 'flag-icons/flags/4x3/in.svg';
import JP from 'flag-icons/flags/4x3/jp.svg';
import SG from 'flag-icons/flags/4x3/sg.svg';
import US from 'flag-icons/flags/4x3/us.svg';
import { groupBy } from 'ramda';
import * as React from 'react';
import { makeStyles } from '@mui/styles';
import { Theme } from '@mui/material/styles';
import SingleValue from 'src/components/EnhancedSelect/components/SingleValue';
import Select, {
BaseSelectProps,
GroupType,
} from 'src/components/EnhancedSelect/Select';
import * as React from 'react';
import SingleValue from 'src/components/EnhancedSelect/components/SingleValue';
import { Region } from '@linode/api-v4/lib/regions';
import RegionOption, { RegionItem } from './RegionOption';
import { Flag } from 'src/components/Flag';
import {
CONTINENT_CODE_TO_CONTINENT,
COUNTRY_CODE_TO_CONTINENT_CODE,
ContinentNames,
} from './utils';

interface Props extends Omit<BaseSelectProps, 'onChange'> {
regions: Region[];
Expand All @@ -29,83 +24,48 @@ interface Props extends Omit<BaseSelectProps, 'onChange'> {
width?: number;
}

export const flags = {
au: () => <AU width="32" height="24" viewBox="0 0 640 480" />,
us: () => <US width="32" height="24" viewBox="0 0 640 480" />,
sg: () => <SG id="singapore" width="32" height="24" viewBox="0 0 640 480" />,
jp: () => (
<JP
id="japan"
width="32"
height="24"
viewBox="0 0 640 480"
style={{ backgroundColor: '#fff' }}
/>
),
uk: () => <UK width="32" height="24" viewBox="0 0 640 480" />,
eu: () => <UK width="32" height="24" viewBox="0 0 640 480" />,
de: () => <DE width="32" height="24" viewBox="0 0 640 480" />,
ca: () => <CA width="32" height="24" viewBox="0 0 640 480" />,
in: () => <IN width="32" height="24" viewBox="0 0 640 480" />,
};

export const selectStyles = {
menuList: (base: any) => ({ ...base, maxHeight: `40vh !important` }),
};
const useStyles = makeStyles((theme: Theme) => ({
root: {
'& svg': {
'& g': {
// Super hacky fix for Firefox rendering of some flag icons that had a clip-path property.
clipPath: 'none !important' as 'none',
},
},
'& #singapore, #japan': {
border: `1px solid ${theme.color.border3}`,
},
},
}));

type RegionGroup = ContinentNames | 'Other';

export const getRegionOptions = (regions: Region[]) => {
const groupedRegions = groupBy<Region>((thisRegion) => {
if (thisRegion.country.match(/(us|ca)/)) {
return 'North America';
}
if (thisRegion.country.match(/(de|uk|eu)/)) {
return 'Europe';
}
if (thisRegion.country.match(/(jp|sg|in|au)/)) {
return 'Asia Pacific';
}
return 'Other';
}, regions);
return ['North America', 'Europe', 'Asia Pacific', 'Other'].reduce(
(accum, thisGroup) => {
if (
!groupedRegions[thisGroup] ||
groupedRegions[thisGroup].length === 0
) {
return accum;
}
return [
...accum,
{
label: thisGroup,
options: groupedRegions[thisGroup]
.map((thisRegion) => ({
label: `${thisRegion.label} (${thisRegion.id})`,
value: thisRegion.id,
flag:
flags[thisRegion.country.toLocaleLowerCase()] ?? (() => null),
country: thisRegion.country,
}))
const groups: Record<RegionGroup, RegionItem[]> = {
'North America': [],
Europe: [],
Asia: [],
'South America': [],
Oceania: [],
Africa: [],
Antartica: [],
Other: [],
};

.sort(sortRegions),
},
for (const region of regions) {
Copy link
Contributor

Choose a reason for hiding this comment

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

not a blocker at all but using reduce could be nicer? something like that

regions.reduce((acc, region) => {
    const country = region.country.toUpperCase();
  
    const continentCode = COUNTRY_CODE_TO_CONTINENT_CODE[
      country as keyof typeof COUNTRY_CODE_TO_CONTINENT_CODE
    ];
  
    const group = continentCode
      ? CONTINENT_CODE_TO_CONTINENT[continentCode] ?? 'Other'
      : 'Other';
  
    if (!acc[group]) {
      acc[group] = [];
    }
  
    acc[group].push({
      label: `${region.label} (${region.id})`,
      value: region.id,
      flag: <Flag country={region.country} />,
      country: region.country,
    });
  
    return acc;
  }, {});

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I'll stick with the for loop for the readability and I think the time complexity is about the same

const country = region.country.toUpperCase();

const continentCode =
COUNTRY_CODE_TO_CONTINENT_CODE[
country as keyof typeof COUNTRY_CODE_TO_CONTINENT_CODE
];
},
[]
);

const group = continentCode
? CONTINENT_CODE_TO_CONTINENT[continentCode] ?? 'Other'
: 'Other';

groups[group].push({
label: `${region.label} (${region.id})`,
value: region.id,
flag: <Flag country={region.country} />,
country: region.country,
});
}

return Object.keys(groups).map((group: RegionGroup) => ({
label: group,
options: groups[group].sort(sortRegions),
}));
};

export const getSelectedRegionById = (
Expand Down Expand Up @@ -137,7 +97,7 @@ const sortRegions = (region1: RegionItem, region2: RegionItem) => {
return 0;
};

const SelectRegionPanel: React.FC<Props> = (props) => {
export const RegionSelect = React.memo((props: Props) => {
const {
label,
disabled,
Expand Down Expand Up @@ -168,12 +128,10 @@ const SelectRegionPanel: React.FC<Props> = (props) => {
[handleSelection]
);

const classes = useStyles();

const options = React.useMemo(() => getRegionOptions(regions), [regions]);

return (
<div className={classes.root} style={{ width }}>
<div style={{ width }}>
<Select
isClearable={Boolean(isClearable)} // Defaults to false if the prop isn't provided
value={getSelectedRegionById(selectedID || '', options) ?? ''}
Expand All @@ -195,6 +153,4 @@ const SelectRegionPanel: React.FC<Props> = (props) => {
/>
</div>
);
};

export default React.memo(SelectRegionPanel);
});
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { flags, default } from './RegionSelect';
export { RegionSelect } from './RegionSelect';
Loading