upcoming: [UIE-9746] - Implement Remove Lock Modal.#13348
upcoming: [UIE-9746] - Implement Remove Lock Modal.#13348tanushree-akamai wants to merge 6 commits intolinode:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements the Remove Lock modal functionality, enabling users to unlock Linodes from the action menu. The implementation includes UI components, integration with the Linode action menu, and comprehensive unit test coverage.
Changes:
- Created RemoveLockDialog component with lock type-specific messaging
- Integrated unlock functionality into Linode action menus (landing and detail pages)
- Added unit tests for RemoveLockDialog and LinodeActionMenu lock/unlock actions
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| RemoveLockDialog.tsx | New dialog component for removing locks with API integration |
| RemoveLockDialog.test.tsx | Comprehensive unit tests for the remove lock dialog |
| LinodeActionMenu.tsx | Updated to trigger remove lock dialog and renamed locks prop |
| LinodeActionMenu.test.tsx | Added tests for lock/unlock action visibility and permissions |
| LinodesLanding.tsx | Added state management and dialog integration for remove lock |
| ListView.tsx | Threaded remove lock handler through to LinodeRow components |
| CardView.tsx | Added remove lock handler to card view rendering |
| DisplayLinodes.tsx | Added openRemoveLockDialog prop to interface definitions |
| DisplayGroupedLinodes.tsx | Added openRemoveLockDialog prop to interface definitions |
| LinodeRow.tsx | Renamed locks prop to linodeLocks for consistency |
| LinodeRow.test.tsx | Added onOpenRemoveLockDialog to test props |
| LinodeDetailHeader.tsx | Added remove lock dialog state and integration |
| LinodeEntityDetailHeader.tsx | Added linodeLocks prop to header interface |
| LinodeEntityDetail.tsx | Passed linodeLocks to entity detail header |
| EntityHeader.stories.tsx | Added linodeLocks prop to storybook examples |
| EntityDetail.stories.tsx | Added onOpenRemoveLockDialog to storybook examples |
| .changeset file | Added changeset entry for the feature |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const locksResponse = await getLocks( | ||
| {}, | ||
| { | ||
| '+and': [{ 'entity.id': linodeId }, { 'entity.type': 'linode' }], | ||
| } | ||
| ); |
There was a problem hiding this comment.
The error message 'No lock found for this Linode.' is ambiguous when getLocks returns an empty array. This could mean the lock was already removed, or there was never a lock. Consider a more specific error message like 'No active lock found. It may have already been removed.' to better inform users.
| } | ||
| ); | ||
|
|
||
| const lock = locksResponse.data[0]; |
There was a problem hiding this comment.
Assuming only the first lock needs to be deleted when multiple locks might exist. If a Linode can have multiple locks, this implementation will only remove the first one. Consider either removing all locks returned by the query or documenting why only the first lock is targeted.
There was a problem hiding this comment.
Agreed.
Long term, should we delete all locks on the Linode? Or is this just temporary untill we let the user select which lock to delete.
const deleteLockRequests = locksResponse.data.map((lock) =>
deleteLock(lock.id)
);
await Promise.all(deleteLockRequests);There was a problem hiding this comment.
If (and when) we have multiple locks - we would enhance the UI to explicitly ask for which lock users would like to remove. I have added inline for clarity: ed7bebd
| } | ||
| ); | ||
|
|
||
| const lock = locksResponse.data[0]; |
There was a problem hiding this comment.
Agreed.
Long term, should we delete all locks on the Linode? Or is this just temporary untill we let the user select which lock to delete.
const deleteLockRequests = locksResponse.data.map((lock) =>
deleteLock(lock.id)
);
await Promise.all(deleteLockRequests);| } | ||
| }, [open]); | ||
|
|
||
| const handleSubmit = async () => { |
There was a problem hiding this comment.
Rather than managing isLoading and error manually, we could use React Query to simplify things
For example:
import { useMutation } from '@tanstack/react-query';
const { mutate, isPending, error } = useMutation<{}, APIError[]>({
mutationFn: async () => {
const locksResponse = await getLocks(
{},
{
'entity.id': linodeId,
'entity.type': 'linode',
}
);
const deleteLockRequests = locksResponse.data.map((lock) =>
deleteLock(lock.id)
);
await Promise.all(deleteLockRequests);
return {};
},
onSuccess() {
enqueueSnackbar(`Lock removed from ${linodeLabel}.`, {
variant: 'success',
});
onClose();
},
});| queryMocks.useFlags.mockReturnValue({ | ||
| resourceLock: { linodes: false }, | ||
| }); | ||
|
|
||
| const { getByLabelText, queryByText } = renderWithTheme( | ||
| <LinodeActionMenu {...props} /> | ||
| ); |
There was a problem hiding this comment.
Rather than mocking the use src/hooks/useFlags module, we should use our buit in flags helper if possible
| queryMocks.useFlags.mockReturnValue({ | |
| resourceLock: { linodes: false }, | |
| }); | |
| const { getByLabelText, queryByText } = renderWithTheme( | |
| <LinodeActionMenu {...props} /> | |
| ); | |
| const { getByLabelText, getByText } = renderWithTheme( | |
| <LinodeActionMenu {...props} linodeLocks={[]} />, | |
| { flags: { resourceLock: { linodes: true } }} | |
| ); |
packages/manager/src/features/Linodes/LinodesDetail/LinodeLock/RemoveLockDialog.test.tsx
Outdated
Show resolved
Hide resolved
ed7bebd to
0e9cfa4
Compare
@tanushree-akamai , not sure why they are failing but can you take a look? |
| const locks = locksResponse.data; | ||
|
|
||
| if (locks.length === 0) { | ||
| throw [{ reason: 'No active lock found for this Linode.' }]; |
There was a problem hiding this comment.
Maybe we can use Promise.reject to address the eslint warning here.
| throw [{ reason: 'No active lock found for this Linode.' }]; | |
| return Promise.reject([ | |
| { reason: 'No active lock found for this Linode.' }, | |
| ]); |
|
Hi @tanushree-akamai, I think I found the culprit. When you rebuild a linode from the Linode details page, the request will not include So that's why it failed the tests.
|
7796324 to
1113318
Compare
Cloud Manager UI test results🎉 866 passing tests on test run #9 ↗︎
|

Description 📝
This PR implements the Remove Lock modal accessible from the Linode action menu.
Changes 🔄
List any change(s) relevant to the reviewer.
Scope 🚢
Upon production release, changes in this PR will be visible to:
Preview 📷
Linode Landing behavior:
https://github.com/user-attachments/assets/4c30fd4e-e67b-41f0-8faf-382842b1d40a
Linode Detail behavior:
https://github.com/user-attachments/assets/14378fa7-fd78-4853-8c47-b364f5997657
Remove Modal when lock type is 'cannot_delete'

Remove Modal when lock type is 'cannot_delete_with_subresources'

How to test 🧪
Reproduction steps
[ "create_lock", "delete_lock" ]
Verification steps
Since IAM permissions for Resource Locks are not fully implemented yet, you can temporarily bypass the permission checks to validate the Lock/Unlock behavior with real API data.
Comment the permission checks on line 247 in LinodeActionMenu.tsx:
Author Checklists
As an Author, to speed up the review process, I considered 🤔
👀 Doing a self review
❔ Our contribution guidelines
🤏 Splitting feature into small PRs
➕ Adding a changeset
🧪 Providing/improving test coverage
🔐 Removing all sensitive information from the code and PR description
🚩 Using a feature flag to protect the release
👣 Providing comprehensive reproduction steps
📑 Providing or updating our documentation
🕛 Scheduling a pair reviewing session
📱 Providing mobile support
♿ Providing accessibility support
As an Author, before moving this PR from Draft to Open, I confirmed ✅