Merge branch 'feat/logged-in-user-data' into 'main'
Feat/logged in user data See merge request stackspin/dashboard!21
This commit is contained in:
commit
ae61e7bf94
15 changed files with 351 additions and 141 deletions
|
@ -1,10 +1,11 @@
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import { Disclosure, Menu, Transition } from '@headlessui/react';
|
import { Disclosure, Menu, Transition } from '@headlessui/react';
|
||||||
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
||||||
import { useAuth } from 'src/services/auth';
|
import { useAuth } from 'src/services/auth';
|
||||||
import Gravatar from 'react-gravatar';
|
import Gravatar from 'react-gravatar';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { CurrentUserModal } from './components/CurrentUserModal';
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Dashboard', to: '/dashboard' },
|
{ name: 'Dashboard', to: '/dashboard' },
|
||||||
|
@ -19,130 +20,138 @@ function classNames(...classes: any[]) {
|
||||||
interface HeaderProps {}
|
interface HeaderProps {}
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = () => {
|
const Header: React.FC<HeaderProps> = () => {
|
||||||
const { logOut } = useAuth();
|
const [currentUserModal, setCurrentUserModal] = useState(false);
|
||||||
|
const { logOut, currentUser } = useAuth();
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
return (
|
const currentUserModalOpen = () => {
|
||||||
<Disclosure as="nav" className="bg-white shadow relative z-10">
|
setCurrentUserModal(true);
|
||||||
{({ open }) => (
|
};
|
||||||
<div className="relative">
|
const currentUserModalClose = () => setCurrentUserModal(false);
|
||||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
|
||||||
<div className="relative flex justify-between h-16">
|
|
||||||
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
|
||||||
{/* Mobile menu button */}
|
|
||||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
{open ? (
|
|
||||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</Disclosure.Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
|
||||||
<div className="flex-shrink-0 flex items-center">
|
|
||||||
<img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" />
|
|
||||||
<img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" />
|
|
||||||
</div>
|
|
||||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
|
||||||
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
to={item.to}
|
|
||||||
className={clsx(
|
|
||||||
'border-primary-50 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium',
|
|
||||||
{
|
|
||||||
'border-primary-500 text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium':
|
|
||||||
pathname.includes(item.to),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
|
||||||
{/* Profile dropdown */}
|
|
||||||
<Menu as="div" className="ml-3 relative">
|
|
||||||
<div>
|
|
||||||
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
|
||||||
<span className="sr-only">Open user menu</span>
|
|
||||||
<span className="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500 overflow-hidden">
|
|
||||||
<Gravatar email="" size={32} rating="pg" protocol="https://" />
|
|
||||||
</span>
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<a
|
|
||||||
onClick={() => logOut()}
|
|
||||||
className={classNames(
|
|
||||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
|
||||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Configure profile
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<a
|
|
||||||
onClick={() => logOut()}
|
|
||||||
className={classNames(
|
|
||||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
|
||||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Sign out
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Disclosure.Panel className="sm:hidden">
|
return (
|
||||||
<div className="pt-2 pb-4 space-y-1">
|
<>
|
||||||
{navigation.map((item) => (
|
<Disclosure as="nav" className="bg-white shadow relative z-10">
|
||||||
<Link
|
{({ open }) => (
|
||||||
key={item.name}
|
<div className="relative">
|
||||||
to={item.to}
|
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||||
// getProps={({ isCurrent }) => {
|
<div className="relative flex justify-between h-16">
|
||||||
// // the object returned here is passed to the
|
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||||
// // anchor element's props
|
{/* Mobile menu button */}
|
||||||
// return {
|
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
|
||||||
// className: isCurrent
|
<span className="sr-only">Open main menu</span>
|
||||||
// ? 'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium'
|
{open ? (
|
||||||
// : 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium',
|
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||||
// 'aria-current': isCurrent ? 'page' : undefined,
|
) : (
|
||||||
// };
|
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||||
// }}
|
)}
|
||||||
>
|
</Disclosure.Button>
|
||||||
{item.name}
|
</div>
|
||||||
</Link>
|
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||||
))}
|
<Link to="/" className="flex-shrink-0 flex items-center">
|
||||||
|
<img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" />
|
||||||
|
<img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" />
|
||||||
|
</Link>
|
||||||
|
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||||
|
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
to={item.to}
|
||||||
|
className={clsx(
|
||||||
|
'border-primary-50 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium',
|
||||||
|
{
|
||||||
|
'border-primary-500 text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium':
|
||||||
|
pathname.includes(item.to),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||||
|
{/* Profile dropdown */}
|
||||||
|
<Menu as="div" className="ml-3 relative">
|
||||||
|
<div>
|
||||||
|
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||||
|
<span className="sr-only">Open user menu</span>
|
||||||
|
<span className="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500 overflow-hidden">
|
||||||
|
<Gravatar email={currentUser.email || ''} size={32} rating="pg" protocol="https://" />
|
||||||
|
</span>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
onClick={() => currentUserModalOpen()}
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Configure profile
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
onClick={() => logOut()}
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Panel>
|
|
||||||
</div>
|
<Disclosure.Panel className="sm:hidden">
|
||||||
)}
|
<div className="pt-2 pb-4 space-y-1">
|
||||||
</Disclosure>
|
{navigation.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
to={item.to}
|
||||||
|
className={clsx(
|
||||||
|
'border-transparent text-gray-500 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium',
|
||||||
|
{
|
||||||
|
'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2': pathname.includes(
|
||||||
|
item.to,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
|
||||||
|
<CurrentUserModal open={currentUserModal} onClose={currentUserModalClose} user={currentUser} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { Modal, Banner } from 'src/components';
|
||||||
|
import { Input } from 'src/components/Form';
|
||||||
|
import { useUsers } from 'src/services/users';
|
||||||
|
import { CurrentUser } from 'src/services/auth';
|
||||||
|
import { appAccessList } from './consts';
|
||||||
|
import { UserModalProps } from './types';
|
||||||
|
|
||||||
|
export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => {
|
||||||
|
const { editUserById, userModalLoading } = useUsers();
|
||||||
|
|
||||||
|
const { control, reset, handleSubmit } = useForm<CurrentUser>({
|
||||||
|
defaultValues: {
|
||||||
|
name: null,
|
||||||
|
email: null,
|
||||||
|
id: null,
|
||||||
|
preferredUsername: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
reset(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset({ name: null, email: null, id: null });
|
||||||
|
};
|
||||||
|
}, [user, reset]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
if (user) {
|
||||||
|
await handleSubmit((data) => editUserById(data))();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch (e: any) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: any) => {
|
||||||
|
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={userModalLoading} useCancelButton>
|
||||||
|
<div className="bg-white px-4">
|
||||||
|
<div className="space-y-4 divide-y divide-gray-200">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Profile</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||||
|
<div className="sm:col-span-3">
|
||||||
|
<Input control={control} name="name" label="Name" onKeyPress={handleKeyPress} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-3">
|
||||||
|
<Input control={control} name="email" label="Email" type="email" onKeyPress={handleKeyPress} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-6">
|
||||||
|
<Banner title="Editing user status, roles and app access coming soon." titleSm="Comming soon!" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
|
||||||
|
{/* <Select control={control} name="status" label="Status" options={['Active', 'Inactive']} /> */}
|
||||||
|
<label htmlFor="status" className="block text-sm font-medium text-gray-700">
|
||||||
|
Status
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
name="status"
|
||||||
|
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option>Active</option>
|
||||||
|
<option>Inactive</option>
|
||||||
|
<option>Banned</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
|
||||||
|
<label htmlFor="status" className="block text-sm font-medium text-gray-700">
|
||||||
|
Role
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
name="status"
|
||||||
|
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option>User</option>
|
||||||
|
<option>Admin</option>
|
||||||
|
<option>Super Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="opacity-40 cursor-default pointer-events-none select-none">
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flow-root mt-6">
|
||||||
|
<ul className="-my-5 divide-y divide-gray-200 ">
|
||||||
|
{appAccessList.map((app: any) => {
|
||||||
|
return (
|
||||||
|
<li className="py-4" key={app.name}>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="flex-shrink-0 flex-1 flex items-center">
|
||||||
|
<img className="h-10 w-10 rounded-md overflow-hidden" src={app.image} alt={app.name} />
|
||||||
|
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">{app.name}</h3>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
id={app.name}
|
||||||
|
name={app.name}
|
||||||
|
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option>User</option>
|
||||||
|
<option>Admin</option>
|
||||||
|
<option>Super Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
18
src/components/Header/components/CurrentUserModal/consts.ts
Normal file
18
src/components/Header/components/CurrentUserModal/consts.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export const appAccessList = [
|
||||||
|
{
|
||||||
|
image: '/assets/wekan.svg',
|
||||||
|
name: 'Wekan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: '/assets/wordpress.svg',
|
||||||
|
name: 'Wordpress',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: '/assets/nextcloud.svg',
|
||||||
|
name: 'NextCloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: '/assets/zulip.svg',
|
||||||
|
name: 'Zulip',
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1 @@
|
||||||
|
export { CurrentUserModal } from './CurrentUserModal';
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { CurrentUser } from 'src/services/auth';
|
||||||
|
|
||||||
|
export type UserModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
user: CurrentUser;
|
||||||
|
};
|
1
src/components/Header/components/index.ts
Normal file
1
src/components/Header/components/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { CurrentUserModal } from './CurrentUserModal';
|
|
@ -40,14 +40,12 @@ axios.interceptors.response.use(
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<Provider store={store}>
|
||||||
<Provider store={store}>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
<BrowserRouter>
|
||||||
<BrowserRouter>
|
<App />
|
||||||
<App />
|
</BrowserRouter>
|
||||||
</BrowserRouter>
|
</PersistGate>
|
||||||
</PersistGate>
|
</Provider>,
|
||||||
</Provider>
|
|
||||||
</React.StrictMode>,
|
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
);
|
);
|
||||||
|
|
|
@ -66,7 +66,7 @@ export const Users: React.FC = () => {
|
||||||
const { row } = props;
|
const { row } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-right opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button
|
<button
|
||||||
onClick={() => configureModalOpen(row.original.id)}
|
onClick={() => configureModalOpen(row.original.id)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -90,7 +90,7 @@ export const Users: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 h-full flex-grow">
|
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
|
||||||
<div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
<div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
||||||
<h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1>
|
<h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1>
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
@ -144,9 +144,9 @@ export const Users: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="-my-2 sm:-mx-6 lg:-mx-8">
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
<div className="shadow border-b border-gray-200 sm:rounded-lg">
|
<div className="shadow border-b border-gray-200 sm:rounded-lg overflow-hidden">
|
||||||
<Table data={filterSearch as any} columns={columns} getSelectedRowIds={selectedRows} selectable />
|
<Table data={filterSearch as any} columns={columns} getSelectedRowIds={selectedRows} selectable />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,11 +5,13 @@ import { Modal, Banner } from 'src/components';
|
||||||
import { Input } from 'src/components/Form';
|
import { Input } from 'src/components/Form';
|
||||||
import { useUsers } from 'src/services/users';
|
import { useUsers } from 'src/services/users';
|
||||||
import { CurrentUserState } from 'src/services/users/redux';
|
import { CurrentUserState } from 'src/services/users/redux';
|
||||||
|
import { useAuth } from 'src/services/auth';
|
||||||
import { appAccessList } from './consts';
|
import { appAccessList } from './consts';
|
||||||
import { UserModalProps } from './types';
|
import { UserModalProps } from './types';
|
||||||
|
|
||||||
export const UserModal = ({ open, onClose, userId }: UserModalProps) => {
|
export const UserModal = ({ open, onClose, userId }: UserModalProps) => {
|
||||||
const { user, loadUser, editUserById, createNewUser, userModalLoading, deleteUserById } = useUsers();
|
const { user, loadUser, editUserById, createNewUser, userModalLoading, deleteUserById } = useUsers();
|
||||||
|
const { currentUser } = useAuth();
|
||||||
|
|
||||||
const { control, reset, handleSubmit } = useForm<CurrentUserState>({
|
const { control, reset, handleSubmit } = useForm<CurrentUserState>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
@ -78,11 +80,12 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => {
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
isLoading={userModalLoading}
|
isLoading={userModalLoading}
|
||||||
leftActions={
|
leftActions={
|
||||||
userId && (
|
userId &&
|
||||||
|
user.email !== currentUser.email && (
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
className="mb-4 sm:mb-0 inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||||
>
|
>
|
||||||
<TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
<TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
Delete
|
Delete
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { getAuthToken, signIn, signOut } from '../redux';
|
import { getAuthToken, getCurrentUser, signIn, signOut } from '../redux';
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const currentUser = useSelector(getCurrentUser);
|
||||||
const authToken = useSelector(getAuthToken);
|
const authToken = useSelector(getAuthToken);
|
||||||
|
|
||||||
const logIn = useCallback(
|
const logIn = useCallback(
|
||||||
|
@ -19,6 +20,7 @@ export function useAuth() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authToken,
|
authToken,
|
||||||
|
currentUser,
|
||||||
logIn,
|
logIn,
|
||||||
logOut,
|
logOut,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export { signIn, signOut, AuthActionTypes } from './actions';
|
export { signIn, signOut, AuthActionTypes } from './actions';
|
||||||
export { default as reducer } from './reducers';
|
export { default as reducer } from './reducers';
|
||||||
export { getAuth, getIsAuthLoading, getAuthToken } from './selectors';
|
export { getAuth, getIsAuthLoading, getAuthToken, getCurrentUser } from './selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
|
@ -2,15 +2,24 @@ import { createApiReducer, chainReducers, INITIAL_API_STATUS } from 'src/service
|
||||||
|
|
||||||
import { AuthState } from './types';
|
import { AuthState } from './types';
|
||||||
import { AuthActionTypes } from './actions';
|
import { AuthActionTypes } from './actions';
|
||||||
|
import { CurrentUser } from '../types';
|
||||||
|
|
||||||
|
const initialCurrentUserState: CurrentUser = {
|
||||||
|
email: null,
|
||||||
|
name: null,
|
||||||
|
preferredUsername: null,
|
||||||
|
id: null,
|
||||||
|
};
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
token: null,
|
token: null,
|
||||||
|
userInfo: initialCurrentUserState,
|
||||||
_status: INITIAL_API_STATUS,
|
_status: INITIAL_API_STATUS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const auth = createApiReducer(
|
const auth = createApiReducer(
|
||||||
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
||||||
(data) => ({ token: data.accessToken }),
|
(data) => ({ token: data.accessToken, userInfo: data.userInfo }),
|
||||||
(data) => data.error.message,
|
(data) => data.error.message,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ export const getAuth = (state: State) => state.auth;
|
||||||
|
|
||||||
export const getAuthToken = (state: State) => state.auth.token;
|
export const getAuthToken = (state: State) => state.auth.token;
|
||||||
|
|
||||||
|
export const getCurrentUser = (state: State) => state.auth.userInfo;
|
||||||
|
|
||||||
export const getIsAuthLoading = (state: State) => isLoading(getAuth(state));
|
export const getIsAuthLoading = (state: State) => isLoading(getAuth(state));
|
||||||
|
|
||||||
export const getToken = (state: State) => state.auth.token;
|
export const getToken = (state: State) => state.auth.token;
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
export interface Auth {
|
export interface Auth {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
userInfo: CurrentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentUser {
|
||||||
|
email: string | null;
|
||||||
|
name: string | null;
|
||||||
|
preferredUsername: string | null;
|
||||||
|
id: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ export const updateUserById = (user: any) => async (dispatch: Dispatch<any>) =>
|
||||||
payload: transformResponseUser(data),
|
payload: transformResponseUser(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
showToast('User updated sucessfully.', ToastType.Success);
|
showToast('User updated successfully.', ToastType.Success);
|
||||||
|
|
||||||
dispatch(fetchUsers());
|
dispatch(fetchUsers());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -95,7 +95,7 @@ export const createUser = (user: any) => async (dispatch: Dispatch<any>) => {
|
||||||
payload: transformResponseUser(data),
|
payload: transformResponseUser(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
showToast('User created sucessfully.', ToastType.Success);
|
showToast('User created successfully.', ToastType.Success);
|
||||||
|
|
||||||
dispatch(fetchUsers());
|
dispatch(fetchUsers());
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -121,7 +121,7 @@ export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => {
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
showToast('User deleted sucessfully.', ToastType.Success);
|
showToast('User deleted successfully.', ToastType.Success);
|
||||||
|
|
||||||
dispatch(fetchUsers());
|
dispatch(fetchUsers());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
Loading…
Reference in a new issue