Skip to content

Commit

Permalink
upcoming: [M3-7618] - Delete Placement Group Modal (linode#10162)
Browse files Browse the repository at this point in the history
* Initial commit: save work

* Wrap up comment and add test

* Cleanup

* Error handling

* Cleanup and more error handling

* Add linode list

* Add unassign logic

* error handling

* Test

* Restore initial mock data

* Cleanup

* Test and story for changes in removable selection list

* Added changeset: Add Delete Placement Group Modal

* Invalidate related linode when removed from PG

* Feedback
  • Loading branch information
abailly-akamai authored Feb 16, 2024
1 parent 455f09b commit 42137a0
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add Delete Placement Group Modal ([#10162](https://github.com/linode/manager/pull/10162))
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,52 @@ export const CustomHeightAndWidth: Story = {
),
};

/**
* Example of a RemovableSelectionsList with no data to remove
*/
export const WithReadableRemoveCTA: Story = {
render: () => {
const SpecifiedLabelWrapper = () => {
const [data, setData] = React.useState(diffLabelListItems);

const handleRemove = (item: RemovableItem) => {
setData([...data].filter((data) => data.id !== item.id));
};

const resetList = () => {
setData([...diffLabelListItems]);
};

return (
<>
<RemovableSelectionsList
RemoveButton={() => (
<Button
sx={(theme) => ({
fontFamily: theme.font.normal,
fontSize: '0.875rem',
})}
variant="text"
>
Remove
</Button>
)}
headerText="Linodes to remove"
noDataText="No Linodes available"
onRemove={handleRemove}
selectionData={data}
/>
<Button onClick={resetList} sx={{ marginTop: 2 }}>
Reset list
</Button>
</>
);
};

return <SpecifiedLabelWrapper />;
},
};

/**
* Example of a RemovableSelectionsList with no data to remove
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { Button } from '../Button/Button';
import { RemovableSelectionsList } from './RemovableSelectionsList';

const defaultList = Array.from({ length: 5 }, (_, index) => {
Expand Down Expand Up @@ -89,4 +90,15 @@ describe('Removable Selections List', () => {
const removeButton = screen.queryByLabelText(`remove my-linode-1`);
expect(removeButton).not.toBeInTheDocument();
});

it('should render the remove button as text when removeButtonText is declared', () => {
const { queryAllByText } = renderWithTheme(
<RemovableSelectionsList
{...props}
RemoveButton={() => <Button>Remove Linode</Button>}
isRemovable
/>
);
expect(queryAllByText('Remove Linode')).toHaveLength(5);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from './RemovableSelectionsList.style';

import type { SxProps, Theme } from '@mui/material';
import type { ButtonProps } from 'src/components/Button/Button';

export type RemovableItem = {
id: number;
Expand All @@ -29,6 +30,11 @@ export interface RemovableSelectionsListProps {
* The custom label component
*/
LabelComponent?: React.ComponentType<{ selection: RemovableItem }>;
/**
* Overrides the render of the X Button
* Has no effect if isRemovable is false
*/
RemoveButton?: (props: ButtonProps) => JSX.Element;
/**
* The descriptive text to display above the list
*/
Expand Down Expand Up @@ -78,6 +84,7 @@ export const RemovableSelectionsList = (
) => {
const {
LabelComponent,
RemoveButton,
headerText,
id,
isRemovable = true,
Expand Down Expand Up @@ -115,9 +122,9 @@ export const RemovableSelectionsList = (
>
<StyledScrollBox maxHeight={maxHeight} maxWidth={maxWidth}>
<SelectedOptionsList
data-qa-selection-list
isRemovable={isRemovable}
ref={listRef}
data-qa-selection-list
>
{selectionData.map((selection) => (
<SelectedOptionsListItem alignItems="center" key={selection.id}>
Expand All @@ -130,20 +137,23 @@ export const RemovableSelectionsList = (
selection.label
)}
</StyledLabel>
{isRemovable && (
<IconButton
aria-label={`remove ${
preferredDataLabel
? selection[preferredDataLabel]
: selection.label
}`}
disableRipple
onClick={() => handleOnClick(selection)}
size="medium"
>
<Close />
</IconButton>
)}
{isRemovable &&
(RemoveButton ? (
<RemoveButton onClick={() => handleOnClick(selection)} />
) : (
<IconButton
aria-label={`remove ${
preferredDataLabel
? selection[preferredDataLabel]
: selection.label
}`}
disableRipple
onClick={() => handleOnClick(selection)}
size="medium"
>
<Close />
</IconButton>
))}
</SelectedOptionsListItem>
))}
</SelectedOptionsList>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface EntityInfo {
| 'Linode'
| 'Load Balancer'
| 'NodeBalancer'
| 'Placement Group'
| 'Subnet'
| 'VPC'
| 'Volume';
Expand All @@ -49,6 +50,7 @@ export const TypeToConfirmDialog = (props: CombinedProps) => {
children,
entity,
errors,
inputProps,
label,
loading,
onClick,
Expand Down Expand Up @@ -120,6 +122,7 @@ export const TypeToConfirmDialog = (props: CombinedProps) => {
data-testid={'dialog-confirm-text-input'}
expand
hideInstructions={entity.subType === 'CloseAccount'}
inputProps={inputProps}
label={label}
placeholder={entity.subType === 'CloseAccount' ? 'Username' : ''}
textFieldStyle={textFieldStyle}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { fireEvent } from '@testing-library/react';
import * as React from 'react';

import { linodeFactory, placementGroupFactory } from 'src/factories';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { PlacementGroupsDeleteModal } from './PlacementGroupsDeleteModal';

const queryMocks = vi.hoisted(() => ({
useAllLinodesQuery: vi.fn().mockReturnValue({}),
useDeletePlacementGroup: vi.fn().mockReturnValue({
mutateAsync: vi.fn().mockResolvedValue({}),
reset: vi.fn(),
}),
useParams: vi.fn().mockReturnValue({}),
usePlacementGroupQuery: vi.fn().mockReturnValue({}),
}));

vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useParams: queryMocks.useParams,
};
});

vi.mock('src/queries/placementGroups', async () => {
const actual = await vi.importActual('src/queries/placementGroups');
return {
...actual,
useDeletePlacementGroup: queryMocks.useDeletePlacementGroup,
usePlacementGroupQuery: queryMocks.usePlacementGroupQuery,
};
});

vi.mock('src/queries/linodes/linodes', async () => {
const actual = await vi.importActual('src/queries/linodes/linodes');
return {
...actual,
useAllLinodesQuery: queryMocks.useAllLinodesQuery,
};
});

const props = {
onClose: vi.fn(),
open: true,
};

describe('PlacementGroupsDeleteModal', () => {
beforeAll(() => {
queryMocks.useParams.mockReturnValue({
id: '1',
});
queryMocks.useAllLinodesQuery.mockReturnValue({
data: [
linodeFactory.build({
id: 1,
label: 'test-linode',
}),
],
});
});

it('should render the right form elements', () => {
queryMocks.usePlacementGroupQuery.mockReturnValue({
data: placementGroupFactory.build({
affinity_type: 'anti_affinity',
id: 1,
label: 'PG-to-delete',
linode_ids: [1],
}),
});

const { getByRole, getByTestId, getByText } = renderWithTheme(
<PlacementGroupsDeleteModal {...props} />
);

expect(
getByRole('heading', {
name: 'Delete Placement Group PG-to-delete (Anti-affinity)',
})
).toBeInTheDocument();
expect(
getByText(
'Linodes assigned to Placement Group PG-to-delete (Anti-affinity)'
)
).toBeInTheDocument();
expect(getByTestId('assigned-linodes')).toContainElement(
getByText('test-linode')
);
expect(getByTestId('textfield-input')).toBeDisabled();
expect(getByRole('button', { name: 'Close' })).toBeInTheDocument();
expect(getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(getByRole('button', { name: 'Delete' })).toBeDisabled();
});

it("should be enabled when there's no assigned linodes", () => {
queryMocks.usePlacementGroupQuery.mockReturnValue({
data: placementGroupFactory.build({
affinity_type: 'anti_affinity',
id: 1,
label: 'PG-to-delete',
linode_ids: [],
}),
});
const { getByRole, getByTestId, getByText } = renderWithTheme(
<PlacementGroupsDeleteModal {...props} />
);

expect(getByText('No Linodes assigned to this Placement Group.'));

const textField = getByTestId('textfield-input');
const deleteButton = getByRole('button', { name: 'Delete' });

expect(textField).toBeEnabled();
expect(deleteButton).toBeDisabled();

fireEvent.change(textField, { target: { value: 'PG-to-delete' } });

expect(deleteButton).toBeEnabled();
fireEvent.click(deleteButton);

expect(queryMocks.useDeletePlacementGroup).toHaveBeenCalled();
});
});
Loading

0 comments on commit 42137a0

Please sign in to comment.