-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Fixed and Updated OpenApi and changed users endpoint * Data hooks: added MutationOptions and changed keys * Added Users page and Comming Soon page for roles * Added useFuzySearch hook * Typo * Removed submodule files * Removed 204 response for roles * Fix: cannot delete default roles * Moved toCaslResources to ms-v2 * Removed debugger statement * PUT /role now uses only fields that changed for auth * Updated authorization rules * Fixed API endpoints in OpenAPI description * Refactor and implemented useInvalidateAsseta * Updated subject type to accept objects * Moved AuthCallbackListener to be inside of AntDesign context * Updated active segment logic * Moved user list to a separate component * Implemented role pages
- Loading branch information
1 parent
5a5f77f
commit 213da0f
Showing
38 changed files
with
1,329 additions
and
273 deletions.
There are no files selected for viewing
74 changes: 74 additions & 0 deletions
74
src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
'use client'; | ||
|
||
import Content from '@/components/content'; | ||
import Auth from '@/lib/AuthCanWrapper'; | ||
import { useGetAsset } from '@/lib/fetch-data'; | ||
import { Button, Result, Skeleton, Space, Tabs } from 'antd'; | ||
import { LeftOutlined } from '@ant-design/icons'; | ||
import { ComponentProps } from 'react'; | ||
import RoleGeneralData from './roleGeneralData'; | ||
import { useRouter } from 'next/navigation'; | ||
import RolePermissions from './rolePermissions'; | ||
import RoleMembers from './role-members'; | ||
|
||
type Items = ComponentProps<typeof Tabs>['items']; | ||
|
||
function RolePage({ params: { roleId } }: { params: { roleId: string } }) { | ||
const router = useRouter(); | ||
const { | ||
data: role, | ||
isLoading, | ||
error, | ||
} = useGetAsset('/roles/{id}', { | ||
params: { path: { id: roleId } }, | ||
}); | ||
|
||
const items: Items = role | ||
? [ | ||
{ | ||
key: 'members', | ||
label: 'Manage Members', | ||
children: <RoleMembers role={role} isLoadingRole={isLoading} />, | ||
}, | ||
{ key: 'permissions', label: 'Permissions', children: <RolePermissions role={role} /> }, | ||
{ | ||
key: 'generalData', | ||
label: 'General Data', | ||
children: <RoleGeneralData roleId={roleId} />, | ||
}, | ||
] | ||
: []; | ||
|
||
if (error) return; | ||
<Result | ||
status="error" | ||
title="Failed to fetch role" | ||
subTitle="An error ocurred while fetching role, please try again." | ||
/>; | ||
|
||
return ( | ||
<Content | ||
title={ | ||
<Space> | ||
<Button icon={<LeftOutlined />} onClick={() => router.push('/iam/roles')} type="text"> | ||
Back | ||
</Button> | ||
{role?.name} | ||
</Space> | ||
} | ||
> | ||
<Skeleton loading={isLoading}> | ||
<Tabs items={items} /> | ||
</Skeleton> | ||
</Content> | ||
); | ||
} | ||
|
||
export default Auth( | ||
{ | ||
action: ['view', 'manage'], | ||
resource: 'Role', | ||
fallbackRedirect: '/', | ||
}, | ||
RolePage, | ||
); |
154 changes: 154 additions & 0 deletions
154
src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
'use client'; | ||
|
||
import { FC, useMemo, useState } from 'react'; | ||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; | ||
import { | ||
ApiData, | ||
useDeleteAsset, | ||
useGetAsset, | ||
useInvalidateAsset, | ||
usePostAsset, | ||
} from '@/lib/fetch-data'; | ||
import UserList, { UserListProps } from '@/components/user-list'; | ||
import { Button, Modal, Popconfirm, Tooltip } from 'antd'; | ||
|
||
type Role = ApiData<'/roles', 'get'>[number]; | ||
|
||
const AddUserModal: FC<{ role: Role; open: boolean; close: () => void }> = ({ | ||
role, | ||
open, | ||
close, | ||
}) => { | ||
const { data: users, isLoading: isLoadingUsers } = useGetAsset('/users', {}); | ||
const invalidateRole = useInvalidateAsset('/roles/{id}', { params: { path: { id: role.id } } }); | ||
const { mutateAsync, isLoading: isLoadingMutation } = usePostAsset('/role-mappings', { | ||
onSuccess: invalidateRole, | ||
}); | ||
|
||
type AddUserParams = Parameters<NonNullable<UserListProps['selectedRowActions']>>; | ||
const addUsers = async (users: AddUserParams[2], clearIds?: AddUserParams[1]) => { | ||
if (clearIds) clearIds(); | ||
await mutateAsync({ | ||
body: users.map((user) => ({ | ||
userId: user.id, | ||
roleId: role.id, | ||
email: user.email, | ||
lastName: user.lastName, | ||
firstName: user.firstName, | ||
username: user.username, | ||
})), | ||
parseAs: 'text', | ||
}); | ||
}; | ||
|
||
const usersNotInRole = useMemo(() => { | ||
if (!users) return []; | ||
|
||
const usersInRole = new Set(role.members.map((member) => member.userId)); | ||
|
||
return users.filter((user) => !usersInRole.has(user.id)); | ||
}, [users, role.members]); | ||
|
||
return ( | ||
<Modal | ||
open={open} | ||
onCancel={close} | ||
footer={null} | ||
width={800} | ||
title={`Add members to ${role.name}`} | ||
> | ||
<UserList | ||
users={usersNotInRole} | ||
loading={isLoadingUsers || isLoadingMutation} | ||
columns={(clearSelected) => [ | ||
{ | ||
dataIndex: 'id', | ||
render: (_, user) => ( | ||
<Tooltip placement="top" title="Add to role"> | ||
<Button | ||
icon={<PlusOutlined />} | ||
type="text" | ||
onClick={() => addUsers([user], clearSelected)} | ||
/> | ||
</Tooltip> | ||
), | ||
}, | ||
]} | ||
selectedRowActions={(_, clearIds, users) => ( | ||
<Tooltip placement="top" title="Add to role"> | ||
<Button icon={<PlusOutlined />} type="text" onClick={() => addUsers(users, clearIds)} /> | ||
</Tooltip> | ||
)} | ||
/> | ||
</Modal> | ||
); | ||
}; | ||
|
||
const RoleMembers: FC<{ role: Role; isLoadingRole?: boolean }> = ({ role, isLoadingRole }) => { | ||
const [addUserModalOpen, setAddUserModalOpen] = useState(false); | ||
|
||
const refetchRole = useInvalidateAsset('/roles/{id}', { params: { path: { id: role.id } } }); | ||
const { mutateAsync: deleteUser, isLoading: isLoadingDelete } = useDeleteAsset( | ||
'/role-mappings/users/{userId}/roles/{roleId}', | ||
{ onSuccess: refetchRole }, | ||
); | ||
|
||
async function deleteMembers(userIds: string[], clearIds?: () => void) { | ||
if (clearIds) clearIds(); | ||
|
||
await Promise.allSettled( | ||
userIds.map((userId) => | ||
deleteUser({ | ||
params: { path: { roleId: role.id, userId: userId } }, | ||
}), | ||
), | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<AddUserModal role={role} open={addUserModalOpen} close={() => setAddUserModalOpen(false)} /> | ||
<UserList | ||
users={role.members.map((member) => ({ ...member, id: member.userId }))} | ||
loading={isLoadingDelete || isLoadingRole} | ||
columns={(clearSelected) => [ | ||
{ | ||
dataIndex: 'id', | ||
key: 'remove', | ||
title: '', | ||
width: 100, | ||
render: (id: string) => ( | ||
<Tooltip placement="top" title="Remove member"> | ||
<Popconfirm | ||
title="Remove member" | ||
description="Are you sure you want to remove this member?" | ||
onConfirm={() => deleteMembers([id], clearSelected)} | ||
> | ||
<Button icon={<DeleteOutlined />} type="text" /> | ||
</Popconfirm> | ||
</Tooltip> | ||
), | ||
}, | ||
]} | ||
selectedRowActions={(ids, clearIds) => ( | ||
<Tooltip placement="top" title="Remove members"> | ||
<Popconfirm | ||
title="Remove member" | ||
description="Are you sure you want to remove this member?" | ||
onConfirm={() => deleteMembers(ids, clearIds)} | ||
> | ||
<Button icon={<DeleteOutlined />} type="text" /> | ||
</Popconfirm> | ||
</Tooltip> | ||
)} | ||
searchBarRightNode={ | ||
<Button type="primary" onClick={() => setAddUserModalOpen(true)}> | ||
Add member | ||
</Button> | ||
} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
export default RoleMembers; |
90 changes: 90 additions & 0 deletions
90
src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import Auth from '@/lib/AuthCanWrapper'; | ||
import { toCaslResource } from '@/lib/ability/caslAbility'; | ||
import { useGetAsset, usePutAsset } from '@/lib/fetch-data'; | ||
import { useAuthStore } from '@/lib/iam'; | ||
import { Alert, App, Button, DatePicker, Form, Input, Spin } from 'antd'; | ||
import { FC } from 'react'; | ||
import dayjs from 'dayjs'; | ||
import germanLocale from 'antd/es/date-picker/locale/de_DE'; | ||
|
||
const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => { | ||
const { message } = App.useApp(); | ||
const ability = useAuthStore((store) => store.ability); | ||
const [form] = Form.useForm(); | ||
|
||
const { data, isLoading, error } = useGetAsset('/roles/{id}', { | ||
params: { path: { id: roleId } }, | ||
}); | ||
|
||
const { mutateAsync: updateRole, isLoading: putLoading } = usePutAsset('/roles/{id}', { | ||
onError: () => message.open({ type: 'error', content: 'Something went wrong' }), | ||
}); | ||
|
||
if (isLoading || error || !data) return <Spin />; | ||
|
||
const role = toCaslResource('Role', data); | ||
|
||
async function submitChanges(values: Record<string, any>) { | ||
if (typeof values.expirationDayJs === 'object') { | ||
values.expiration = (values.expirationDayJs as dayjs.Dayjs).toISOString(); | ||
delete values.expirationDayJs; | ||
} | ||
|
||
try { | ||
await updateRole({ | ||
params: { path: { id: roleId } }, | ||
body: values, | ||
}); | ||
|
||
// success message has to go here, or else the mutation will stop loading when the message | ||
// disappears | ||
message.open({ type: 'success', content: 'Role updated' }); | ||
} catch (e) {} | ||
} | ||
|
||
return ( | ||
<Form form={form} layout="vertical" onFinish={submitChanges} initialValues={role}> | ||
{role.note && ( | ||
<> | ||
<Alert type="warning" message={role.note} /> | ||
<br /> | ||
</> | ||
)} | ||
<Form.Item label="Name" name="name"> | ||
<Input placeholder="input placeholder" disabled={!ability.can('update', role, 'name')} /> | ||
</Form.Item> | ||
|
||
<Form.Item label="Description" name="description"> | ||
<Input.TextArea | ||
placeholder="input placeholder" | ||
disabled={!ability.can('update', role, 'description')} | ||
/> | ||
</Form.Item> | ||
|
||
<Form.Item label="Expiration" name="expirationDayJs"> | ||
<DatePicker | ||
// Note german locale hard coded | ||
locale={germanLocale} | ||
allowClear={true} | ||
disabled={!ability.can('update', role, 'expiration')} | ||
defaultValue={role.expiration ? dayjs(new Date(role.expiration)) : undefined} | ||
/> | ||
</Form.Item> | ||
|
||
<Form.Item> | ||
<Button type="primary" htmlType="submit" loading={putLoading}> | ||
Submit | ||
</Button> | ||
</Form.Item> | ||
</Form> | ||
); | ||
}; | ||
|
||
export default Auth( | ||
{ | ||
action: ['view', 'manage'], | ||
resource: 'Role', | ||
fallbackRedirect: '/', | ||
}, | ||
RoleGeneralData, | ||
); |
Oops, something went wrong.