Repository: opendatahub-io/odh-dashboard
Pull request #4197: Permissions Created Outside Dashboard Bug
Author: login: katieperry4
Labels: name: lgtm
name: approved
Created: 2025-05-12T19:51:52Z | Merged: 2025-05-16T20:25:55Z

Closes [RHOAIENG-926](https://issues.redhat.com/browse/RHOAIENG-926)

## Description
<!--- Describe your changes in detail; the what, the why, any findings, etc -->
<!--- Include any screenshots of changed UI; Include any gifs if it was a flow / UX change -->
Permissions created in in openshift didn't have the dashboard labels:
```
    opendatahub.io/dashboard: 'true'
    opendatahub.io/project-sharing: 'true'
```
so they were being filtered out and not shown in the dashboard so I:
- removed the filter for dashboard label 
- added an alert at the top of the permissions page
- added a util to check if the user is about to edit/delete their own permissions
- added a modal to confirm they really want that change
- added unit and cypress tests
- removed logic for preventing repeat permissions
- edited the edit logic to allow for patching instead of create/delete

Before:
![old](https://github.com/user-attachments/assets/93cdff50-de25-4db2-abb1-30c368886549)

Now: (Now it includes your own permissions like mine, katperry and an alert at the top of the page)
<img width="1141" alt="Screenshot 2025-05-14 at 9 40 06 AM" src="https://github.com/user-attachments/assets/752fa19f-b7eb-4c35-bfc4-822b90d43163" />
<img width="1201" alt="Screenshot 2025-05-15 at 11 31 57 AM" src="https://github.com/user-attachments/assets/dde2b88a-5d8e-48d4-afac-727cd96f8b3f" />


Upon hitting delete on your own permissions:
![Screenshot 2025-05-16 at 1 08 22 PM](https://github.com/user-attachments/assets/18c2a28f-34c0-4224-9146-dd3e7c1a5b7a)




Upon hitting save after trying to edit your own permissions:
![Screenshot 2025-05-16 at 12 57 20 PM](https://github.com/user-attachments/assets/33495ed6-84f4-40f9-90db-2dba45bfaeb9)



and Model Registry permissions is working too:
<img width="1197" alt="Screenshot 2025-05-15 at 11 32 49 AM" src="https://github.com/user-attachments/assets/2d3ed7bd-d7f7-4e28-b296-18f168c7cdbb" />
<img width="1176" alt="Screenshot 2025-05-15 at 11 33 01 AM" src="https://github.com/user-attachments/assets/272315f4-dd51-4fab-9a3f-5a38a95c041d" />


## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
tested locally and with `npm run test`
It seems like a lot of lines changed in `RoleBindingPermissionsTableRow` but really it just got wrapped in `<>...</>` the only changes were related to the modal in some `onClicks` and whatnot. The diff just looks a little wild, it might be easier just to look at the two files side-by-side.

I also ran the e2e tests on Jenkins, some test failures unrelated to the changes made in this pr. Some tests in `testProjectAdminPermissions` failed but pass when run locally -> tested again on Jenkins with another cluster and those tests are passing.

If you want to test this:
- Make a new project or go to an existing one
- You'll notice many more permissions there than existed before
- Specifically you'll see your own permissions there
- Delete them -> it won't work the modal will show up
- Type in your username and the delete button will enable
- You can actually hit delete if you want or just hit cancel
- Try to edit your permissions
- Hit the checkmark to save
- Modal should show
- Also check out the same concepts in the model registry permissions area

## Test Impact
<!--- What tests have you done to cover the implemented functionality -->
<!--- If tests are not applicable, explain why here -->
added unit and cypress tests

## Request review criteria:
<!--- This PR will be merged by any repository approver when it meets all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->

Self checklist (all need to be checked):
- [x] The developer has manually tested the changes and verified that the changes work
- [x] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
- [x] The developer has added tests or explained why testing cannot be added (unit or cypress tests for related changes)

If you have UI changes: 
<!--- You can ignore these if you are doing manifest, backend, internal logic, etc changes; aka non-UI / visual changes -->
- [x] Included any necessary screenshots or gifs if it was a UI change.
- [ ] Included tags to the UX team if it was a UI/UX change.

After the PR is posted & before it merges:
- [ ] The developer has tested their solution on a cluster by using the image produced by the PR to `main`



Diff:
diff --git a/frontend/src/__tests__/cypress/cypress/pages/permissions.ts b/frontend/src/__tests__/cypress/cypress/pages/permissions.ts
index da5672f623..baa579bda8 100644
--- a/frontend/src/__tests__/cypress/cypress/pages/permissions.ts
+++ b/frontend/src/__tests__/cypress/cypress/pages/permissions.ts
@@ -1,5 +1,6 @@
 import { Contextual } from './components/Contextual';
 import { TableRow } from './components/table';
+import { DeleteModal } from './components/DeleteModal';
 
 class PermissionsTableRow extends TableRow {}
 
@@ -31,6 +32,24 @@ class PermissionsTab {
   }
 }
 
+class RoleBindingPermissionsChangeModal extends DeleteModal {
+  findPermissionsChangeModal() {
+    return cy.findByTestId('delete-modal');
+  }
+
+  findModalCancelButton() {
+    return cy.get('button').contains('Cancel');
+  }
+
+  findModalInput() {
+    return cy.findByTestId('delete-modal-input');
+  }
+
+  findModalConfirmButton(action: string) {
+    return cy.get('button').contains(action);
+  }
+}
+
 class PermissionTable extends Contextual<HTMLElement> {
   findRows() {
     return this.find().find(`[data-label=Username]`);
@@ -92,3 +111,4 @@ class PermissionTable extends Contextual<HTMLElement> {
 }
 
 export const permissions = new PermissionsTab();
+export const roleBindingPermissionsChangeModal = new RoleBindingPermissionsChangeModal();
diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/tabs/permissions.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/tabs/permissions.cy.ts
index 9276fe502a..d8ca8d6414 100644
--- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/tabs/permissions.cy.ts
+++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/tabs/permissions.cy.ts
@@ -1,7 +1,10 @@
 import { mockK8sResourceList, mockProjectK8sResource } from '~/__mocks__';
 import { mock200Status } from '~/__mocks__/mockK8sStatus';
 import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource';
-import { permissions } from '~/__tests__/cypress/cypress/pages/permissions';
+import {
+  permissions,
+  roleBindingPermissionsChangeModal,
+} from '~/__tests__/cypress/cypress/pages/permissions';
 import { be } from '~/__tests__/cypress/cypress/utils/should';
 import { ProjectModel, RoleBindingModel } from '~/__tests__/cypress/cypress/utils/models';
 import type { RoleBindingSubject } from '~/k8sTypes';
@@ -13,6 +16,11 @@ const userSubjects: RoleBindingSubject[] = [
     apiGroup: 'rbac.authorization.k8s.io',
     name: 'user-1',
   },
+  {
+    kind: 'User',
+    apiGroup: 'rbac.authorization.k8s.io',
+    name: 'test-user',
+  },
 ];
 
 const groupSubjects: RoleBindingSubject[] = [
@@ -40,7 +48,12 @@ const initIntercepts = ({ isEmpty = false }: HandlersProps) => {
         : [
             mockRoleBindingK8sResource({
               name: 'user-1',
-              subjects: userSubjects,
+              subjects: [userSubjects[0]],
+              roleRefName: 'edit',
+            }),
+            mockRoleBindingK8sResource({
+              name: 'test-user',
+              subjects: [userSubjects[1]],
               roleRefName: 'edit',
             }),
             mockRoleBindingK8sResource({
@@ -172,6 +185,64 @@ describe('Permissions tab', () => {
 
       cy.wait('@deleteUser');
     });
+
+    it('Shows confirmation modal when editing own permissions', () => {
+      initIntercepts({ isEmpty: false });
+      permissions.visit('test-project');
+
+      userTable.getTableRow('test-user').findKebabAction('Edit').click();
+      userTable.findEditInput('test-user').clear().type('test-user');
+      userTable.selectPermission('test-user', 'Admin Edit the project and manage user access');
+      userTable.findEditSaveButton('test-user').click();
+
+      roleBindingPermissionsChangeModal.findPermissionsChangeModal().should('exist');
+      roleBindingPermissionsChangeModal.findModalInput().should('exist').type('test-user');
+      roleBindingPermissionsChangeModal.findModalConfirmButton('Save').should('not.be.disabled');
+      roleBindingPermissionsChangeModal.findModalCancelButton().click();
+    });
+
+    it('Shows confirmation modal when deleting own permissions', () => {
+      cy.interceptK8s(
+        'DELETE',
+        { model: RoleBindingModel, ns: 'test-project', name: 'test-user' },
+        mock200Status({}),
+      ).as('deleteUser');
+      initIntercepts({ isEmpty: false });
+      permissions.visit('test-project');
+
+      userTable.getTableRow('test-user').findKebabAction('Delete').click();
+
+      roleBindingPermissionsChangeModal.findPermissionsChangeModal().should('exist');
+      roleBindingPermissionsChangeModal.findModalInput().should('exist').type('test-user');
+      roleBindingPermissionsChangeModal.findModalConfirmButton('Delete').should('not.be.disabled');
+      roleBindingPermissionsChangeModal.findModalCancelButton().click();
+    });
+
+    it('Does not show confirmation modal when editing other users permissions', () => {
+      initIntercepts({ isEmpty: false });
+      permissions.visit('test-project');
+
+      userTable.getTableRow('user-1').findKebabAction('Edit').click();
+      userTable.findEditInput('user-1').clear().type('user-3');
+      userTable.selectPermission('user-3', 'Admin Edit the project and manage user access');
+      userTable.findEditSaveButton('user-3').click();
+
+      roleBindingPermissionsChangeModal.findPermissionsChangeModal().should('not.exist');
+    });
+
+    it('Does not show confirmation modal when deleting other users permissions', () => {
+      cy.interceptK8s(
+        'DELETE',
+        { model: RoleBindingModel, ns: 'test-project', name: 'user-1' },
+        mock200Status({}),
+      ).as('deleteUser');
+      initIntercepts({ isEmpty: false });
+      permissions.visit('test-project');
+
+      userTable.getTableRow('user-1').findKebabAction('Delete').click();
+      roleBindingPermissionsChangeModal.findPermissionsChangeModal().should('not.exist');
+      cy.wait('@deleteUser');
+    });
   });
 
   describe('Groups table', () => {
diff --git a/frontend/src/api/k8s/__tests__/roleBindings.spec.ts b/frontend/src/api/k8s/__tests__/roleBindings.spec.ts
index 9e2b2beaf5..d44f2e7fdb 100644
--- a/frontend/src/api/k8s/__tests__/roleBindings.spec.ts
+++ b/frontend/src/api/k8s/__tests__/roleBindings.spec.ts
@@ -18,6 +18,7 @@ import {
   getRoleBinding,
   listRoleBindings,
   patchRoleBindingOwnerRef,
+  patchRoleBindingSubjects,
 } from '~/api/k8s/roleBindings';
 import { RoleBindingModel } from '~/api/models/k8s';
 import {
@@ -356,3 +357,31 @@ describe('patchRoleBindingOwnerRef', () => {
     expect(result).toStrictEqual(roleBindingMock);
   });
 });
+
+describe('patchRoleBindingSubjects', () => {
+  it('should patch role binding subjects', async () => {
+    const newSubjects: RoleBindingSubject[] = [
+      {
+        apiGroup: 'rbac.authorization.k8s.io',
+        kind: 'User',
+        name: 'test-user',
+      },
+    ];
+    k8sPatchResourceMock.mockResolvedValue(roleBindingMock);
+    const result = await patchRoleBindingSubjects('rbName', namespace, newSubjects);
+    expect(k8sPatchResourceMock).toHaveBeenCalledWith({
+      fetchOptions: { requestInit: {} },
+      model: RoleBindingModel,
+      patches: [
+        {
+          op: 'replace',
+          path: '/subjects',
+          value: newSubjects,
+        },
+      ],
+      queryOptions: { name: 'rbName', ns: namespace, queryParams: {} },
+    });
+    expect(k8sPatchResourceMock).toHaveBeenCalledTimes(1);
+    expect(result).toStrictEqual(roleBindingMock);
+  });
+});
diff --git a/frontend/src/api/k8s/roleBindings.ts b/frontend/src/api/k8s/roleBindings.ts
index e9d1147c92..a691becabf 100644
--- a/frontend/src/api/k8s/roleBindings.ts
+++ b/frontend/src/api/k8s/roleBindings.ts
@@ -150,3 +150,26 @@ export const patchRoleBindingOwnerRef = (
       opts,
     ),
   );
+
+export const patchRoleBindingSubjects = (
+  rbName: string,
+  namespace: string,
+  subjects: RoleBindingSubject[],
+  opts?: K8sAPIOptions,
+): Promise<RoleBindingKind> =>
+  k8sPatchResource<RoleBindingKind>(
+    applyK8sAPIOptions(
+      {
+        model: RoleBindingModel,
+        queryOptions: { name: rbName, ns: namespace },
+        patches: [
+          {
+            op: 'replace',
+            path: '/subjects',
+            value: subjects,
+          },
+        ],
+      },
+      opts,
+    ),
+  );
diff --git a/frontend/src/concepts/roleBinding/RoleBindingPermissions.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissions.tsx
index 0b292e106b..03ccf11460 100644
--- a/frontend/src/concepts/roleBinding/RoleBindingPermissions.tsx
+++ b/frontend/src/concepts/roleBinding/RoleBindingPermissions.tsx
@@ -1,5 +1,6 @@
 import * as React from 'react';
 import {
+  Alert,
   EmptyState,
   EmptyStateBody,
   EmptyStateVariant,
@@ -133,6 +134,9 @@ const RoleBindingPermissions: React.FC<RoleBindingPermissionsProps> = ({
       id={ProjectSectionID.PERMISSIONS}
     >
       <Stack hasGutter>
+        <Alert variant="warning" title="Warning" isInline>
+          Changing user or group permissions may remove their access to this resource.
+        </Alert>
         <StackItem>{description}</StackItem>
         <StackItem>{isGroupFirst ? groupTable : userTable}</StackItem>
         <StackItem>{isGroupFirst ? userTable : groupTable}</StackItem>
diff --git a/frontend/src/concepts/roleBinding/RoleBindingPermissionsChangeModal.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsChangeModal.tsx
new file mode 100644
index 0000000000..f8bd02bcde
--- /dev/null
+++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsChangeModal.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react';
+import DeleteModal from '~/pages/projects/components/DeleteModal';
+
+type RoleBindingPermissionsChangeModalProps = {
+  onClose: () => void;
+  onEdit: () => void;
+  onDelete: () => void;
+  isDeleting: boolean;
+  roleName?: string;
+};
+
+const RoleBindingPermissionsChangeModal: React.FC<RoleBindingPermissionsChangeModalProps> = ({
+  onClose,
+  onEdit,
+  onDelete,
+  isDeleting,
+  roleName,
+}) => {
+  const textToShow = isDeleting ? 'Delete' : 'Edit';
+  const [submitted, setSubmitted] = React.useState(false);
+  return (
+    <DeleteModal
+      title={`Confirm ${textToShow.toLowerCase()}`}
+      onClose={onClose}
+      deleting={submitted}
+      onDelete={() => {
+        setSubmitted(true);
+        if (isDeleting) {
+          onDelete();
+        } else {
+          onEdit();
+        }
+      }}
+      deleteName={roleName || 'delete'}
+      submitButtonLabel={isDeleting ? 'Delete' : 'Save'}
+      genericLabel
+    >
+      Are you sure you want to {isDeleting ? 'delete' : 'edit'} permissions for{' '}
+      <strong>{roleName || 'this role binding'}</strong>? {isDeleting ? 'Deleting' : 'Editing'}{' '}
+      these permissions may result in loss of access to this resource.
+    </DeleteModal>
+  );
+};
+
+export default RoleBindingPermissionsChangeModal;
diff --git a/frontend/src/concepts/roleBinding/RoleBindingPermissionsTable.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTable.tsx
index 09b035d2ec..0f6fa07652 100644
--- a/frontend/src/concepts/roleBinding/RoleBindingPermissionsTable.tsx
+++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTable.tsx
@@ -6,7 +6,7 @@ import { generateRoleBindingPermissions } from '~/api';
 import RoleBindingPermissionsTableRow from './RoleBindingPermissionsTableRow';
 import { columnsRoleBindingPermissions } from './data';
 import { RoleBindingPermissionsRoleType } from './types';
-import { firstSubject } from './utils';
+import { firstSubject, tryPatchRoleBinding } from './utils';
 
 type RoleBindingPermissionsTableProps = {
   ownerReference?: K8sResourceCommon;
@@ -51,26 +51,12 @@ const RoleBindingPermissionsTable: React.FC<RoleBindingPermissionsTableProps> =
   refresh,
 }) => {
   const [editCell, setEditCell] = React.useState<string[]>([]);
-  const createProjectRoleBinding = (
-    subjectName: string,
+
+  const createProjectRoleBinding = async (
     newRBObject: RoleBindingKind,
     oldRBObject?: RoleBindingKind,
   ) => {
-    const roleType = newRBObject.subjects[0].kind.toLowerCase();
-    const usedNames: string[] = permissions
-      .map((p) => p.subjects[0].name)
-      .filter((name) => isAdding || name !== oldRBObject?.subjects[0].name);
-    const isDuplicateName = usedNames.includes(subjectName);
-    if (isDuplicateName) {
-      // Prevent duplicate role binding
-      onError(
-        <>
-          The {roleType} <strong>{subjectName}</strong> already exists. Edit the {roleType}
-          &apos;s existing permissions, or add a new {roleType} to assign it permissions.
-        </>,
-      );
-      refresh();
-    } else if (isAdding) {
+    if (isAdding) {
       // Add new role binding
       createRoleBinding(newRBObject)
         .then(() => {
@@ -80,27 +66,31 @@ const RoleBindingPermissionsTable: React.FC<RoleBindingPermissionsTableProps> =
         .catch((e) => {
           onError(<>{e}</>);
         });
-    } else {
-      // Edit existing role binding
-      createRoleBinding(newRBObject)
-        .then(() => {
-          if (oldRBObject) {
+    } else if (oldRBObject) {
+      const patchSucceeded = await tryPatchRoleBinding(oldRBObject, newRBObject);
+      if (patchSucceeded) {
+        setEditCell((prev) => prev.filter((cell) => cell !== oldRBObject.metadata.name));
+        onDismissNewRow();
+        refresh();
+      } else {
+        createRoleBinding(newRBObject)
+          .then(() => {
             deleteRoleBinding(oldRBObject.metadata.name, oldRBObject.metadata.namespace)
               .then(() => refresh())
               .catch((e) => {
                 onError(<>{e}</>);
                 setEditCell((prev) => prev.filter((cell) => cell !== oldRBObject.metadata.name));
               });
-          }
-        })
-        .then(() => {
-          onDismissNewRow();
-          refresh();
-        })
-        .catch((e) => {
-          onError(<>{e}</>);
-          setEditCell((prev) => prev.filter((cell) => cell !== oldRBObject?.metadata.name));
-        });
+          })
+          .then(() => {
+            onDismissNewRow();
+            refresh();
+          })
+          .catch((e) => {
+            onError(<>{e}</>);
+            setEditCell((prev) => prev.filter((cell) => cell !== oldRBObject.metadata.name));
+          });
+      }
     }
   };
   return (
@@ -130,7 +120,7 @@ const RoleBindingPermissionsTable: React.FC<RoleBindingPermissionsTableProps> =
                 labels,
                 ownerReference,
               );
-              createProjectRoleBinding(subjectName, newRBObject);
+              createProjectRoleBinding(newRBObject);
             }}
             onCancel={onDismissNewRow}
           />
@@ -159,7 +149,7 @@ const RoleBindingPermissionsTable: React.FC<RoleBindingPermissionsTableProps> =
               labels,
               ownerReference,
             );
-            createProjectRoleBinding(subjectName, newRBObject, rb);
+            createProjectRoleBinding(newRBObject, rb);
             refresh();
           }}
           onDelete={() => {
diff --git a/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx
index 196a83c286..45bfa52491 100644
--- a/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx
+++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx
@@ -22,10 +22,17 @@ import { relativeTime } from '~/utilities/time';
 import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
 import { projectDisplayNameToNamespace } from '~/concepts/projects/utils';
 import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton';
-import { castRoleBindingPermissionsRoleType, firstSubject, roleLabel } from './utils';
+import { useUser } from '~/redux/selectors';
+import {
+  castRoleBindingPermissionsRoleType,
+  firstSubject,
+  roleLabel,
+  isCurrentUserChanging,
+} from './utils';
 import { RoleBindingPermissionsRoleType } from './types';
 import RoleBindingPermissionsNameInput from './RoleBindingPermissionsNameInput';
 import RoleBindingPermissionsPermissionSelection from './RoleBindingPermissionsPermissionSelection';
+import RoleBindingPermissionsChangeModal from './RoleBindingPermissionsChangeModal';
 
 type RoleBindingPermissionsTableRowProps = {
   roleBindingObject?: RoleBindingKind;
@@ -67,6 +74,8 @@ const RoleBindingPermissionsTableRow: React.FC<RoleBindingPermissionsTableRowPro
   onEdit,
   onDelete,
 }) => {
+  const currentUser = useUser();
+  const isCurrentUserBeingChanged = isCurrentUserChanging(obj, currentUser.username);
   const { projects } = React.useContext(ProjectsContext);
   const [roleBindingName, setRoleBindingName] = React.useState(() => {
     if (isAdding || !obj) {
@@ -84,6 +93,8 @@ const RoleBindingPermissionsTableRow: React.FC<RoleBindingPermissionsTableRowPro
   const [isLoading, setIsLoading] = React.useState(false);
   const createdDate = new Date(obj?.metadata.creationTimestamp ?? '');
   const isDefaultGroup = obj?.metadata.name === defaultRoleBindingName;
+  const [showModal, setShowModal] = React.useState(false);
+  const [isDeleting, setIsDeleting] = React.useState(false);
 
   //Sync local state with props if exiting edit mode
   React.useEffect(() => {
@@ -98,135 +109,177 @@ const RoleBindingPermissionsTableRow: React.FC<RoleBindingPermissionsTableRowPro
   }, [obj, isEditing, isProjectSubject, projects]);
 
   return (
-    <Tbody>
-      <Tr>
-        <Td dataLabel="Username">
-          {isEditing || isAdding ? (
-            <RoleBindingPermissionsNameInput
-              subjectKind={subjectKind}
-              value={roleBindingName}
-              onChange={(selection: React.SetStateAction<string>) => {
-                setRoleBindingName(selection);
-              }}
-              onClear={() => setRoleBindingName('')}
-              placeholderText={isProjectSubject ? 'Select or enter a project' : 'Select a group'}
-              typeAhead={typeAhead}
-              isProjectSubject={isProjectSubject}
-            />
-          ) : (
-            <Content component="p">
-              <Truncate content={roleBindingName} />
-              {` `}
-              {isDefaultGroup && (
-                <Popover
-                  bodyContent={
-                    <div>
-                      This group is created by default. You can add users to this group in OpenShift
-                      user management, or ask the cluster admin to do so.
-                    </div>
-                  }
+    <>
+      <Tbody>
+        <Tr>
+          <Td dataLabel="Username">
+            {isEditing || isAdding ? (
+              <RoleBindingPermissionsNameInput
+                subjectKind={subjectKind}
+                value={roleBindingName}
+                onChange={(selection: React.SetStateAction<string>) => {
+                  setRoleBindingName(selection);
+                }}
+                onClear={() => setRoleBindingName('')}
+                placeholderText={isProjectSubject ? 'Select or enter a project' : 'Select a group'}
+                typeAhead={typeAhead}
+                isProjectSubject={isProjectSubject}
+              />
+            ) : (
+              <Content component="p">
+                <Truncate content={roleBindingName} />
+                {` `}
+                {isDefaultGroup && (
+                  <Popover
+                    bodyContent={
+                      <div>
+                        This group is created by default. You can add users to this group in
+                        OpenShift user management, or ask the cluster admin to do so.
+                      </div>
+                    }
+                  >
+                    <DashboardPopupIconButton
+                      icon={<OutlinedQuestionCircleIcon />}
+                      aria-label="More info"
+                    />
+                  </Popover>
+                )}
+              </Content>
+            )}
+          </Td>
+          <Td dataLabel="Permission">
+            {(isEditing || isAdding) && permissionOptions.length > 1 ? (
+              <RoleBindingPermissionsPermissionSelection
+                permissionOptions={permissionOptions}
+                selection={roleBindingRoleRef}
+                onSelect={(selection) => {
+                  setRoleBindingRoleRef(selection);
+                }}
+              />
+            ) : (
+              <Content component="p">{roleLabel(roleBindingRoleRef)}</Content>
+            )}
+          </Td>
+          <Td dataLabel="Date added">
+            {!isEditing && !isAdding && (
+              <Content component="p">
+                <Timestamp
+                  date={createdDate}
+                  tooltip={{ variant: TimestampTooltipVariant.default }}
                 >
-                  <DashboardPopupIconButton
-                    icon={<OutlinedQuestionCircleIcon />}
-                    aria-label="More info"
+                  {relativeTime(Date.now(), createdDate.getTime())}
+                </Timestamp>
+              </Content>
+            )}
+          </Td>
+          <Td isActionCell modifier="nowrap" style={{ textAlign: 'right' }}>
+            {isEditing || isAdding ? (
+              <Split>
+                <SplitItem>
+                  <Button
+                    data-testid={isAdding ? `save-new-button` : `save-button ${roleBindingName}`}
+                    data-id="save-rolebinding-button"
+                    aria-label="Save role binding"
+                    variant="link"
+                    icon={<CheckIcon />}
+                    isDisabled={isLoading || !roleBindingName || !roleBindingRoleRef}
+                    onClick={() => {
+                      if (isCurrentUserBeingChanged) {
+                        setIsDeleting(false);
+                        setShowModal(true);
+                      } else {
+                        setIsLoading(true);
+                        onChange(
+                          isProjectSubject
+                            ? `system:serviceaccounts:${projectDisplayNameToNamespace(
+                                roleBindingName,
+                                projects,
+                              )}`
+                            : roleBindingName,
+                          roleBindingRoleRef,
+                        );
+                        setIsLoading(false);
+                      }
+                    }}
                   />
-                </Popover>
-              )}
-            </Content>
-          )}
-        </Td>
-        <Td dataLabel="Permission">
-          {(isEditing || isAdding) && permissionOptions.length > 1 ? (
-            <RoleBindingPermissionsPermissionSelection
-              permissionOptions={permissionOptions}
-              selection={roleBindingRoleRef}
-              onSelect={(selection) => {
-                setRoleBindingRoleRef(selection);
-              }}
-            />
-          ) : (
-            <Content component="p">{roleLabel(roleBindingRoleRef)}</Content>
-          )}
-        </Td>
-        <Td dataLabel="Date added">
-          {!isEditing && !isAdding && (
-            <Content component="p">
-              <Timestamp date={createdDate} tooltip={{ variant: TimestampTooltipVariant.default }}>
-                {relativeTime(Date.now(), createdDate.getTime())}
-              </Timestamp>
-            </Content>
-          )}
-        </Td>
-        <Td isActionCell modifier="nowrap" style={{ textAlign: 'right' }}>
-          {isEditing || isAdding ? (
-            <Split>
-              <SplitItem>
-                <Button
-                  data-testid={isAdding ? `save-new-button` : `save-button ${roleBindingName}`}
-                  data-id="save-rolebinding-button"
-                  aria-label="Save role binding"
-                  variant="link"
-                  icon={<CheckIcon />}
-                  isDisabled={isLoading || !roleBindingName || !roleBindingRoleRef}
-                  onClick={() => {
-                    setIsLoading(true);
-                    onChange(
-                      isProjectSubject
-                        ? `system:serviceaccounts:${projectDisplayNameToNamespace(
-                            roleBindingName,
-                            projects,
-                          )}`
-                        : roleBindingName,
-                      roleBindingRoleRef,
-                    );
-                    setIsLoading(false);
-                  }}
-                />
-              </SplitItem>
-              <SplitItem>
+                </SplitItem>
+                <SplitItem>
+                  <Button
+                    data-id="cancel-rolebinding-button"
+                    aria-label="Cancel role binding"
+                    variant="plain"
+                    isDisabled={isLoading}
+                    icon={<TimesIcon />}
+                    onClick={() => {
+                      onCancel();
+                    }}
+                  />
+                </SplitItem>
+              </Split>
+            ) : isDefaultGroup ? (
+              <Tooltip content="The default group cannot be edited or deleted. The group's members can be managed via the API.">
                 <Button
-                  data-id="cancel-rolebinding-button"
-                  aria-label="Cancel role binding"
+                  icon={<EllipsisVIcon />}
                   variant="plain"
-                  isDisabled={isLoading}
-                  icon={<TimesIcon />}
-                  onClick={() => {
-                    onCancel();
-                  }}
+                  isAriaDisabled
+                  aria-label="The default group always has access to model registry."
                 />
-              </SplitItem>
-            </Split>
-          ) : isDefaultGroup ? (
-            <Tooltip content="The default group cannot be edited or deleted. The group’s members can be managed via the API.">
-              <Button
-                icon={<EllipsisVIcon />}
-                variant="plain"
-                isAriaDisabled
-                aria-label="The default group always has access to model registry."
-              />
-            </Tooltip>
-          ) : (
-            <ActionsColumn
-              items={[
-                {
-                  title: 'Edit',
-                  onClick: () => {
-                    onEdit?.();
+              </Tooltip>
+            ) : (
+              <ActionsColumn
+                items={[
+                  {
+                    title: 'Edit',
+                    onClick: () => {
+                      onEdit?.();
+                    },
                   },
-                },
-                { isSeparator: true },
-                {
-                  title: 'Delete',
-                  onClick: () => {
-                    onDelete?.();
+                  { isSeparator: true },
+                  {
+                    title: 'Delete',
+                    onClick: () => {
+                      if (isCurrentUserBeingChanged) {
+                        setIsDeleting(true);
+                        setShowModal(true);
+                      } else {
+                        onDelete?.();
+                      }
+                    },
                   },
-                },
-              ]}
-            />
-          )}
-        </Td>
-      </Tr>
-    </Tbody>
+                ]}
+              />
+            )}
+          </Td>
+        </Tr>
+      </Tbody>
+      {showModal && (
+        <RoleBindingPermissionsChangeModal
+          roleName={currentUser.username}
+          onClose={() => {
+            setShowModal(false);
+            if (isEditing) {
+              onCancel();
+            }
+          }}
+          onEdit={() => {
+            setIsLoading(true);
+            onChange(
+              isProjectSubject
+                ? `system:serviceaccounts:${projectDisplayNameToNamespace(
+                    roleBindingName,
+                    projects,
+                  )}`
+                : roleBindingName,
+              roleBindingRoleRef,
+            );
+            setIsLoading(false);
+            setShowModal(false);
+          }}
+          onDelete={() => onDelete?.()}
+          isDeleting={isDeleting}
+        />
+      )}
+    </>
   );
 };
 
diff --git a/frontend/src/concepts/roleBinding/__tests__/utils.spec.ts b/frontend/src/concepts/roleBinding/__tests__/utils.spec.ts
new file mode 100644
index 0000000000..9fd21465a7
--- /dev/null
+++ b/frontend/src/concepts/roleBinding/__tests__/utils.spec.ts
@@ -0,0 +1,141 @@
+import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource';
+import {
+  castRoleBindingPermissionsRoleType,
+  isCurrentUserChanging,
+  tryPatchRoleBinding,
+} from '~/concepts/roleBinding/utils';
+import { patchRoleBindingSubjects } from '~/api';
+import { RoleBindingPermissionsRoleType } from '~/concepts/roleBinding/types';
+
+// Mock the patchRoleBindingSubjects function
+jest.mock('~/api', () => ({
+  patchRoleBindingSubjects: jest.fn(),
+}));
+
+describe('isCurrentUserChanging', () => {
+  it('should return true when role binding subject matches current username', () => {
+    const roleBinding = mockRoleBindingK8sResource({
+      name: 'test-user',
+      subjects: [{ kind: 'User', apiGroup: 'rbac.authorization.k8s.io', name: 'test-user' }],
+    });
+    expect(isCurrentUserChanging(roleBinding, 'test-user')).toBe(true);
+  });
+
+  it('should return false when role binding subject does not match current username', () => {
+    const roleBinding = mockRoleBindingK8sResource({
+      name: 'other-user',
+      subjects: [{ kind: 'User', apiGroup: 'rbac.authorization.k8s.io', name: 'other-user' }],
+    });
+    expect(isCurrentUserChanging(roleBinding, 'test-user')).toBe(false);
+  });
+
+  it('should return false when role binding is undefined', () => {
+    expect(isCurrentUserChanging(undefined, 'test-user')).toBe(false);
+  });
+});
+
+describe('tryPatchRoleBinding', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should return false when roleRef is different', async () => {
+    const oldRBObject = mockRoleBindingK8sResource({
+      name: 'test-user',
+      subjects: [{ kind: 'User', apiGroup: 'rbac.authorization.k8s.io', name: 'test-user' }],
+      roleRefName: 'view',
+    });
+    const newRBObject = mockRoleBindingK8sResource({
+      name: 'test-user',
+      subjects: [{ kind: 'User', apiGroup: 'rbac.authorization.k8s.io', name: 'test-user' }],
+      roleRefName: 'edit',
+    });
+    const result = await tryPatchRoleBinding(oldRBObject, newRBObject);
+    expect(result).toBe(false);
+    expect(patchRoleBindingSubjects).not.toHaveBeenCalled();
+  });
+
+  it('should return false when dry run fails', async () => {
+    const oldRBObject = mockRoleBindingK8sResource({
+      name: 'test-user',
+      subjects: [{ kind: 'User', apiGroup: 'rbac.authorization.k8s.io', name: 'old-user' }],
+      roleRefName: 'view',
+    });
+    const newRBObject = mockRoleBindingK8sResource({
+      name: 'test-user',
+      subjects: [{ kind: 'User', apiGroup: 'rbac.authorization.k8s.io', name: 'new-user' }],
+      roleRefName: 'view',
+    });
+
+    // Mock the dry run to fail
+    (patchRoleBindingSubjects as jest.Mock).mockRejectedValueOnce(new Error('Dry run failed'));
+
+    const result = await tryPatchRoleBinding(oldRBObject, newRBObject);
+    expect(result).toBe(false);
+    expect(patchRoleBindingSubjects).toHaveBeenCalledTimes(1);
+    expect(patchRoleBindingSubjects).toHaveBeenCalledWith(
+      oldRBObject.metadata.name,
+      oldRBObject.metadata.namespace,
+      newRBObject.subjects,
+      { dryRun: true },
+    );
+  });
+
+  it('should return true when both dry run and patch succeed', async () => {
+    const oldRBObject = mockRoleBindingK8sResource({
+      name: 'test-user',
+      subjects: [{ kind: 'User', name: 'old-user' }],
+      roleRefName: 'view',
+    });
+    const newRBObject = mockRoleBindingK8sResource({
+      name: 'test-user',
+      subjects: [{ kind: 'User', name: 'new-user' }],
+      roleRefName: 'view',
+    });
+
+    // Mock both calls to succeed
+    (patchRoleBindingSubjects as jest.Mock)
+      .mockResolvedValueOnce(undefined) // dry run succeeds
+      .mockResolvedValueOnce(undefined); // actual patch succeeds
+
+    const result = await tryPatchRoleBinding(oldRBObject, newRBObject);
+    expect(result).toBe(true);
+    expect(patchRoleBindingSubjects).toHaveBeenCalledTimes(2);
+    expect(patchRoleBindingSubjects).toHaveBeenNthCalledWith(
+      1,
+      oldRBObject.metadata.name,
+      oldRBObject.metadata.namespace,
+      newRBObject.subjects,
+      { dryRun: true },
+    );
+    expect(patchRoleBindingSubjects).toHaveBeenNthCalledWith(
+      2,
+      oldRBObject.metadata.name,
+      oldRBObject.metadata.namespace,
+      newRBObject.subjects,
+      { dryRun: false },
+    );
+  });
+});
+
+describe('castRoleBindingPermissionsRoleType', () => {
+  it('should return default when role includes registry-user', () => {
+    expect(castRoleBindingPermissionsRoleType('registry-user')).toBe(
+      RoleBindingPermissionsRoleType.DEFAULT,
+    );
+  });
+
+  it('should return admin when role is admin', () => {
+    expect(castRoleBindingPermissionsRoleType('admin')).toBe(RoleBindingPermissionsRoleType.ADMIN);
+  });
+
+  it('should return edit when role is edit', () => {
+    expect(castRoleBindingPermissionsRoleType('edit')).toBe(RoleBindingPermissionsRoleType.EDIT);
+  });
+
+  it('should return custom when role is not admin, edit, or registry-user', () => {
+    expect(castRoleBindingPermissionsRoleType('custom')).toBe(
+      RoleBindingPermissionsRoleType.CUSTOM,
+    );
+  });
+});
diff --git a/frontend/src/concepts/roleBinding/types.ts b/frontend/src/concepts/roleBinding/types.ts
index db9015875d..170d12861a 100644
--- a/frontend/src/concepts/roleBinding/types.ts
+++ b/frontend/src/concepts/roleBinding/types.ts
@@ -9,6 +9,7 @@ export enum RoleBindingPermissionsRoleType {
   EDIT = 'edit',
   ADMIN = 'admin',
   DEFAULT = 'default',
+  CUSTOM = 'custom',
 }
 
 export type RoleBindingSubjectWithRole = RoleBindingSubject & {
diff --git a/frontend/src/concepts/roleBinding/utils.ts b/frontend/src/concepts/roleBinding/utils.ts
index b7ab3a0e9f..35da28b139 100644
--- a/frontend/src/concepts/roleBinding/utils.ts
+++ b/frontend/src/concepts/roleBinding/utils.ts
@@ -1,6 +1,7 @@
 import { capitalize } from '@patternfly/react-core';
 import { ProjectKind, RoleBindingKind } from '~/k8sTypes';
 import { namespaceToProjectDisplayName } from '~/concepts/projects/utils';
+import { patchRoleBindingSubjects } from '~/api';
 import { RoleBindingPermissionsRBType, RoleBindingPermissionsRoleType } from './types';
 
 export const filterRoleBindingSubjects = (
@@ -25,7 +26,10 @@ export const castRoleBindingPermissionsRoleType = (
   if (role === RoleBindingPermissionsRoleType.EDIT) {
     return RoleBindingPermissionsRoleType.EDIT;
   }
-  return RoleBindingPermissionsRoleType.DEFAULT;
+  if (role.includes('registry-user')) {
+    return RoleBindingPermissionsRoleType.DEFAULT;
+  }
+  return RoleBindingPermissionsRoleType.CUSTOM;
 };
 
 export const firstSubject = (
@@ -49,3 +53,44 @@ export const roleLabel = (value: RoleBindingPermissionsRoleType): string => {
 
 export const removePrefix = (roleBindings: RoleBindingKind[]): string[] =>
   roleBindings.map((rb) => rb.subjects[0]?.name.replace(/^system:serviceaccounts:/, ''));
+
+export const isCurrentUserChanging = (
+  roleBinding: RoleBindingKind | undefined,
+  currentUsername: string,
+): boolean => {
+  if (!roleBinding) {
+    return false;
+  }
+  return currentUsername === roleBinding.subjects[0].name;
+};
+
+export const tryPatchRoleBinding = async (
+  oldRBObject: RoleBindingKind,
+  newRBObject: RoleBindingKind,
+): Promise<boolean> => {
+  // Trying to patch roleRef will always fail
+  if (oldRBObject.roleRef.name !== newRBObject.roleRef.name) {
+    return false;
+  }
+  try {
+    await patchRoleBindingSubjects(
+      oldRBObject.metadata.name,
+      oldRBObject.metadata.namespace,
+      newRBObject.subjects,
+      { dryRun: true },
+    );
+  } catch (e) {
+    return false;
+  }
+  try {
+    await patchRoleBindingSubjects(
+      oldRBObject.metadata.name,
+      oldRBObject.metadata.namespace,
+      newRBObject.subjects,
+      { dryRun: false },
+    );
+    return true;
+  } catch {
+    return false;
+  }
+};
diff --git a/frontend/src/pages/projects/components/DeleteModal.tsx b/frontend/src/pages/projects/components/DeleteModal.tsx
index a5227fadf7..b6ffb8de8b 100644
--- a/frontend/src/pages/projects/components/DeleteModal.tsx
+++ b/frontend/src/pages/projects/components/DeleteModal.tsx
@@ -23,6 +23,7 @@ type DeleteModalProps = {
   error?: Error;
   children: React.ReactNode;
   testId?: string;
+  genericLabel?: boolean;
 };
 
 const DeleteModal: React.FC<DeleteModalProps> = ({
@@ -35,6 +36,7 @@ const DeleteModal: React.FC<DeleteModalProps> = ({
   error,
   submitButtonLabel = 'Delete',
   testId,
+  genericLabel,
 }) => {
   const [value, setValue] = React.useState('');
 
@@ -66,7 +68,8 @@ const DeleteModal: React.FC<DeleteModalProps> = ({
           <StackItem>
             <Flex direction={{ default: 'column' }} spaceItems={{ default: 'spaceItemsSm' }}>
               <FlexItem>
-                Type <strong>{deleteNameSanitized}</strong> to confirm deletion:
+                Type <strong>{deleteNameSanitized}</strong> to confirm
+                {genericLabel ? '' : ' deletion'}:
               </FlexItem>
 
               <TextInput
diff --git a/frontend/src/pages/projects/projectSharing/useProjectSharing.ts b/frontend/src/pages/projects/projectSharing/useProjectSharing.ts
index 5135fd8d98..33d1a6b094 100644
--- a/frontend/src/pages/projects/projectSharing/useProjectSharing.ts
+++ b/frontend/src/pages/projects/projectSharing/useProjectSharing.ts
@@ -1,16 +1,12 @@
 import * as React from 'react';
 import { listRoleBindings } from '~/api';
-import { LABEL_SELECTOR_DASHBOARD_RESOURCE, LABEL_SELECTOR_PROJECT_SHARING } from '~/const';
 import { RoleBindingKind } from '~/k8sTypes';
 import useFetchState, { FetchState } from '~/utilities/useFetchState';
 
 const useProjectSharing = (namespace?: string): FetchState<RoleBindingKind[]> => {
   const getProjectSharingRoleBindings = React.useCallback(
     () =>
-      listRoleBindings(
-        namespace,
-        `${LABEL_SELECTOR_DASHBOARD_RESOURCE},${LABEL_SELECTOR_PROJECT_SHARING}`,
-      ).catch((e) => {
+      listRoleBindings(namespace).catch((e) => {
         if (e.statusObject?.code === 404) {
           throw new Error('No rolebindings found.');
         }


Reviews:
user: login: andrewballantyne
body: For posterity -- [requested more work on slack](https://redhat-internal.slack.com/archives/C069KSM8T9N/p1747142892467409?thread_ts=1747141271.919949&cid=C069KSM8T9N)
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-13T15:10:04Z
user: login: Griffin-Sullivan
body: One thing I'm not sure about is non-dashboard created permissions for groups say `Default`. If someone changed this in the dashboard, they wouldn't be able to change it back to `Default` since the options are only `Contributor` or `Admin`

![image](https://github.com/user-attachments/assets/c764e91f-5ce0-4924-993e-9868653d4ae5)

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T18:29:31Z
user: login: Griffin-Sullivan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T18:46:17Z
user: login: emilys314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T18:46:53Z

Review comments:
diff_hunk: @@ -172,6 +182,60 @@ describe('Permissions tab', () => {
 
       cy.wait('@deleteUser');
     });
+
+    it('Shows confirmation modal when editing own permissions', () => {
+      initIntercepts({ isEmpty: false });
+      permissions.visit('test-project');
+
+      userTable.getTableRow('test-user').findKebabAction('Edit').click();
+      userTable.findEditInput('test-user').clear().type('test-user');
+      userTable.selectPermission('test-user', 'Admin Edit the project and manage user access');
+      userTable.findEditSaveButton('test-user').click();
+
+      permissions.findConfirmModal().should('be.visible');
+      permissions.findModalCancelButton().click();
path: frontend/src/__tests__/cypress/cypress/tests/mocked/projects/tabs/permissions.cy.ts
user: login: Griffin-Sullivan
body: you should be able to import the base modal and just use it's cancel and submit buttons. Can also delete the `find` functions you made above if you do that
created_at: 2025-05-14T18:18:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,12 @@
 import * as React from 'react';
 import { listRoleBindings } from '~/api';
-import { LABEL_SELECTOR_DASHBOARD_RESOURCE, LABEL_SELECTOR_PROJECT_SHARING } from '~/const';
 import { RoleBindingKind } from '~/k8sTypes';
 import useFetchState, { FetchState } from '~/utilities/useFetchState';
 
 const useProjectSharing = (namespace?: string): FetchState<RoleBindingKind[]> => {
   const getProjectSharingRoleBindings = React.useCallback(
     () =>
-      listRoleBindings(
-        namespace,
-        `${LABEL_SELECTOR_DASHBOARD_RESOURCE},${LABEL_SELECTOR_PROJECT_SHARING}`,
path: frontend/src/pages/projects/projectSharing/useProjectSharing.ts
user: login: Griffin-Sullivan
body: Do we still want the project sharing label?
created_at: 2025-05-14T18:24:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,6 +29,18 @@ class PermissionsTab {
   getGroupTable() {
     return new PermissionTable(() => cy.findByTestId('role-binding-table Group'));
   }
+
+  findConfirmModal() {
+    return cy.findByTestId('role-binding-permissions-change-modal');
+  }
+
+  findModalCancelButton() {
+    return cy.findByTestId('cancel-button');
+  }
+
+  findModalConfirmButton() {
+    return cy.findByTestId('confirm-button');
+  }
path: frontend/src/__tests__/cypress/cypress/pages/permissions.ts
user: login: Griffin-Sullivan
body: Other option is to move these to their own class and call it something like `PermissionsModal` and extend the base Modal. Add the confirm button and some sort of is visible.
created_at: 2025-05-14T18:26:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+import * as React from 'react'; //import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
path: frontend/src/concepts/roleBinding/RoleBindingPermissionsChangeModal.tsx
user: login: emilys314
body: ```suggestion
import * as React from 'react';
```

nitpick but remove the commented out code
created_at: 2025-05-14T18:35:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,12 @@
 import * as React from 'react';
 import { listRoleBindings } from '~/api';
-import { LABEL_SELECTOR_DASHBOARD_RESOURCE, LABEL_SELECTOR_PROJECT_SHARING } from '~/const';
 import { RoleBindingKind } from '~/k8sTypes';
 import useFetchState, { FetchState } from '~/utilities/useFetchState';
 
 const useProjectSharing = (namespace?: string): FetchState<RoleBindingKind[]> => {
   const getProjectSharingRoleBindings = React.useCallback(
     () =>
-      listRoleBindings(
-        namespace,
-        `${LABEL_SELECTOR_DASHBOARD_RESOURCE},${LABEL_SELECTOR_PROJECT_SHARING}`,
path: frontend/src/pages/projects/projectSharing/useProjectSharing.ts
user: login: Griffin-Sullivan
body: I was being dumb nevermind haha
created_at: 2025-05-14T18:46:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+import * as React from 'react'; //import { Modal, ModalVariant } from '@patternfly/react-core/deprecated';
+import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from '@patternfly/react-core';
+
+type RoleBindingPermissionsChangeModalProps = {
+  onClose: () => void;
+  onEdit: () => void;
+  onDelete: () => void;
+  onCancel: () => void;
+  isDeleting: boolean;
+  roleName?: string;
+};
+
+const RoleBindingPermissionsChangeModal: React.FC<RoleBindingPermissionsChangeModalProps> = ({
+  onClose,
+  onEdit,
+  onDelete,
+  onCancel,
+  isDeleting,
+  roleName,
+}) => {
+  const textToShow = isDeleting ? 'Delete' : 'Edit';
+  return (
+    <Modal
+      variant="small"
+      aria-label="Role binding permissions change modal"
+      data-testid="role-binding-permissions-change-modal"
+      appendTo={document.body}
+      isOpen
+      onClose={onClose}
+    >
+      <ModalHeader title={`Confirm ${textToShow.toLowerCase()}`} />
+      <ModalBody>
+        Are you sure you want to {isDeleting ? 'delete' : 'edit'} permissions for{' '}
+        <strong>{roleName || 'this role binding'}</strong>? {isDeleting ? 'Deleting' : 'Editing'}{' '}
+        these permissions may result in loss of access to this resource.
+      </ModalBody>
+      <ModalFooter>
+        <Button
+          variant="primary"
+          key="confirm"
+          onClick={isDeleting ? onDelete : onEdit}
path: frontend/src/concepts/roleBinding/RoleBindingPermissionsChangeModal.tsx
user: login: emilys314
body: Delete is a very destructive action so I've always seen the pattern of using the DeleteModal which will make you confirm it by having to type it. The styling is also different, the confirm button is red which indiciates a more destructive action is going to happen
![image](https://github.com/user-attachments/assets/0fa57bf4-bf26-4baa-8f3c-2fb1fc669d51)
created_at: 2025-05-14T18:46:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-13T00:59:32Z
body: ## [Codecov](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) Report
Attention: Patch coverage is `80.00000%` with `23 lines` in your changes missing coverage. Please review.
> Project coverage is 82.62%. Comparing base [(`bb8454d`)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/commit/bb8454dd61e20c857b254b754148eda8415ec6a4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) to head [(`1b41496`)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/commit/1b41496515d23b07e3a7a5de230251b0ace3758a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).
> Report is 4 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) | Patch % | Lines |
|---|---|---|
| [...pts/roleBinding/RoleBindingPermissionsTableRow.tsx](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2FRoleBindingPermissionsTableRow.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL1JvbGVCaW5kaW5nUGVybWlzc2lvbnNUYWJsZVJvdy50c3g=) | 81.35% | [11 Missing :warning: ](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) |
| [...ncepts/roleBinding/RoleBindingPermissionsTable.tsx](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2FRoleBindingPermissionsTable.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL1JvbGVCaW5kaW5nUGVybWlzc2lvbnNUYWJsZS50c3g=) | 66.66% | [6 Missing :warning: ](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) |
| [.../roleBinding/RoleBindingPermissionsChangeModal.tsx](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2FRoleBindingPermissionsChangeModal.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL1JvbGVCaW5kaW5nUGVybWlzc2lvbnNDaGFuZ2VNb2RhbC50c3g=) | 64.28% | [5 Missing :warning: ](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) |
| [frontend/src/concepts/roleBinding/utils.ts](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2Futils.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL3V0aWxzLnRz) | 94.73% | [1 Missing :warning: ](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) |

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197/graphs/tree.svg?width=650&height=150&src=pr&token=UQM7NLQUXJ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)

```diff
@@            Coverage Diff             @@
##             main    #4197      +/-   ##
==========================================
+ Coverage   82.60%   82.62%   +0.02%     
==========================================
  Files        1651     1652       +1     
  Lines       34844    34900      +56     
  Branches    10305    10321      +16     
==========================================
+ Hits        28782    28836      +54     
- Misses       6062     6064       +2     
```

| [Files with missing lines](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io) | Coverage Δ | |
|---|---|---|
| [frontend/src/api/k8s/roleBindings.ts](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fapi%2Fk8s%2FroleBindings.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2FwaS9rOHMvcm9sZUJpbmRpbmdzLnRz) | `100.00% <100.00%> (ø)` | |
| [...rc/concepts/roleBinding/RoleBindingPermissions.tsx](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2FRoleBindingPermissions.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL1JvbGVCaW5kaW5nUGVybWlzc2lvbnMudHN4) | `92.85% <ø> (ø)` | |
| [frontend/src/concepts/roleBinding/types.ts](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2Ftypes.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL3R5cGVzLnRz) | `100.00% <100.00%> (ø)` | |
| [...tend/src/pages/projects/components/DeleteModal.tsx](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fpages%2Fprojects%2Fcomponents%2FDeleteModal.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL3BhZ2VzL3Byb2plY3RzL2NvbXBvbmVudHMvRGVsZXRlTW9kYWwudHN4) | `91.30% <100.00%> (+0.39%)` | :arrow_up: |
| [...pages/projects/projectSharing/useProjectSharing.ts](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fpages%2Fprojects%2FprojectSharing%2FuseProjectSharing.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL3BhZ2VzL3Byb2plY3RzL3Byb2plY3RTaGFyaW5nL3VzZVByb2plY3RTaGFyaW5nLnRz) | `85.71% <100.00%> (-1.79%)` | :arrow_down: |
| [frontend/src/concepts/roleBinding/utils.ts](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2Futils.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL3V0aWxzLnRz) | `97.56% <94.73%> (-2.44%)` | :arrow_down: |
| [.../roleBinding/RoleBindingPermissionsChangeModal.tsx](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2FRoleBindingPermissionsChangeModal.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL1JvbGVCaW5kaW5nUGVybWlzc2lvbnNDaGFuZ2VNb2RhbC50c3g=) | `64.28% <64.28%> (ø)` | |
| [...ncepts/roleBinding/RoleBindingPermissionsTable.tsx](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2FRoleBindingPermissionsTable.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL1JvbGVCaW5kaW5nUGVybWlzc2lvbnNUYWJsZS50c3g=) | `85.18% <66.66%> (+7.99%)` | :arrow_up: |
| [...pts/roleBinding/RoleBindingPermissionsTableRow.tsx](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?src=pr&el=tree&filepath=frontend%2Fsrc%2Fconcepts%2FroleBinding%2FRoleBindingPermissionsTableRow.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io#diff-ZnJvbnRlbmQvc3JjL2NvbmNlcHRzL3JvbGVCaW5kaW5nL1JvbGVCaW5kaW5nUGVybWlzc2lvbnNUYWJsZVJvdy50c3g=) | `86.90% <81.35%> (-8.10%)` | :arrow_down: |

... and [9 files with indirect coverage changes](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io). Last update [bb8454d...1b41496](https://app.codecov.io/gh/opendatahub-io/odh-dashboard/pull/4197?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatahub-io).

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: katieperry4
created_at: 2025-05-14T13:46:02Z
body: @kaedward Could I get some input on the alert and modal text 😅 it's not really my strong suit
author_association: CONTRIBUTOR
user: login: Griffin-Sullivan
created_at: 2025-05-14T18:31:06Z
body: Also do we want to run a check if you're changing your own permissions on a group basis?
author_association: CONTRIBUTOR
user: login: katieperry4
created_at: 2025-05-14T18:35:53Z
body: @Griffin-Sullivan for that Andrew said "Don’t worry about that — groups are units in their own right — no looking inside" so I think we're leaving it 🤷‍♀️ 
author_association: CONTRIBUTOR
user: login: emilys314
created_at: 2025-05-16T18:10:25Z
body: Seems to be working good for me!

![image](https://github.com/user-attachments/assets/36bd375e-afda-4b56-ae93-1646c2222671)

author_association: CONTRIBUTOR
user: login: emilys314
created_at: 2025-05-16T18:27:03Z
body: /lgtm
/approve
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-05-16T18:27:12Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/opendatahub-io/odh-dashboard/pull/4197#issuecomment-2887411888" title="Approved">emilys314</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=opendatahub-io%2Fodh-dashboard).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/opendatahub-io/odh-dashboard/blob/main/OWNERS)~~ [emilys314]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR