diff --git a/packages/manager/.changeset/pr-13330-fixed-1769594156861.md b/packages/manager/.changeset/pr-13330-fixed-1769594156861.md new file mode 100644 index 00000000000..5d0a036dcd1 --- /dev/null +++ b/packages/manager/.changeset/pr-13330-fixed-1769594156861.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +IAM: bugs related to resseting to prev page ([#13330](https://github.com/linode/manager/pull/13330)) diff --git a/packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/AssignedEntitiesTable.tsx b/packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/AssignedEntitiesTable.tsx index fa29f2cee59..5c6b58d004e 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/AssignedEntitiesTable.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/AssignedEntitiesTable.tsx @@ -184,7 +184,7 @@ export const AssignedEntitiesTable = ({ username }: Props) => { // If we just deleted the last one on a page, reset to the previous page. const removedLastOnPage = filteredAndSortedRoles.length % pagination.pageSize === 1; - if (removedLastOnPage) { + if (removedLastOnPage && pagination.page > 1) { pagination.handlePageChange(pagination.page - 1); } }; @@ -373,13 +373,15 @@ export const AssignedEntitiesTable = ({ username }: Props) => { handleDialogClose(drawerMode)} + onClose={() => setIsChangeRoleForEntityDrawerOpen(false)} + onSuccess={() => handleDialogClose()} open={isChangeRoleForEntityDrawerOpen} role={selectedRole} username={username} /> handleDialogClose()} + onClose={() => setIsRemoveAssignmentDialogOpen(false)} + onSuccess={() => handleDialogClose()} open={isRemoveAssignmentDialogOpen} role={selectedRole} username={username} diff --git a/packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/ChangeRoleForEntityDrawer.tsx b/packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/ChangeRoleForEntityDrawer.tsx index 3d643b19297..3a3105c6cd3 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/ChangeRoleForEntityDrawer.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedEntitiesTable/ChangeRoleForEntityDrawer.tsx @@ -39,6 +39,7 @@ import type { ExtendedEntityRole } from '../utilities'; interface Props { mode: DrawerModes; onClose: () => void; + onSuccess?: () => void; open: boolean; role: EntitiesRole | undefined; username?: string; @@ -47,6 +48,7 @@ interface Props { export const ChangeRoleForEntityDrawer = ({ mode, onClose, + onSuccess, open, role, username, @@ -159,6 +161,7 @@ export const ChangeRoleForEntityDrawer = ({ variant: 'success', }); + onSuccess?.(); handleClose(); } catch (errors) { for (const error of errors) { diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx index 338422e9542..aff917a2140 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx @@ -167,20 +167,49 @@ export const AssignedRolesTable = () => { }; /** - * Closes the appropriate assignment-related dialog and adjusts pagination if needed. - * - * @param drawerMode Optional mode indicating which dialog should be closed. + * Closes the Unassign role dialog and adjusts pagination if needed. */ - const handleDialogClose = (drawerMode?: DrawerModes) => { - if (drawerMode && drawerMode === 'change-role') { - setIsChangeRoleDrawerOpen(false); - } else { - setIsUnassignRoleDialogOpen(false); + const handleUnassignRoleDialogClose = () => { + setIsUnassignRoleDialogOpen(false); + + if (hadOneRoleOnPage && pagination.page > 1) { + pagination.handlePageChange(pagination.page - 1); + } + }; + + /** + * Closes the Change role dialog and adjusts pagination if needed. + */ + const handleChangeRoleDialogClose = (newTotalRolesCount: number) => { + setIsChangeRoleDrawerOpen(false); + + // Check if the change decreased the total roles by exactly one. + // This occurs when switching to a role the user already had, + // causing the two roles to merge into a single entry. + const decreasedByOne = + filteredAndSortedRoles.length - newTotalRolesCount === 1; + + if (hadOneRoleOnPage && decreasedByOne && pagination.page > 1) { + pagination.handlePageChange(pagination.page - 1); } - // If we just deleted the last one on a page, reset to the previous page. - const removedLastOnPage = - filteredAndSortedRoles.length % pagination.pageSize === 1; - if (removedLastOnPage) { + }; + + /** + * Closes the Remove Assignment Confirmation Dialog and adjusts pagination if needed. + * @param selectedRole Role that was affected by the removal. + */ + const handleRemoveAssignmentDialogClose = ( + selectedRole?: ExtendedRoleView + ) => { + setIsRemoveAssignmentDialogOpen(false); + + // If we just deleted the last role with only one entity on a page, reset to the previous page. + if ( + selectedRole && + selectedRole.entity_ids?.length === 1 && + hadOneRoleOnPage && + pagination.page > 1 + ) { pagination.handlePageChange(pagination.page - 1); } }; @@ -346,6 +375,13 @@ export const AssignedRolesTable = () => { return filteredAndSortedRoles.length; }, [filteredAndSortedRoles]); + // Detect when the current page has exactly one role. + // If a delete or unassign removes it, the page becomes empty, + // so navigate back one page to avoid an empty view. + const hadOneRoleOnPage = React.useMemo(() => { + return filteredAndSortedRoles.length % pagination.pageSize === 1; + }, [filteredAndSortedRoles.length, pagination.pageSize]); + if (accountPermissionsLoading || entitiesLoading || assignedRolesLoading) { return ; } @@ -461,12 +497,16 @@ export const AssignedRolesTable = () => { /> handleDialogClose(drawerMode)} + onClose={() => setIsChangeRoleDrawerOpen(false)} + onSuccess={(newTotalRolesCount: number) => + handleChangeRoleDialogClose(newTotalRolesCount) + } open={isChangeRoleDrawerOpen} role={selectedRole} /> handleDialogClose()} + onClose={() => setIsUnassignRoleDialogOpen(false)} + onSuccess={() => handleUnassignRoleDialogClose()} open={isUnassignRoleDialogOpen} role={selectedRole} /> @@ -477,6 +517,7 @@ export const AssignedRolesTable = () => { /> setIsRemoveAssignmentDialogOpen(false)} + onSuccess={() => handleRemoveAssignmentDialogClose(selectedRole)} open={isRemoveAssignmentDialogOpen} role={selectedRoleDetails} username={username} diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx index 78836d51596..b35e153003b 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx @@ -30,6 +30,7 @@ import { isAccountRole, isEntityRole, } from '../utilities'; +import { combineRoles } from './utils'; import type { DrawerModes, EntitiesOption, ExtendedRoleView } from '../types'; import type { RolesType } from '../utilities'; @@ -37,11 +38,18 @@ import type { RolesType } from '../utilities'; interface Props { mode: DrawerModes; onClose: () => void; + onSuccess?: (nextTotalCount: number) => void; open: boolean; role: ExtendedRoleView | undefined; } -export const ChangeRoleDrawer = ({ mode, onClose, open, role }: Props) => { +export const ChangeRoleDrawer = ({ + mode, + onClose, + onSuccess, + open, + role, +}: Props) => { const theme = useTheme(); const { username } = useParams({ strict: false }); const { data: accountRoles, isLoading: accountPermissionsLoading } = @@ -147,6 +155,12 @@ export const ChangeRoleDrawer = ({ mode, onClose, open, role }: Props) => { await mutationFn(updatedUserRoles); + // Calculate the next total roles count after the change. + // Passed to the parent to determine pagination updates + // (e.g., navigate back if the current page becomes empty). + const nextTotalRolesCount = combineRoles(updatedUserRoles).length; + + onSuccess?.(nextTotalRolesCount); handleClose(); } catch (errors) { setError('root', {