Skip to content

Commit ce44ebf

Browse files
committed
assign/remove roles to principals functionality
1 parent 1996ebe commit ce44ebf

File tree

3 files changed

+218
-72
lines changed

3 files changed

+218
-72
lines changed

src/app/principals/page.tsx

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {useAuthenticatedFetch} from '@/hooks/useAuthenticatedFetch';
77
import Principals, {Principal, PrincipalRoleItem} from '@/app/ui/principals';
88
import CreatePrincipalModal from '@/app/ui/create-principal-modal';
99
import AssignPrincipalRoleModal from '@/app/ui/assign-principal-role-modal';
10+
import RemovePrincipalRoleModal from '@/app/ui/remove-principal-role-modal';
1011

1112
const {Title} = Typography;
1213

@@ -24,6 +25,7 @@ export default function Page() {
2425
const [rolesLoading, setRolesLoading] = useState<Record<string, boolean>>({});
2526
const [createModalVisible, setCreateModalVisible] = useState(false);
2627
const [assignRoleModalVisible, setAssignRoleModalVisible] = useState(false);
28+
const [removeRoleModalVisible, setRemoveRoleModalVisible] = useState(false);
2729
const [selectedPrincipalForRole, setSelectedPrincipalForRole] = useState<string | null>(null);
2830
const {authenticatedFetch} = useAuthenticatedFetch();
2931

@@ -151,28 +153,21 @@ export default function Page() {
151153
}
152154
};
153155

154-
const handleDeletePrincipalRole = async (principalName: string, roleName: string) => {
155-
try {
156-
await authenticatedFetch(
157-
`/api/principals/${encodeURIComponent(principalName)}/principal-roles/${encodeURIComponent(roleName)}`,
158-
{
159-
method: 'DELETE',
160-
}
161-
);
162-
163-
message.success(`Role "${roleName}" removed from principal "${principalName}" successfully!`);
156+
const handleRemoveRole = (principalName: string) => {
157+
setSelectedPrincipalForRole(principalName);
158+
setRemoveRoleModalVisible(true);
159+
};
164160

161+
const handleRemoveRoleSuccess = async () => {
162+
if (selectedPrincipalForRole) {
165163
// Refresh the principal roles for the selected principal
166-
setRolesLoading(prev => ({...prev, [principalName]: true}));
164+
setRolesLoading(prev => ({...prev, [selectedPrincipalForRole]: true}));
167165
try {
168-
const roles = await getPrincipalRoles(principalName);
169-
setPrincipalRoles(prev => ({...prev, [principalName]: roles}));
166+
const roles = await getPrincipalRoles(selectedPrincipalForRole);
167+
setPrincipalRoles(prev => ({...prev, [selectedPrincipalForRole]: roles}));
170168
} finally {
171-
setRolesLoading(prev => ({...prev, [principalName]: false}));
169+
setRolesLoading(prev => ({...prev, [selectedPrincipalForRole]: false}));
172170
}
173-
} catch (error) {
174-
console.error('Error removing principal role:', error);
175-
throw error; // Re-throw to let the modal handle the error display
176171
}
177172
};
178173

@@ -246,7 +241,7 @@ export default function Page() {
246241
onResetCredentials={handleResetCredentials}
247242
onDelete={handleDeletePrincipal}
248243
onAddRole={handleAddRole}
249-
onDeletePrincipalRole={handleDeletePrincipalRole}
244+
onRemoveRole={handleRemoveRole}
250245
/>
251246

252247
<CreatePrincipalModal
@@ -264,6 +259,17 @@ export default function Page() {
264259
}}
265260
onSuccess={handleAssignRoleSuccess}
266261
/>
262+
263+
<RemovePrincipalRoleModal
264+
visible={removeRoleModalVisible}
265+
principalName={selectedPrincipalForRole}
266+
assignedRoles={selectedPrincipalForRole ? (principalRoles[selectedPrincipalForRole] || []) : []}
267+
onClose={() => {
268+
setRemoveRoleModalVisible(false);
269+
setSelectedPrincipalForRole(null);
270+
}}
271+
onSuccess={handleRemoveRoleSuccess}
272+
/>
267273
</Space>
268274
);
269275
}

src/app/ui/principals.tsx

Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
EditOutlined,
99
HomeOutlined,
1010
IdcardOutlined,
11+
MinusOutlined,
1112
PlusOutlined,
1213
ReloadOutlined,
1314
SettingOutlined,
@@ -55,8 +56,8 @@ interface PrincipalsProps {
5556
onDelete?: (principalName: string) => void;
5657
onResetCredentials?: (principalName: string) => void;
5758
onAddRole?: (principalName: string) => void;
59+
onRemoveRole?: (principalName: string) => void;
5860
onEditPrincipalRole?: (principalName: string, roleName: string) => void;
59-
onDeletePrincipalRole?: (principalName: string, roleName: string) => void;
6061
}
6162

6263
function formatDate(timestamp: number): string {
@@ -76,15 +77,13 @@ export default function Principals({
7677
onDelete,
7778
onResetCredentials,
7879
onAddRole,
79-
onEditPrincipalRole,
80-
onDeletePrincipalRole
80+
onRemoveRole,
81+
onEditPrincipalRole
8182
}: PrincipalsProps) {
8283
const [resetModalVisible, setResetModalVisible] = useState(false);
8384
const [selectedPrincipalForReset, setSelectedPrincipalForReset] = useState<string | null>(null);
8485
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
8586
const [selectedPrincipalForDelete, setSelectedPrincipalForDelete] = useState<string | null>(null);
86-
const [deleteRoleModalVisible, setDeleteRoleModalVisible] = useState(false);
87-
const [selectedRoleForDelete, setSelectedRoleForDelete] = useState<{ principalName: string; roleName: string } | null>(null);
8887

8988
const handleResetSuccess = () => {
9089
if (onRefresh) {
@@ -111,17 +110,6 @@ export default function Principals({
111110
}
112111
};
113112

114-
const handleDeleteRoleClick = (principalName: string, roleName: string) => {
115-
setSelectedRoleForDelete({ principalName, roleName });
116-
setDeleteRoleModalVisible(true);
117-
};
118-
119-
const handleDeleteRoleConfirm = async () => {
120-
if (onDeletePrincipalRole && selectedRoleForDelete) {
121-
await onDeletePrincipalRole(selectedRoleForDelete.principalName, selectedRoleForDelete.roleName);
122-
}
123-
};
124-
125113
const getRolesColumns = (principalName: string): ColumnsType<PrincipalRoleItem> => [
126114
{
127115
title: "Name",
@@ -253,18 +241,6 @@ export default function Principals({
253241
}
254242
},
255243
},
256-
{
257-
type: 'divider',
258-
},
259-
{
260-
key: 'delete',
261-
label: 'Remove',
262-
icon: <DeleteOutlined/>,
263-
danger: true,
264-
onClick: () => {
265-
handleDeleteRoleClick(principalName, record.name);
266-
},
267-
},
268244
];
269245

270246
return (
@@ -472,19 +448,35 @@ export default function Principals({
472448
<Tag color="blue">{roles.length}</Tag>
473449
</Space>
474450
</Title>
475-
{onAddRole && (
476-
<Button
477-
variant="outlined"
478-
size="small"
479-
icon={<PlusOutlined/>}
480-
onClick={(e) => {
481-
e.stopPropagation();
482-
onAddRole(record.name);
483-
}}
484-
>
485-
Add Role
486-
</Button>
487-
)}
451+
<Space>
452+
{onAddRole && (
453+
<Button
454+
type="primary"
455+
size="small"
456+
icon={<PlusOutlined/>}
457+
onClick={(e) => {
458+
e.stopPropagation();
459+
onAddRole(record.name);
460+
}}
461+
>
462+
Add Role
463+
</Button>
464+
)}
465+
{onRemoveRole && (
466+
<Button
467+
danger
468+
size="small"
469+
icon={<MinusOutlined/>}
470+
onClick={(e) => {
471+
e.stopPropagation();
472+
onRemoveRole(record.name);
473+
}}
474+
disabled={roles.length === 0}
475+
>
476+
Remove Role
477+
</Button>
478+
)}
479+
</Space>
488480
</Flex>
489481
<Table
490482
columns={getRolesColumns(record.name)}
@@ -559,19 +551,6 @@ export default function Principals({
559551
description=""
560552
warningMessage="This will permanently remove the principal and revoke all access."
561553
/>
562-
563-
<DeleteConfirmationModal
564-
visible={deleteRoleModalVisible}
565-
entityType="Role"
566-
entityName={selectedRoleForDelete?.roleName || null}
567-
onClose={() => {
568-
setDeleteRoleModalVisible(false);
569-
setSelectedRoleForDelete(null);
570-
}}
571-
onConfirm={handleDeleteRoleConfirm}
572-
description={selectedRoleForDelete ? `Remove role from principal "${selectedRoleForDelete.principalName}"` : ''}
573-
warningMessage="This will remove the role assignment from the principal."
574-
/>
575554
</>
576555
);
577556
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
'use client'
2+
import {Form, Input, message, Modal, Select, Space} from 'antd'
3+
import {DeleteOutlined, TeamOutlined} from '@ant-design/icons'
4+
import {useState} from 'react'
5+
import {useAuthenticatedFetch} from '@/hooks/useAuthenticatedFetch'
6+
7+
interface RemovePrincipalRoleModalProps {
8+
visible: boolean;
9+
principalName: string | null;
10+
assignedRoles: { name: string }[];
11+
onClose: () => void;
12+
onSuccess: () => void;
13+
}
14+
15+
interface RemoveRoleFormValues {
16+
roleName: string;
17+
}
18+
19+
export default function RemovePrincipalRoleModal({
20+
visible,
21+
principalName,
22+
assignedRoles,
23+
onClose,
24+
onSuccess
25+
}: RemovePrincipalRoleModalProps) {
26+
const [form] = Form.useForm<RemoveRoleFormValues>();
27+
const [loading, setLoading] = useState(false);
28+
const {authenticatedFetch} = useAuthenticatedFetch();
29+
30+
const handleSubmit = async (values: RemoveRoleFormValues) => {
31+
if (!principalName) {
32+
message.error('No principal selected');
33+
return;
34+
}
35+
36+
console.log('Form submitted with values:', values);
37+
setLoading(true);
38+
39+
try {
40+
console.log('Removing role from principal:', principalName, values.roleName);
41+
42+
const data = await authenticatedFetch(
43+
`/api/principals/${encodeURIComponent(principalName)}/principal-roles/${encodeURIComponent(values.roleName)}`,
44+
{
45+
method: 'DELETE',
46+
}
47+
);
48+
49+
console.log('Received response:', data);
50+
51+
message.success(`Role "${values.roleName}" removed from principal "${principalName}" successfully!`);
52+
form.resetFields();
53+
onSuccess();
54+
onClose();
55+
} catch (error) {
56+
console.error('Error removing principal role:', error);
57+
} finally {
58+
setLoading(false);
59+
}
60+
};
61+
62+
const handleCancel = () => {
63+
form.resetFields();
64+
onClose();
65+
};
66+
67+
return (
68+
<Modal
69+
title={
70+
<Space>
71+
<DeleteOutlined/>
72+
Remove Role from Principal
73+
</Space>
74+
}
75+
open={visible}
76+
onCancel={handleCancel}
77+
footer={null}
78+
width={500}
79+
destroyOnClose
80+
centered
81+
>
82+
<Form
83+
form={form}
84+
layout="vertical"
85+
onFinish={handleSubmit}
86+
onFinishFailed={(errorInfo) => {
87+
console.log('Form validation failed:', errorInfo);
88+
message.error('Please fill in all required fields');
89+
}}
90+
autoComplete="off"
91+
>
92+
<Form.Item
93+
label="Principal"
94+
tooltip="The principal to remove the role from"
95+
>
96+
<Input
97+
prefix={<TeamOutlined/>}
98+
value={principalName || ''}
99+
disabled
100+
/>
101+
</Form.Item>
102+
103+
<Form.Item
104+
label="Role Name"
105+
name="roleName"
106+
rules={[
107+
{required: true, message: 'Please select a role to remove'},
108+
]}
109+
tooltip="Select the role to remove from this principal"
110+
>
111+
<Select
112+
showSearch
113+
placeholder="Select a role to remove"
114+
filterOption={(input, option) =>
115+
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
116+
}
117+
options={assignedRoles.map(role => ({
118+
value: role.name,
119+
label: role.name,
120+
}))}
121+
notFoundContent={assignedRoles.length === 0 ? "No roles assigned to this principal" : "No matching roles"}
122+
/>
123+
</Form.Item>
124+
125+
<Form.Item style={{marginBottom: 0, marginTop: 24}}>
126+
<Space style={{width: '100%', justifyContent: 'flex-end'}}>
127+
<button
128+
type="button"
129+
onClick={handleCancel}
130+
style={{
131+
padding: '4px 15px',
132+
border: '1px solid #d9d9d9',
133+
borderRadius: '6px',
134+
background: 'white',
135+
cursor: 'pointer',
136+
}}
137+
>
138+
Cancel
139+
</button>
140+
<button
141+
type="submit"
142+
disabled={loading || assignedRoles.length === 0}
143+
style={{
144+
padding: '4px 15px',
145+
border: '1px solid #ff4d4f',
146+
borderRadius: '6px',
147+
background: '#ff4d4f',
148+
color: 'white',
149+
cursor: loading || assignedRoles.length === 0 ? 'not-allowed' : 'pointer',
150+
opacity: loading || assignedRoles.length === 0 ? 0.6 : 1,
151+
}}
152+
>
153+
{loading ? 'Removing...' : 'Remove Role'}
154+
</button>
155+
</Space>
156+
</Form.Item>
157+
</Form>
158+
</Modal>
159+
);
160+
}
161+

0 commit comments

Comments
 (0)