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:
Valentino 2022-02-10 13:34:44 +00:00
commit ae61e7bf94
15 changed files with 351 additions and 141 deletions

View file

@ -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,11 +20,18 @@ 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();
const currentUserModalOpen = () => {
setCurrentUserModal(true);
};
const currentUserModalClose = () => setCurrentUserModal(false);
return ( return (
<>
<Disclosure as="nav" className="bg-white shadow relative z-10"> <Disclosure as="nav" className="bg-white shadow relative z-10">
{({ open }) => ( {({ open }) => (
<div className="relative"> <div className="relative">
@ -41,10 +49,10 @@ const Header: React.FC<HeaderProps> = () => {
</Disclosure.Button> </Disclosure.Button>
</div> </div>
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start"> <div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
<div className="flex-shrink-0 flex items-center"> <Link to="/" className="flex-shrink-0 flex items-center">
<img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" /> <img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" />
<img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" /> <img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" />
</div> </Link>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8"> <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" */} {/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
{navigation.map((item) => ( {navigation.map((item) => (
@ -71,7 +79,7 @@ const Header: React.FC<HeaderProps> = () => {
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"> <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="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"> <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://" /> <Gravatar email={currentUser.email || ''} size={32} rating="pg" protocol="https://" />
</span> </span>
</Menu.Button> </Menu.Button>
</div> </div>
@ -88,7 +96,7 @@ const Header: React.FC<HeaderProps> = () => {
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<a <a
onClick={() => logOut()} onClick={() => currentUserModalOpen()}
className={classNames( className={classNames(
active ? 'bg-gray-100 cursor-pointer' : '', active ? 'bg-gray-100 cursor-pointer' : '',
'block px-4 py-2 text-sm text-gray-700 cursor-pointer', 'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
@ -124,16 +132,14 @@ const Header: React.FC<HeaderProps> = () => {
<Link <Link
key={item.name} key={item.name}
to={item.to} to={item.to}
// getProps={({ isCurrent }) => { className={clsx(
// // the object returned here is passed to the 'border-transparent text-gray-500 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium',
// // anchor element's props {
// return { 'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2': pathname.includes(
// className: isCurrent item.to,
// ? 'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium' ),
// : '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', },
// 'aria-current': isCurrent ? 'page' : undefined, )}
// };
// }}
> >
{item.name} {item.name}
</Link> </Link>
@ -143,6 +149,9 @@ const Header: React.FC<HeaderProps> = () => {
</div> </div>
)} )}
</Disclosure> </Disclosure>
<CurrentUserModal open={currentUserModal} onClose={currentUserModalClose} user={currentUser} />
</>
); );
}; };

View file

@ -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>
);
};

View 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',
},
];

View file

@ -0,0 +1 @@
export { CurrentUserModal } from './CurrentUserModal';

View file

@ -0,0 +1,7 @@
import { CurrentUser } from 'src/services/auth';
export type UserModalProps = {
open: boolean;
onClose: () => void;
user: CurrentUser;
};

View file

@ -0,0 +1 @@
export { CurrentUserModal } from './CurrentUserModal';

View file

@ -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'),
); );

View file

@ -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>

View file

@ -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

View file

@ -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,
}; };

View file

@ -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';

View file

@ -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,
); );

View file

@ -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;

View file

@ -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;
} }

View file

@ -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) {