-
-
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.
feat: revamp users, modals, introduced a bunch of new components and
data fetching hooks into use Closes #39 Closes #38 Closes #54 Closes #65 Resolve #15 Resolve #16 Resolve #17 Resolve #18 Resolve #19 Resolve #20 Resolve #21 Resolve #22 Resolve #23 Resolve #24
- Loading branch information
Showing
10 changed files
with
711 additions
and
307 deletions.
There are no files selected for viewing
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,51 @@ | ||
import type {FunctionComponent} from 'react' | ||
import cn from 'classnames' | ||
import {Right} from '@/components/icons/arrows' | ||
import {MissingUserAvatar} from '@/components/icons/user' | ||
|
||
const GroupListItem: FunctionComponent<{name: string; avatars: Array<string>; onClick: () => void}> = ({ | ||
name, | ||
avatars, | ||
onClick | ||
}) => { | ||
return ( | ||
<li> | ||
<button className="block w-full hover:bg-gray-50" onClick={onClick}> | ||
<div className="flex items-center px-4 py-4 sm:px-6"> | ||
<div className="flex-1 min-w-0 sm:flex sm:items-center sm:justify-between"> | ||
<div className="flex flex-row truncate"> | ||
<img className="w-8 h-8" src="/assets/logo.png" alt="" aria-hidden /> | ||
<div className="self-center flex-1 min-w-0 px-4 md:grid md:grid-cols-2 md:gap-4"> | ||
<div className="text-left"> | ||
<p className="text-sm font-medium text-indigo-600 truncate">{name}</p> | ||
</div> | ||
</div> | ||
</div> | ||
{avatars?.length > 0 && ( | ||
<div className="flex-shrink-0 mt-4 sm:mt-0 sm:ml-5"> | ||
<div className="flex overflow-hidden"> | ||
{avatars.map((avatar, index) => | ||
avatar ? ( | ||
<img | ||
className={cn('inline-block w-6 h-6 rounded-full ring-2 ring-white', index && '-ml-1')} | ||
src={avatar} | ||
alt="" | ||
/> | ||
) : ( | ||
<MissingUserAvatar | ||
className={cn('w-6 h-6 text-gray-600 rounded-full bg-gray-50', index && '-ml-1')} | ||
/> | ||
) | ||
)} | ||
</div> | ||
</div> | ||
)} | ||
</div> | ||
<Right className="w-5 h-5 text-gray-400" /> | ||
</div> | ||
</button> | ||
</li> | ||
) | ||
} | ||
|
||
export {GroupListItem} |
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,34 @@ | ||
import type {FunctionComponent} from 'react' | ||
import {Right} from '@/components/icons/arrows' | ||
import {MissingUserAvatar} from '@/components/icons/user' | ||
|
||
const UserListItem: FunctionComponent<{avatar?: string; email: string; userRole: string; onClick: () => void}> = ({ | ||
avatar, | ||
email, | ||
userRole, | ||
onClick | ||
}) => { | ||
return ( | ||
<li> | ||
<button className="block w-full hover:bg-gray-50" onClick={onClick}> | ||
<div className="flex items-center px-4 py-4 sm:px-6"> | ||
<div className="flex items-center flex-1 min-w-0"> | ||
<div className="flex-shrink-0 align-start"> | ||
{avatar && <img className="object-cover object-center w-12 h-12 rounded-full" alt="" />} | ||
{!avatar && <MissingUserAvatar className="w-12 h-12 text-gray-600 rounded-full bg-gray-50" />} | ||
</div> | ||
<div className="flex-1 min-w-0 px-4 md:grid md:grid-cols-2 md:gap-4"> | ||
<div className="text-left"> | ||
<p className="text-sm font-medium text-indigo-600 truncate">{email}</p> | ||
<div className="tag">{userRole}</div> | ||
</div> | ||
</div> | ||
</div> | ||
<Right className="w-5 h-5 text-gray-400" /> | ||
</div> | ||
</button> | ||
</li> | ||
) | ||
} | ||
|
||
export {UserListItem} |
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,75 @@ | ||
import type {FunctionComponent, PropsWithChildren} from 'react' | ||
import {Transition} from '@headlessui/react' | ||
import {Portal} from '@reach/portal' | ||
|
||
const SidePanel: FunctionComponent<PropsWithChildren<{isOpen: boolean; close: () => void}>> = ({ | ||
isOpen, | ||
close, | ||
children | ||
}) => { | ||
return ( | ||
<Portal> | ||
<Transition show={isOpen}> | ||
<div className="fixed inset-0 overflow-hidden w-screen"> | ||
<div className="absolute inset-0 overflow-hidden"> | ||
<Transition.Child | ||
enter="transition ease-in-out duration-500" | ||
enterFrom="opacity-0" | ||
enterTo="opacity-100" | ||
leave="transition ease-in-out duration-500" | ||
leaveFrom="opacity-100" | ||
leaveTo="opacity-0"> | ||
<div | ||
className="absolute inset-0 bg-gray-500 bg-opacity-75 transition-opacity" | ||
aria-hidden="true" | ||
onClick={close} | ||
/> | ||
</Transition.Child> | ||
<section | ||
className="absolute inset-y-0 right-0 flex w-full pl-10 max-w-112" | ||
aria-labelledby="slide-over-heading"> | ||
<Transition.Child | ||
className="w-full" | ||
enter="transition transform ease-in-out duration-500 sm:duration-700" | ||
enterFrom="translate-x-full" | ||
enterTo="translate-x-0" | ||
leave="transition transform ease-in-out duration-500 sm:duration-700" | ||
leaveFrom="translate-x-0" | ||
leaveTo="translate-x-full"> | ||
<div className="relative w-full max-w-md"> | ||
<Transition.Child | ||
enter="transition ease-in-out duration-500" | ||
enterFrom="opacity-0" | ||
enterTo="opacity-100" | ||
leave="transition ease-in-out duration-500" | ||
leaveFrom="opacity-100" | ||
leaveTo="opacity-0"> | ||
<div className="absolute top-0 left-0 flex pt-4 pr-2 -ml-8 sm:-ml-10 sm:pr-4"> | ||
<button | ||
className="text-gray-300 rounded-md hover:text-white focus:outline-none focus:ring-2 focus:ring-white" | ||
onClick={close}> | ||
<span className="sr-only">Close panel</span> | ||
<svg | ||
className="w-6 h-6" | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
stroke="currentColor" | ||
aria-hidden="true"> | ||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /> | ||
</svg> | ||
</button> | ||
</div> | ||
</Transition.Child> | ||
<div className="flex flex-col h-screen py-6 overflow-y-scroll bg-white shadow-xl">{children}</div> | ||
</div> | ||
</Transition.Child> | ||
</section> | ||
</div> | ||
</div> | ||
</Transition> | ||
</Portal> | ||
) | ||
} | ||
|
||
export {SidePanel} |
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,170 @@ | ||
import type {FunctionComponent} from 'react' | ||
import {useState} from 'react' | ||
import take from 'lodash.take' | ||
import {useForm} from 'react-hook-form' | ||
import VisuallyHidden from '@reach/visually-hidden' | ||
import {useDisclosure} from 'react-use-disclosure' | ||
import {Alert, Select, Input} from '@/components/lib' | ||
import {Spinner} from '@/components/icons/spinner' | ||
import {GroupListItem} from '@/components/pages/users/group-list-item' | ||
import {SidePanel} from '@/components/side-panel' | ||
import {Notification} from '@/components/notifications' | ||
import {useFetch, useMutate} from '@/utils/query-builder' | ||
import type {PyGridUserGroup} from '@/types/users' | ||
|
||
const UserGroups: FunctionComponent = () => { | ||
const [group, setGroup] = useState(null) | ||
const {isOpen, open, close} = useDisclosure() | ||
const {register, handleSubmit} = useForm() | ||
const {data: groups, isLoading, isError, error} = useFetch('/groups') | ||
const create = useMutate<Pick<PyGridUserGroup, 'name'>, PyGridUserGroup>({url: '/groups', invalidate: '/groups'}) | ||
const deleteGroup = useMutate({url: `/groups/${group?.id}`, method: 'delete', invalidate: '/groups'}) | ||
const editGroup = useMutate<Partial<PyGridUserGroup>, PyGridUserGroup>({ | ||
url: `groups/${group?.id}`, | ||
method: 'delete', | ||
invalidate: '/groups' | ||
}) | ||
|
||
const closePanel = () => { | ||
close() | ||
create.reset() | ||
} | ||
|
||
const submit = values => { | ||
create.mutate(values as Pick<PyGridUserGroup, 'name'>, {onSuccess: close}) | ||
} | ||
|
||
const edit = values => { | ||
editGroup.mutate(values as Partial<PyGridUserGroup>, {onSuccess: () => setGroup(null)}) | ||
} | ||
|
||
const remove = () => { | ||
deleteGroup.mutate(null, {onSuccess: () => setGroup(null)}) | ||
} | ||
|
||
return ( | ||
<article> | ||
<header> | ||
<h1>User groups</h1> | ||
<p className="subtitle">Manage all user groups permissions</p> | ||
</header> | ||
<section className="mt-6 overflow-hidden bg-white shadow sm:rounded-md"> | ||
<header> | ||
<div className="flex items-center justify-between px-4 py-4 text-xs font-medium tracking-wider text-gray-500 uppercase bg-gray-100 border-b border-gray-200 sm:px-6"> | ||
<div className="flex flex-col sm:flex-row"> | ||
<h2 className="flex-shrink-0 mr-2">User groups</h2> | ||
<div className="flex-shrink-0"> | ||
{groups?.length > 0 && ( | ||
<p> | ||
({groups.length} group{groups.length > 1 && 's'}) | ||
</p> | ||
)} | ||
{isLoading && <Spinner className="w-4" />} | ||
</div> | ||
</div> | ||
<div className="text-right"> | ||
<button className="btn" onClick={open}> | ||
<span className="hidden sm:inline-block">Add Group</span> | ||
<span className="sm:hidden">Add</span> | ||
</button> | ||
</div> | ||
</div> | ||
</header> | ||
<ul className="divide-y divide-gray-200"> | ||
{groups?.map((group: PyGridUserGroup) => ( | ||
<GroupListItem | ||
key={group.id} | ||
name={group.name} | ||
avatars={take( | ||
group.users.map( | ||
({email}) => `https://www.avatarapi.com/js.aspx?email=${email.trim().toLowerCase()}&size=128` | ||
), | ||
4 | ||
)} | ||
onClick={() => setGroup(group)} | ||
/> | ||
))} | ||
</ul> | ||
</section> | ||
{!isLoading && isError && ( | ||
<div className="mt-4"> | ||
<VisuallyHidden>An error occurred.</VisuallyHidden> | ||
<Alert | ||
error="It was not possible to get the list of user groups. Please check if the Domain API is reachable." | ||
description={error.message ?? 'Check your connection status'} | ||
/> | ||
</div> | ||
)} | ||
<SidePanel isOpen={isOpen} close={closePanel}> | ||
<article className="p-4 pr-8 space-y-6"> | ||
<section> | ||
<header> | ||
<h3 className="text-2xl font-medium text-gray-900 leading-6">Create a new group</h3> | ||
<p className="mt-2 text-sm text-gray-500">Add a name for the group.</p> | ||
</header> | ||
</section> | ||
<form onSubmit={handleSubmit(submit)}> | ||
<section className="flex flex-col space-y-4"> | ||
<Input name="name" label="Group name" ref={register} placeholder="Name" /> | ||
<div className="w-full sm:text-right"> | ||
<button | ||
className="w-full btn lg:w-auto transition-all ease-in-out duration-700" | ||
disabled={create.isLoading}> | ||
{create.isLoading ? <Spinner className="w-4 text-white" /> : 'Create a new group'} | ||
</button> | ||
</div> | ||
{create.isError && ( | ||
<div> | ||
<Alert error="There was an error creating the group" description={create.error.message} /> | ||
</div> | ||
)} | ||
</section> | ||
</form> | ||
</article> | ||
</SidePanel> | ||
<SidePanel isOpen={Boolean(group)} close={() => setGroup(null)}> | ||
<article className="p-4 pr-8 space-y-6"> | ||
<section> | ||
<header> | ||
<h3 className="text-2xl font-medium text-gray-900 leading-6">Modify an existing group</h3> | ||
<p className="mt-2 text-sm text-gray-500">Change its name and purge users from the group.</p> | ||
</header> | ||
</section> | ||
<form onSubmit={handleSubmit(submit)}> | ||
<section className="flex flex-col space-y-4"> | ||
<Input name="name" label="User Group" ref={register} defaultValue={group?.name} /> | ||
<p className="text-sm text-gray-400">Users in this group:</p> | ||
<div className="flex flex-col text-right lg:flex-row-reverse"> | ||
<button | ||
className="lg:ml-4 btn transition-all ease-in-out duration-700" | ||
disabled={create.isLoading} | ||
onClick={edit}> | ||
{create.isLoading ? <Spinner className="w-4 text-white" /> : 'Edit'} | ||
</button> | ||
<button | ||
className="mt-4 font-normal text-red-600 bg-white shadow-none lg:mt-0 btn transition-all ease-in-out duration-700 hover:bg-red-400 hover:text-white active:bg-red-700" | ||
disabled={create.isLoading} | ||
type="button" | ||
onClick={remove}> | ||
{create.isLoading ? <Spinner className="w-4 text-white" /> : 'Delete'} | ||
</button> | ||
</div> | ||
{create.isError && ( | ||
<div> | ||
<Alert error="There was an error creating the user" description={create.error.message} /> | ||
</div> | ||
)} | ||
</section> | ||
</form> | ||
</article> | ||
</SidePanel> | ||
{create.isSuccess && ( | ||
<Notification type="success"> | ||
<p>Group successfully {group ? 'created' : 'modified'}</p> | ||
</Notification> | ||
)} | ||
</article> | ||
) | ||
} | ||
|
||
export default UserGroups |
Oops, something went wrong.