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

upcoming: [M3-7993] - PlacementGroups Select optimizations & cleanup #10455

Merged
merged 10 commits into from
May 14, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

PlacementGroups Select optimizations & cleanup ([#10455](https://github.com/linode/manager/pull/10455))
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('Tags list', () => {
value: ['tag1', 'tag2'].map((tag) => ({ label: tag, value: tag })),
}}
handlePlacementGroupChange={handlePlacementGroupChange}
selectedPlacementGroupId={null}
/>
);

Expand Down Expand Up @@ -53,6 +54,7 @@ describe('Tags list', () => {
})),
}}
handlePlacementGroupChange={handlePlacementGroupChange}
selectedPlacementGroupId={null}
/>
);

Expand All @@ -69,6 +71,7 @@ describe('Tags list', () => {
value: '',
}}
handlePlacementGroupChange={handlePlacementGroupChange}
selectedPlacementGroupId={null}
/>
);
expect(queryByLabelText(TAG_LABEL)).not.toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import type { PlacementGroup } from '@linode/api-v4';

interface DetailsPanelProps {
error?: string;
handlePlacementGroupChange: (selected: PlacementGroup) => void;
handlePlacementGroupChange: (selected: PlacementGroup | null) => void;
labelFieldProps?: TextFieldProps;
selectedPlacementGroupId: null | number;
selectedRegionId?: string;
tagsInputProps?: TagsInputProps;
}
Expand All @@ -24,6 +25,7 @@ export const DetailsPanel = (props: DetailsPanelProps) => {
error,
handlePlacementGroupChange,
labelFieldProps,
selectedPlacementGroupId,
selectedRegionId,
tagsInputProps,
} = props;
Expand Down Expand Up @@ -62,6 +64,7 @@ export const DetailsPanel = (props: DetailsPanelProps) => {
{isPlacementGroupsEnabled && (
<PlacementGroupsDetailPanel
handlePlacementGroupChange={handlePlacementGroupChange}
selectedPlacementGroupId={selectedPlacementGroupId}
selectedRegionId={selectedRegionId}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ import { PlacementGroupsSelect } from './PlacementGroupsSelect';
import type { PlacementGroupsSelectProps } from './PlacementGroupsSelect';

const props: PlacementGroupsSelectProps = {
errorText: '',
handlePlacementGroupChange: vi.fn(),
id: '',
label: 'Placement Groups in Atlanta, GA (us-southeast)',
noOptionsMessage: '',
selectedPlacementGroup: null,
selectedPlacementGroupId: null,
selectedRegion: regionFactory.build({ id: 'us-southeast' }),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,52 @@ import type { PlacementGroup, Region } from '@linode/api-v4';
import type { SxProps } from '@mui/system';

export interface PlacementGroupsSelectProps {
clearOnBlur?: boolean;
clearable?: boolean;
defaultValue?: PlacementGroup;
/**
* If true, the component will be disabled.
*/
disabled?: boolean;
errorText?: string;
handlePlacementGroupChange: (selected: PlacementGroup) => void;
id?: string;
/**
* A callback to execute when the selected Placement Group changes.
* The selection is handled by a parent component.
*/
handlePlacementGroupChange: (selected: PlacementGroup | null) => void;
/**
* The label for the TextField component.
*/
label: string;
/**
* If true, the component will display a loading spinner. (usually when fetching data)
*/
loading?: boolean;
/**
* The message to display when there are no options available.
*/
noOptionsMessage?: string;
onBlur?: (e: React.FocusEvent) => void;
selectedPlacementGroup: PlacementGroup | null;
selectedRegion?: Region;
/**
* The ID of the selected Placement Group.
*/
selectedPlacementGroupId: null | number;
/**
* We want to pass the full region object here so we can check if the selected Placement Group is at capacity.
*/
selectedRegion: Region | undefined;
/**
* Any additional styles to apply to the root element.
*/
sx?: SxProps;
/**
* Any additional props to pass to the TextField component.
*/
textFieldProps?: Partial<TextFieldProps>;
}

export const PlacementGroupsSelect = (props: PlacementGroupsSelectProps) => {
const {
clearOnBlur,
clearable = true,
defaultValue,
disabled,
errorText,
handlePlacementGroupChange,
id,
label,
loading,
noOptionsMessage,
onBlur,
selectedPlacementGroup,
selectedPlacementGroupId,
selectedRegion,
sx,
...textFieldProps
Expand All @@ -51,8 +66,15 @@ export const PlacementGroupsSelect = (props: PlacementGroupsSelectProps) => {
const {
data: placementGroups,
error,
isFetching,
isLoading,
} = useAllPlacementGroupsQuery(Boolean(selectedRegion?.id));
} = useAllPlacementGroupsQuery({
enabled: Boolean(selectedRegion?.id),
// Placement Group selection is always dependent on a selected region.
filter: {
region: selectedRegion?.id,
},
});

const isDisabledPlacementGroup = (
selectedPlacementGroup: PlacementGroup,
Expand All @@ -68,26 +90,18 @@ export const PlacementGroupsSelect = (props: PlacementGroupsSelectProps) => {
});
};

if (!placementGroups) {
return null;
}

const placementGroupsOptions: PlacementGroup[] = placementGroups.filter(
(placementGroup) => placementGroup.region === selectedRegion?.id
);

const selection =
placementGroupsOptions.find(
(placementGroup) => placementGroup.id === selectedPlacementGroup?.id
placementGroups?.find(
(placementGroup) => placementGroup.id === selectedPlacementGroupId
) ?? null;

return (
<Autocomplete
noOptionsText={
noOptionsMessage ?? getDefaultNoOptionsMessage(error, isLoading)
}
onChange={(_, selectedOption: PlacementGroup) => {
handlePlacementGroupChange(selectedOption);
onChange={(_, selectedOption) => {
handlePlacementGroupChange(selectedOption ?? null);
}}
renderOption={(props, option, { selected }) => {
return (
Expand All @@ -101,18 +115,14 @@ export const PlacementGroupsSelect = (props: PlacementGroupsSelectProps) => {
/>
);
}}
clearOnBlur={clearOnBlur}
clearOnBlur={true}
data-testid="placement-groups-select"
defaultValue={defaultValue}
disableClearable={!clearable}
disabled={Boolean(!selectedRegion?.id) || disabled}
errorText={errorText}
errorText={error?.[0]?.reason}
getOptionLabel={(placementGroup: PlacementGroup) => placementGroup.label}
id={id}
label={label}
loading={isLoading || loading}
onBlur={onBlur}
options={placementGroupsOptions ?? []}
loading={isFetching}
options={placementGroups ?? []}
placeholder="None"
sx={sx}
value={selection}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ export const PlacementGroupPanel = () => {
return (
<PlacementGroupsDetailPanel
handlePlacementGroupChange={(placementGroup) =>
field.onChange(placementGroup.id)
field.onChange(placementGroup?.id)
}
selectedPlacementGroupId={field.value ?? null}
selectedRegionId={regionId}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export interface LinodeCreateProps {
handleAgreementChange: () => void;
handleFirewallChange: (firewallId: number) => void;
handleIPv4RangesForVPC: (ranges: ExtendedIP[]) => void;
handlePlacementGroupChange: (placementGroup: PlacementGroup) => void;
handlePlacementGroupChange: (placementGroup: PlacementGroup | null) => void;
handleShowApiAwarenessModal: () => void;
handleSubmitForm: HandleSubmit;
handleSubnetChange: (subnetId: number) => void;
Expand Down Expand Up @@ -656,6 +656,9 @@ export class LinodeCreate extends React.PureComponent<
onChange: (e) => updateLabel(e.target.value),
value: label || '',
}}
selectedPlacementGroupId={
this.props.placementGroupSelection?.id ?? null
}
tagsInputProps={
this.props.createType !== 'fromLinode'
? tagsInputProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ class LinodeCreateContainer extends React.PureComponent<CombinedProps, State> {
imageDisplayInfo={this.getImageInfo()}
ipamAddress={this.state.vlanIPAMAddress}
label={this.generateLabel()}
placementGroupSelection={this.state.placementGroupSelection}
regionDisplayInfo={this.getRegionInfo()}
regionsData={regionsData}
resetCreationState={this.clearCreationState}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { RegionSelect } from 'src/components/RegionSelect/RegionSelect';
import { sxEdgeIcon } from 'src/components/RegionSelect/RegionSelect.styles';
import { TooltipIcon } from 'src/components/TooltipIcon';
import { Typography } from 'src/components/Typography';
import { NO_PLACEMENT_GROUPS_IN_SELECTED_REGION_MESSAGE } from 'src/features/PlacementGroups/constants';
import { useIsPlacementGroupsEnabled } from 'src/features/PlacementGroups/utils';
import { useFlags } from 'src/hooks/useFlags';
import { useRegionsQuery } from 'src/queries/regions/regions';
Expand Down Expand Up @@ -208,8 +209,8 @@ export const ConfigureForm = React.memo((props: Props) => {
disabled={isPlacementGroupSelectDisabled}
key={selectedRegion}
label={placementGroupSelectLabel}
noOptionsMessage="There are no Placement Groups in this region."
selectedPlacementGroup={selectedPlacementGroup}
noOptionsMessage={NO_PLACEMENT_GROUPS_IN_SELECTED_REGION_MESSAGE}
selectedPlacementGroupId={selectedPlacementGroup?.id ?? null}
selectedRegion={newRegion}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const PlacementGroupsAssignLinodesDrawer = (
const {
data: allPlacementGroups,
error: allPlacementGroupsError,
} = useAllPlacementGroupsQuery();
} = useAllPlacementGroupsQuery({});
const { enqueueSnackbar } = useSnackbar();

// We display a notice and disable inputs in case the user reaches this drawer somehow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ export const PlacementGroupsCreateDrawer = (
selectedRegionId,
} = props;
const { data: regions } = useRegionsQuery();
const { data: allPlacementGroups } = useAllPlacementGroupsQuery();
const { data: allPlacementGroupsInRegion } = useAllPlacementGroupsQuery({
enabled: Boolean(selectedRegionId),
filter: {
region: selectedRegionId,
},
});
const { error, mutateAsync } = useCreatePlacementGroup();
const { enqueueSnackbar } = useSnackbar();
const {
Expand Down Expand Up @@ -183,7 +188,7 @@ export const PlacementGroupsCreateDrawer = (
handleDisabledRegion={(region) => {
const isRegionAtCapacity = hasRegionReachedPlacementGroupCapacity(
{
allPlacementGroups,
allPlacementGroups: allPlacementGroupsInRegion,
region,
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { PlacementGroupsDetailPanel } from './PlacementGroupsDetailPanel';

const defaultProps = {
handlePlacementGroupChange: vi.fn(),
selectedPlacementGroupId: null,
};

const queryMocks = vi.hoisted(() => ({
Expand Down
Loading
Loading