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-7842] - Update Assign Linode Drawer and improve query skipping #10263

Merged
merged 7 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Update Assign Linode Drawer and improve query skipping ([#10263](https://github.com/linode/manager/pull/10263))
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,15 @@ describe('PlacementGroupsAssignLinodesDrawer', () => {
error: [{ reason: 'Not found' }],
});

const { getByText } = renderWithTheme(
const { container } = renderWithTheme(
<PlacementGroupsAssignLinodesDrawer
onClose={vi.fn()}
open={true}
selectedPlacementGroup={placementGroupFactory.build()}
/>
);

expect(
getByText(
'There was a problem retrieving your placement group. Please try again'
)
).toBeInTheDocument();
expect(container).toBeEmptyDOMElement;
abailly-akamai marked this conversation as resolved.
Show resolved Hide resolved
});

it('should render the drawer components', () => {
Expand Down Expand Up @@ -131,12 +127,7 @@ describe('PlacementGroupsAssignLinodesDrawer', () => {
})
);

const {
getByPlaceholderText,
getByRole,
getByTestId,
getByText,
} = renderWithTheme(
const { getByPlaceholderText, getByRole, getByText } = renderWithTheme(
<PlacementGroupsAssignLinodesDrawer
selectedPlacementGroup={placementGroupFactory.build({
affinity_type: 'anti_affinity',
Expand All @@ -148,33 +139,23 @@ describe('PlacementGroupsAssignLinodesDrawer', () => {
/>
);

const linodesSelect = getByPlaceholderText('Select a Linode');
const addLinodeButton = getByRole('button', { name: 'Add Linode' });
const removableLinodesList = getByTestId('pg-linode-removable-list');
const linodesSelect = getByPlaceholderText(
'Select Linode or type to search'
);
const assignLinodeButton = getByRole('button', { name: 'Assign Linode' });

expect(linodesSelect).toBeInTheDocument();
expect(addLinodeButton).toHaveAttribute('aria-disabled', 'true');
expect(removableLinodesList).toHaveTextContent(
'No Linodes have been assigned.'
);
expect(assignLinodeButton).toHaveAttribute('aria-disabled', 'true');

fireEvent.focus(linodesSelect);
fireEvent.change(linodesSelect, { target: { value: 'Linode-11' } });
const optionElement = getByText('Linode-11');
fireEvent.click(optionElement);

expect(addLinodeButton).not.toHaveAttribute('aria-disabled', 'true');

fireEvent.click(getByRole('button', { name: 'Add Linode' }));
expect(assignLinodeButton).not.toHaveAttribute('aria-disabled', 'true');

expect(addLinodeButton).toHaveAttribute('aria-disabled', 'true');
expect(removableLinodesList).toHaveTextContent('Linode-11');
fireEvent.click(getByRole('button', { name: 'Assign Linode' }));

const removeButton = getByRole('button', { name: 'remove Linode-11' });
fireEvent.click(removeButton);

expect(removableLinodesList).toHaveTextContent(
'No Linodes have been assigned.'
);
expect(assignLinodeButton).toHaveAttribute('aria-disabled', 'true');
});
});
abailly-akamai marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,31 @@ import Grid from '@mui/material/Unstable_Grid2';
import * as React from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Box } from 'src/components/Box';
import { Divider } from 'src/components/Divider';
import { Drawer } from 'src/components/Drawer';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { FormLabel } from 'src/components/FormLabel';
import { Link } from 'src/components/Link';
import { Notice } from 'src/components/Notice/Notice';
import { RemovableSelectionsList } from 'src/components/RemovableSelectionsList/RemovableSelectionsList';
import { Stack } from 'src/components/Stack';
import { TooltipIcon } from 'src/components/TooltipIcon';
import { Typography } from 'src/components/Typography';
import { usePlacementGroupData } from 'src/hooks/usePlacementGroupsData';
import { useAllLinodesQuery } from 'src/queries/linodes/linodes';
import {
useAssignLinodesToPlacementGroup,
useUnassignLinodesFromPlacementGroup,
useUnpaginatedPlacementGroupsQuery,
} from 'src/queries/placementGroups';

import { LinodeSelect } from '../Linodes/LinodeSelect/LinodeSelect';
import { getLinodesFromAllPlacementGroups } from './utils';
import {
getAffinityEnforcement,
getLinodesFromAllPlacementGroups,
} from './utils';

import type { PlacementGroupsAssignLinodesDrawerProps } from './types';
import type {
AssignLinodesToPlacementGroupPayload,
Linode,
UnassignLinodesFromPlacementGroupPayload,
} from '@linode/api-v4';

export const PlacementGroupsAssignLinodesDrawer = (
Expand All @@ -41,94 +42,70 @@ export const PlacementGroupsAssignLinodesDrawer = (
region: selectedPlacementGroup?.region,
abailly-akamai marked this conversation as resolved.
Show resolved Hide resolved
},
],
}
},
open
);
const {
data: allPlacementGroups,
error: allPlacementGroupsError,
} = useUnpaginatedPlacementGroupsQuery();

// We display a notice and disable inputs in case the user reaches this drawer somehow
// (not supposed to happen as the "Assign Linode to Placement Group" button should be disabled
const { hasReachedCapacity, region } = usePlacementGroupData({
placementGroup: selectedPlacementGroup,
});

const { mutateAsync: assignLinodes } = useAssignLinodesToPlacementGroup(
selectedPlacementGroup?.id ?? -1
);
const { mutateAsync: unassignLinodes } = useUnassignLinodesFromPlacementGroup(
selectedPlacementGroup?.id ?? -1
);
const [selectedLinode, setSelectedLinode] = React.useState<Linode | null>(
null
);
const [localLinodesSelection, setLocalLinodesSelection] = React.useState<
Linode[]
>([]);
const [generalError, setGeneralError] = React.useState<string | undefined>(
undefined
);

React.useEffect(() => {
if (open) {
setSelectedLinode(null);
setLocalLinodesSelection([]);
setGeneralError(undefined);
}
}, [open]);
const handleResetForm = () => {
setSelectedLinode(null);
setGeneralError(undefined);
};

const handleDrawerClose = () => {
onClose();
handleResetForm();
};

const linodesFromAllPlacementGroups = getLinodesFromAllPlacementGroups(
allPlacementGroups
);

const getLinodeSelectOptions = (): Linode[] => {
// We filter out Linodes that are already assigned to a Placement Group
return (
allLinodesInRegion?.filter((linode) => {
return !linodesFromAllPlacementGroups.includes(linode.id);
}) ?? []
);
abailly-akamai marked this conversation as resolved.
Show resolved Hide resolved
};

if (
!allLinodesInRegion ||
!selectedPlacementGroup ||
linodesError ||
allPlacementGroupsError
) {
return (
<ErrorState errorText="There was a problem retrieving your placement group. Please try again" />
);
return null;
}

const getLinodeSelectOptions = (): Linode[] => {
return (
allLinodesInRegion.filter((linode) => {
const isNotAlreadyAssigned = !linodesFromAllPlacementGroups.includes(
linode.id
);
const isNotAssignedInDrawer = !localLinodesSelection.find(
(l) => l.id === linode.id
);

return isNotAlreadyAssigned && isNotAssignedInDrawer;
}) ?? []
);
};

const { affinity_type, label } = selectedPlacementGroup;
const linodeSelectLabel = region
? `Linodes in ${region.label} (${region.id})`
: 'Linodes';

const drawerTitle =
label && affinity_type
? `Add Linodes to Placement Group ${label} (${AFFINITY_TYPES[affinity_type]})`
: 'Add Linodes to Placement Group';

const removableSelectionsListLabel = (
<>
<FormLabel htmlFor="pg-linode-removable-list">
{label && affinity_type
? `Linodes Assigned to Placement Group ${label}
(${AFFINITY_TYPES[affinity_type]})`
: 'Linodes Assigned to Placement Group'}
</FormLabel>
<Typography component="span" display="block" fontSize="0.8rem">
Maximum Number of Linodes for this group: {region?.maximum_vms_per_pg}
</Typography>
</>
);
? `Assign Linodes to Placement Group ${label} (${AFFINITY_TYPES[affinity_type]})`
: 'Assign Linodes to Placement Group';

const handleAssignLinode = async (e: React.SyntheticEvent<HTMLElement>) => {
e.preventDefault();
Expand All @@ -138,107 +115,79 @@ export const PlacementGroupsAssignLinodesDrawer = (
return;
}

setLocalLinodesSelection([...localLinodesSelection, selectedLinode]);

// Since we allow only one Linode to be assigned to a Placement Group,
// We're using a tuple with the selected Linode ID
const payload: AssignLinodesToPlacementGroupPayload = {
linodes: [selectedLinode.id],
};

try {
await assignLinodes(payload);
handleDrawerClose();
} catch (error) {
setGeneralError(
error?.[0]?.reason
? error[0].reason
: 'An error occurred while adding the Linode to the group'
);
}
};

const handleUnassignLinode = async (linode: Linode) => {
setLocalLinodesSelection(
localLinodesSelection.filter((l) => l.id !== linode.id)
);

const payload: UnassignLinodesFromPlacementGroupPayload = {
linodes: [linode.id],
};

try {
await unassignLinodes(payload);
} catch (error) {
setGeneralError(
error
? error?.[0]?.reason
: 'An error occurred while removing the Linode from the group'
: 'An error occurred while assigning the Linode to the group'
);
}
};

return (
<Drawer onClose={onClose} open={open} title={drawerTitle}>
<Drawer onClose={handleDrawerClose} open={open} title={drawerTitle}>
<Grid>
abailly-akamai marked this conversation as resolved.
Show resolved Hide resolved
{generalError ? <Notice text={generalError} variant="error" /> : null}
<Typography my={4}>
<strong>Affinity Enforcement: </strong>
{getAffinityEnforcement(selectedPlacementGroup.is_strict)}
</Typography>
<Divider sx={{ mb: 4 }} />
<form onSubmit={handleAssignLinode}>
<Stack spacing={1}>
{hasReachedCapacity && open && (
<Notice
text="This Placement Group has the maximum number of Linodes allowed"
variant="error"
/>
)}
<Typography>
A Linode can only be assigned to a single Placement Group.
</Typography>

<Typography>
If you need to add a new Linode, go to{' '}
If you need to create a new Linode, go to{' '}
<Link to="/linodes/create">Create Linode</Link> and return to this
page to assign it to this Placement Group.
</Typography>
<LinodeSelect
onSelectionChange={(value) => {
setSelectedLinode(value);
}}
disabled={hasReachedCapacity}
helperText="Only displaying Linodes that aren’t assigned to a Placement Group"
label={linodeSelectLabel}
options={getLinodeSelectOptions()}
value={selectedLinode?.id ?? null}
/>
<Box sx={{ alignItems: 'flex-end', display: 'flex' }}>
<LinodeSelect
onSelectionChange={(value) => {
setSelectedLinode(value);
}}
disabled={hasReachedCapacity}
label={linodeSelectLabel}
options={getLinodeSelectOptions()}
placeholder="Select Linode or type to search"
sx={{ flexGrow: 1 }}
value={selectedLinode?.id ?? null}
/>
<TooltipIcon
placement="right"
status="help"
sxTooltipIcon={{ position: 'relative', top: 4 }}
text="Only displaying Linodes that aren’t assigned to a Placement Group"
/>
</Box>

<ActionsPanel
primaryButtonProps={{
'data-testid': 'submit',
disabled: !selectedLinode || hasReachedCapacity,
label: 'Add Linode',
label: 'Assign Linode',
type: 'submit',
}}
sx={{ pt: 2 }}
/>
{hasReachedCapacity && (
<Notice
text="This Placement Group has the maximum number of Linodes allowed"
variant="warning"
/>
)}
<RemovableSelectionsList
LabelComponent={({ selection }) => {
return (
<Typography component="span">{selection.label}</Typography>
);
}}
onRemove={(data: Linode) => {
handleUnassignLinode(data);
}}
headerText={removableSelectionsListLabel}
id="pg-linode-removable-list"
noDataText={'No Linodes have been assigned.'}
selectionData={localLinodesSelection}
sx={{ pt: 2 }}
/>
<ActionsPanel
primaryButtonProps={{
buttonType: 'outlined',
label: 'Done',
onClick: onClose,
}}
sx={{ pt: 2 }}
/>
</Stack>
</form>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ interface Props {
export const PlacementGroupsDeleteModal = (props: Props) => {
const { onClose, open } = props;
const { id } = useParams<{ id: string }>();
const { data: selectedPlacementGroup } = usePlacementGroupQuery(+id);
const { data: selectedPlacementGroup } = usePlacementGroupQuery(
+id,
Boolean(id)
);
const {
assignedLinodes,
isLoading: placementGroupDataLoading,
Expand Down
Loading
Loading