Skip to content

Commit

Permalink
feat: revamp users, modals, introduced a bunch of new components and
Browse files Browse the repository at this point in the history
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
tcp committed Apr 1, 2021
1 parent c927f50 commit 39254b7
Show file tree
Hide file tree
Showing 10 changed files with 711 additions and 307 deletions.
51 changes: 51 additions & 0 deletions src/components/pages/users/group-list-item.tsx
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}
34 changes: 34 additions & 0 deletions src/components/pages/users/user-list-item.tsx
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}
75 changes: 75 additions & 0 deletions src/components/side-panel.tsx
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}
170 changes: 170 additions & 0 deletions src/pages/users/groups.tsx
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
Loading

0 comments on commit 39254b7

Please sign in to comment.