remove stuff

This commit is contained in:
Philipp Rothmann 2022-09-12 14:47:57 +02:00
parent 24724c1f90
commit b43552c5eb
53 changed files with 26 additions and 2316 deletions

View file

@ -1,2 +1 @@
REACT_APP_API_URL=http://stackspin_proxy:8081/api/v1
REACT_APP_HYDRA_PUBLIC_URL=https://sso.init.stackspin.net REACT_APP_HYDRA_PUBLIC_URL=https://sso.init.stackspin.net

View file

@ -1,277 +0,0 @@
import React, { useEffect, useState } from 'react';
import _ from 'lodash';
import { TrashIcon } from '@heroicons/react/outline';
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
import { Banner, Modal, ConfirmationModal } from 'src/components';
import { Input, Select } from 'src/components/Form';
import { User, UserRole, useUsers } from 'src/services/users';
import { useAuth } from 'src/services/auth';
import { appAccessList, initialUserForm } from './consts';
import { UserModalProps } from './types';
export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) => {
const [deleteModal, setDeleteModal] = useState(false);
const [isAdminRoleSelected, setAdminRoleSelected] = useState(true);
const [isPersonalModal, setPersonalModal] = useState(false);
const {
user,
loadUser,
loadPersonalInfo,
editUserById,
editPersonalInfo,
createNewUser,
userModalLoading,
deleteUserById,
clearSelectedUser,
} = useUsers();
const { currentUser, isAdmin } = useAuth();
const { control, reset, handleSubmit } = useForm<User>({
defaultValues: initialUserForm,
});
const { fields, update } = useFieldArray({
control,
name: 'app_roles',
});
useEffect(() => {
if (userId) {
const currentUserId = currentUser?.id;
if (currentUserId === userId) {
setPersonalModal(true);
loadPersonalInfo();
} else {
loadUser(userId);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, open]);
useEffect(() => {
if (!_.isEmpty(user)) {
reset(user);
}
return () => {
reset(initialUserForm);
};
}, [user, reset, open]);
const dashboardRole = useWatch({
control,
name: 'app_roles.0.role',
});
useEffect(() => {
const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin;
setAdminRoleSelected(isAdminDashboardRoleSelected);
if (isAdminDashboardRoleSelected) {
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin }));
} else {
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.User }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardRole]);
const handleSave = async () => {
try {
if (isPersonalModal) {
await handleSubmit((data) => editPersonalInfo(data))();
} else if (userId) {
await handleSubmit((data) => editUserById(data))();
} else {
await handleSubmit((data) => createNewUser(data))();
}
} catch (e: any) {
// Continue
}
onClose();
clearSelectedUser();
setUserId(null);
};
const handleKeyPress = (e: any) => {
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
handleSave();
}
};
const handleClose = () => {
onClose();
clearSelectedUser();
setUserId(null);
};
const deleteModalOpen = () => setDeleteModal(true);
const deleteModalClose = () => setDeleteModal(false);
const handleDelete = () => {
if (userId) {
deleteUserById(userId);
}
clearSelectedUser();
setUserId(null);
handleClose();
deleteModalClose();
};
return (
<>
<Modal
onClose={handleClose}
open={open}
onSave={handleSave}
isLoading={userModalLoading}
leftActions={
userId &&
user.email !== currentUser?.email && (
<button
onClick={deleteModalOpen}
type="button"
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" />
Delete
</button>
)
}
useCancelButton
>
<div className="bg-white px-4">
<div className="space-y-10 divide-y divide-gray-200">
<div>
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">{userId ? 'Edit user' : 'Add new user'}</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} required={false} />
</div>
<div className="sm:col-span-3">
<Input
control={control}
name="email"
label="Email"
type="email"
onKeyPress={handleKeyPress}
required
/>
</div>
{isAdmin && (
<>
<div className="sm:col-span-3">
{fields
.filter((field) => field.name === 'dashboard')
.map((item, index) => (
<Select
key={item.name}
control={control}
name={`app_roles.${index}.role`}
label="Role"
options={[
{ value: UserRole.User, name: 'User' },
{ value: UserRole.Admin, name: 'Admin' },
]}
/>
))}
</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">
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>
</div>
{isAdmin && !userModalLoading && (
<div>
<div className="mt-8">
<h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
</div>
{isAdminRoleSelected && (
<div className="sm:col-span-6">
<Banner
title="Admin users automatically have admin-level access to all apps."
titleSm="Admin user"
/>
</div>
)}
{!isAdminRoleSelected && (
<div>
<div className="flow-root mt-6">
<ul className="-my-5 divide-y divide-gray-200">
{fields.map((item, index) => {
if (item.name === 'dashboard') {
return null;
}
return (
<li className="py-4" key={item.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={_.find(appAccessList, ['name', item.name!])?.image}
alt={item.name ?? 'Image'}
/>
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
{_.find(appAccessList, ['name', item.name!])?.label}
</h3>
</div>
<div>
<Select
key={item.id}
control={control}
name={`app_roles.${index}.role`}
disabled={isAdminRoleSelected}
options={[
{ value: UserRole.NoAccess, name: 'No Access' },
{ value: UserRole.User, name: 'User' },
{ value: UserRole.Admin, name: 'Admin' },
]}
/>
</div>
</div>
</li>
);
})}
</ul>
</div>
</div>
)}
</div>
)}
</div>
</div>
</Modal>
<ConfirmationModal
onDeleteAction={handleDelete}
open={deleteModal}
onClose={deleteModalClose}
title="Delete user"
body="Are you sure you want to delete this user? All of the user data will be permanently removed. This action cannot be undone."
/>
</>
);
};

View file

@ -1,64 +0,0 @@
import { UserRole } from 'src/services/users';
export const appAccessList = [
{
name: 'wekan',
image: '/assets/wekan.svg',
label: 'Wekan',
},
{
name: 'wordpress',
image: '/assets/wordpress.svg',
label: 'Wordpress',
},
{
name: 'nextcloud',
image: '/assets/nextcloud.svg',
label: 'Nextcloud',
},
{
name: 'zulip',
image: '/assets/zulip.svg',
label: 'Zulip',
},
];
export const allAppAccessList = [
{
name: 'dashboard',
image: '/assets/logo-small.svg',
label: 'Dashboard',
},
...appAccessList,
];
export const initialAppRoles = [
{
name: 'dashboard',
role: UserRole.User,
},
{
name: 'wekan',
role: UserRole.User,
},
{
name: 'wordpress',
role: UserRole.User,
},
{
name: 'nextcloud',
role: UserRole.User,
},
{
name: 'zulip',
role: UserRole.User,
},
];
export const initialUserForm = {
id: '',
name: '',
email: '',
app_roles: initialAppRoles,
status: '',
};

View file

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

View file

@ -1,6 +0,0 @@
export type UserModalProps = {
open: boolean;
onClose: () => void;
userId: string | null;
setUserId: any;
};

View file

@ -4,5 +4,4 @@ export { Table } from './Table';
export { Banner } from './Banner'; export { Banner } from './Banner';
export { Tabs } from './Tabs'; export { Tabs } from './Tabs';
export { Modal, ConfirmationModal, StepsModal } from './Modal'; export { Modal, ConfirmationModal, StepsModal } from './Modal';
export { UserModal } from './UserModal';
export { ProgressSteps } from './ProgressSteps'; export { ProgressSteps } from './ProgressSteps';

View file

@ -1,51 +1,12 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import axios from 'axios';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import { isValid } from './services/api';
import { configureStore } from './redux';
import { AuthActionTypes } from './services/auth/redux';
import App from './App'; import App from './App';
import './index.css'; import './index.css';
const { store, persistor } = configureStore();
axios.interceptors.request.use(
(config) => {
const { auth } = store.getState();
if (isValid(auth)) {
config.headers.Authorization = `Bearer ${auth.token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
// when we receive 401 error, we sign out the logged in user
axios.interceptors.response.use(
(response: any) => response,
(error: any) => {
if (error.response !== undefined && error.response.status === 401) {
store.dispatch({ type: AuthActionTypes.SIGN_OUT, payload: error });
}
return Promise.reject(error);
},
);
ReactDOM.render( ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>,
</PersistGate>
</Provider>,
document.getElementById('root'), document.getElementById('root'),
); );

View file

@ -13,7 +13,7 @@ export const AppIframe: React.FC<any> = ({ app }: { app: any }) => {
overflow="hidden" overflow="hidden"
scrolling="no" scrolling="no"
title={app.name} title={app.name}
url={app.url} url={app.externalUrl}
/> />
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Modal } from 'src/components'; import { Modal } from 'src/components';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => { export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
const [readMoreVisible, setReadMoreVisible] = useState(false); const [readMoreVisible, setReadMoreVisible] = useState(false);
@ -17,9 +18,18 @@ export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
.catch(() => {}); .catch(() => {});
}, [app.markdownSrc]); }, [app.markdownSrc]);
const url = `/${app.internalUrl}`;
return ( return (
<> <>
<div className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0" key={app.name}> <Link
to={url}
// className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
<div
className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0"
key={app.name}
>
<div className="px-4 py-5 sm:p-6"> <div className="px-4 py-5 sm:p-6">
<div className="mr-4 flex items-center"> <div className="mr-4 flex items-center">
<img <img
@ -33,24 +43,8 @@ export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
</div> </div>
</div> </div>
</div> </div>
<div className="px-2.5 py-2.5 sm:px-4 flex justify-end">
<a
href={app.internalUrl}
rel="noreferrer"
className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Im Dashboard öffnen
</a>
<a
href={app.externalUrl}
target="_blank"
rel="noreferrer"
className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
neues Fenster
</a>
</div>
</div> </div>
</Link>
<Modal open={readMoreVisible} onClose={onReadMoreCloseClick} title={app.name}> <Modal open={readMoreVisible} onClose={onReadMoreCloseClick} title={app.name}>
<ReactMarkdown className="prose">{content}</ReactMarkdown> <ReactMarkdown className="prose">{content}</ReactMarkdown>

View file

@ -1,4 +1,2 @@
export { Login } from './login';
export { Dashboard } from './dashboard'; export { Dashboard } from './dashboard';
export { Apps, AppSingle } from './apps'; export { Apps, AppSingle } from './apps';
export { Users } from './users';

View file

@ -1,47 +0,0 @@
import React from 'react';
import clsx from 'clsx';
import { LockClosedIcon } from '@heroicons/react/solid';
import { performApiCall } from 'src/services/api';
import { showToast, ToastType } from 'src/common/util/show-toast';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function Login() {
const handleSubmit = async () => {
try {
const { data } = await performApiCall({
path: '/login',
method: 'POST',
});
if (data.authorizationUrl) {
window.location.href = data.authorizationUrl;
}
} catch (e: any) {
showToast('Something went wrong', ToastType.Error);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="flex justify-center">
<img className="lg:block" src="assets/logo.svg" alt="Stackspin" />
<h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Sign in</h2>
</div>
<button
onClick={handleSubmit}
type="button"
className={clsx(
'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-dark hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
)}
>
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
<LockClosedIcon className="h-5 w-5 text-white group-hover:text-primary-light" aria-hidden="true" />
</span>
Sign in
</button>
</div>
</div>
);
}

View file

@ -1,58 +0,0 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { showToast, ToastType } from 'src/common/util/show-toast';
import { useAuth } from 'src/services/auth';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function LoginCallback() {
const currentURL = window.location.href;
const indexOfQuestionMark = currentURL.indexOf('?');
const params = currentURL.slice(indexOfQuestionMark);
const navigate = useNavigate();
const { logIn } = useAuth();
useEffect(() => {
async function logInUser() {
if (params.length > 2) {
const res = await logIn(params);
// @ts-ignore
if (!res.ok) {
navigate('/login');
showToast('Something went wrong, please try logging in again.', ToastType.Error);
}
}
}
logInUser();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="flex flex-col justify-center items-center">
<div className="flex justify-center items-center border border-transparent text-base font-medium rounded-md text-white transition ease-in-out duration-150">
<svg
className="animate-spin h-10 w-10 text-primary"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-50" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-100"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
<p className="text-lg text-primary-600 mt-2">Logging You in, just a moment.</p>
</div>
</div>
</div>
);
}

View file

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

View file

@ -1,186 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { SearchIcon, PlusIcon, ViewGridAddIcon } from '@heroicons/react/solid';
import { CogIcon, TrashIcon } from '@heroicons/react/outline';
import { useUsers } from 'src/services/users';
import { Table } from 'src/components';
import { debounce } from 'lodash';
import { useAuth } from 'src/services/auth';
import { UserModal } from '../../components/UserModal';
import { MultipleUsersModal } from './components';
export const Users: React.FC = () => {
const [selectedRowsIds, setSelectedRowsIds] = useState({});
const [configureModal, setConfigureModal] = useState(false);
const [multipleUsersModal, setMultipleUsersModal] = useState(false);
const [userId, setUserId] = useState(null);
const [search, setSearch] = useState('');
const { users, loadUsers, userTableLoading } = useUsers();
const { isAdmin } = useAuth();
const handleSearch = (event: any) => {
setSearch(event.target.value);
};
const debouncedSearch = useCallback(debounce(handleSearch, 250), []);
useEffect(() => {
loadUsers();
return () => {
debouncedSearch.cancel();
};
}, []);
const filterSearch = useMemo(() => {
return users.filter((item: any) => item.email?.toLowerCase().includes(search.toLowerCase()));
}, [search, users]);
const configureModalOpen = (id: any) => {
setUserId(id);
setConfigureModal(true);
};
const configureModalClose = () => setConfigureModal(false);
const multipleUsersModalClose = () => setMultipleUsersModal(false);
const columns: any = React.useMemo(
() => [
{
Header: 'Name',
accessor: 'name',
width: 'auto',
},
{
Header: 'Email',
accessor: 'email',
width: 'auto',
},
{
Header: 'Status',
accessor: 'status',
width: 'auto',
},
{
Header: ' ',
Cell: (props: any) => {
const { row } = props;
if (isAdmin) {
return (
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => configureModalOpen(row.original.id)}
type="button"
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
<CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Configure
</button>
</div>
);
}
return null;
},
width: 'auto',
},
],
[isAdmin],
);
const selectedRows = useCallback((rows: Record<string, boolean>) => {
setSelectedRowsIds(rows);
}, []);
return (
<div className="relative">
<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">
<h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1>
{isAdmin && (
<div className="mt-3 sm:mt-0 sm:ml-4">
<button
onClick={() => configureModalOpen(null)}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
>
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new user
</button>
<button
onClick={() => setMultipleUsersModal(true)}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
>
<ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new users
</button>
</div>
)}
</div>
<div className="flex justify-between w-100 my-3 items-center mb-5 ">
<div className="flex items-center">
<div className="inline-block">
<label htmlFor="email" className="block text-sm font-medium text-gray-700 sr-only">
Search candidates
</label>
<div className="mt-1 flex rounded-md shadow-sm">
<div className="relative flex items-stretch flex-grow focus-within:z-10">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="email"
id="email"
className="focus:ring-primary-500 focus:border-primary-500 block w-full rounded-md pl-10 sm:text-sm border-gray-200"
placeholder="Search Users"
onChange={debouncedSearch}
/>
</div>
</div>
</div>
</div>
{selectedRowsIds && Object.keys(selectedRowsIds).length !== 0 && (
<div className="flex items-center">
<button
onClick={() => {}}
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"
>
<TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Delete
</button>
</div>
)}
</div>
<div className="flex flex-col">
<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="shadow border-b border-gray-200 sm:rounded-lg overflow-hidden">
<Table
data={filterSearch as any}
columns={columns}
getSelectedRowIds={selectedRows}
loading={userTableLoading}
/>
</div>
</div>
</div>
</div>
{configureModal && (
<UserModal open={configureModal} onClose={configureModalClose} userId={userId} setUserId={setUserId} />
)}
{multipleUsersModal && <MultipleUsersModal open={multipleUsersModal} onClose={multipleUsersModalClose} />}
</div>
</div>
);
};

View file

@ -1,248 +0,0 @@
import React, { useEffect, useState } from 'react';
import _ from 'lodash';
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
import { Banner, StepsModal, ProgressSteps } from 'src/components';
import { Select, TextArea } from 'src/components/Form';
import { MultipleUsersData, UserRole, useUsers } from 'src/services/users';
import { allAppAccessList } from 'src/components/UserModal/consts';
import { ProgressStepInfo, ProgressStepStatus } from 'src/components/ProgressSteps/types';
import { initialMultipleUsersForm, MultipleUsersModalProps } from './types';
export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) => {
const [steps, setSteps] = useState<ProgressStepInfo[]>([]);
const [isAdminRoleSelected, setAdminRoleSelected] = useState(false);
const { createUsers, userModalLoading } = useUsers();
const { control, handleSubmit } = useForm<MultipleUsersData>({
defaultValues: initialMultipleUsersForm,
});
const { fields, update } = useFieldArray({
control,
name: 'appRoles',
});
const dashboardRole = useWatch({
control,
name: 'appRoles.0.role',
});
const csvDataWatch = useWatch({
control,
name: 'csvUserData',
});
useEffect(() => {
const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin;
setAdminRoleSelected(isAdminDashboardRoleSelected);
if (isAdminDashboardRoleSelected) {
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin }));
} else {
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.User }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardRole]);
const renderUsersCsvDataInput = () => {
return (
<div>
<div className="mt-8">
<h3 className="text-lg leading-6 font-medium text-gray-900">CSV data</h3>
</div>
<div className="mt-6">
<TextArea
control={control}
name="csvUserData"
placeholder={`Please paste users in CSV format: email, name\nuser1@example.com,User One\nuser2@example.com,User Two`}
rows={15}
required
/>
</div>
</div>
);
};
const renderAppAccess = () => {
return (
<div>
<div className="mt-8">
<h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
</div>
{isAdminRoleSelected && (
<div className="sm:col-span-6">
<Banner title="Admin users automatically have admin-level access to all apps." titleSm="Admin user" />
</div>
)}
<div>
<div className="flow-root mt-6">
<ul className="-my-5 divide-y divide-gray-200">
{fields
.filter((field) => field.name === 'dashboard')
.map((item, index) => (
<li className="py-4" key={item.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={_.find(allAppAccessList, ['name', item.name!])?.image}
alt={item.name ?? 'Image'}
/>
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
{_.find(allAppAccessList, ['name', item.name!])?.label}
</h3>
</div>
<div className="sm:col-span-2">
<Select
key={item.id}
control={control}
name={`appRoles.${index}.role`}
options={[
{ value: UserRole.User, name: 'User' },
{ value: UserRole.Admin, name: 'Admin' },
]}
/>
</div>
</div>
</li>
))}
{!isAdminRoleSelected &&
fields.map((item, index) => {
if (item.name === 'dashboard') {
return null;
}
return (
<li className="py-4" key={item.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={_.find(allAppAccessList, ['name', item.name!])?.image}
alt={item.name ?? 'Image'}
/>
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
{_.find(allAppAccessList, ['name', item.name!])?.label}
</h3>
</div>
<div className="sm:col-span-2">
<Select
key={item.id}
control={control}
name={`appRoles.${index}.role`}
disabled={isAdminRoleSelected}
options={[
{ value: UserRole.NoAccess, name: 'No Access' },
{ value: UserRole.User, name: 'User' },
{ value: UserRole.Admin, name: 'Admin' },
]}
/>
</div>
</div>
</li>
);
})}
</ul>
</div>
</div>
</div>
);
};
useEffect(() => {
setSteps([
{
id: 'Step 1',
name: 'Enter CSV user data',
status: ProgressStepStatus.Current,
},
{
id: 'Step 2',
name: 'Define app access roles',
status: ProgressStepStatus.Upcoming,
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
const handleSave = async () => {
try {
await handleSubmit((data) => createUsers(data))();
} catch (e: any) {
// Continue
}
onClose();
};
const handleClose = () => {
onClose();
};
const getActiveStepIndex = () => _.findIndex(steps, { status: ProgressStepStatus.Current });
const updateStepsStatus = (nextIndex: number) => {
const updatedSteps = [...steps];
_.forEach(updatedSteps, (step, index) => {
if (index < nextIndex) {
step.status = ProgressStepStatus.Complete;
} else if (index === nextIndex) {
step.status = ProgressStepStatus.Current;
} else {
step.status = ProgressStepStatus.Upcoming;
}
});
setSteps(updatedSteps);
};
const handleStepClick = (stepId: string) => {
const activeStepIndex = _.findIndex(steps, { id: stepId });
updateStepsStatus(activeStepIndex);
};
const handleNext = () => {
const nextIndex = getActiveStepIndex() + 1;
updateStepsStatus(nextIndex);
};
const handlePrevious = () => {
const nextIndex = getActiveStepIndex() - 1;
updateStepsStatus(nextIndex);
};
const activeStepIndex = getActiveStepIndex();
const showSave = !_.some(steps, { status: ProgressStepStatus.Upcoming });
const showPrevious = _.some(steps, { status: ProgressStepStatus.Complete });
return (
<StepsModal
onClose={handleClose}
open={open}
onSave={handleSave}
onNext={handleNext}
onPrevious={handlePrevious}
showPreviousButton={showPrevious}
isLoading={userModalLoading}
useCancelButton
showSaveButton={showSave}
saveButtonDisabled={_.isEmpty(csvDataWatch)}
>
<div className="bg-white px-4">
<div className="space-y-10 divide-y divide-gray-200">
<div>
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">Add new users</h3>
</div>
<div className="sm:px-6 pt-6">
<ProgressSteps steps={steps} onStepClick={handleStepClick}>
{activeStepIndex === 0 ? renderUsersCsvDataInput() : renderAppAccess()}
</ProgressSteps>
</div>
</div>
</div>
</div>
</StepsModal>
);
};

View file

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

View file

@ -1,10 +0,0 @@
import { initialAppRoles } from 'src/components/UserModal/consts';
export type MultipleUsersModalProps = {
open: boolean;
onClose: () => void;
};
export const initialMultipleUsersForm = {
appRoles: initialAppRoles,
};

View file

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

View file

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

View file

@ -1,2 +0,0 @@
export { configureStore } from './store';
export * from './types';

View file

@ -1,35 +0,0 @@
import { createStore, compose, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { reducer as authReducer } from 'src/services/auth';
import usersReducer from 'src/services/users/redux/reducers';
import { State } from './types';
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth'],
};
const appReducer = combineReducers<State>({
auth: authReducer,
users: usersReducer,
});
const persistedReducer = persistReducer(persistConfig, appReducer);
const middlewares = [thunkMiddleware];
// @ts-ignore
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export const configureStore = () => {
const store = createStore(persistedReducer, composeEnhancers(applyMiddleware(...middlewares)));
const persistor = persistStore(store);
return { store, persistor };
};

View file

@ -1,11 +0,0 @@
import { Store } from 'redux';
import { AuthState } from 'src/services/auth/redux';
import { UsersState } from 'src/services/users/redux';
export interface AppStore extends Store, State {}
export interface State {
auth: AuthState;
users: UsersState;
}

View file

@ -1,42 +0,0 @@
import _ from 'lodash';
import axios from 'axios';
import { api } from './config';
import { ApiConfig } from './types';
/**
* Function which creates an API call for given api config
*/
export const createApiCall = (apiConfig: ApiConfig) => async () => {
const hostname = apiConfig.hostname || api.hostname;
const { path } = apiConfig;
const method = _.get(apiConfig, 'method', 'GET');
const contentType =
_.includes(['POST', 'PUT', 'PATCH'], method) && !apiConfig.formData
? {
'Content-Type': 'application/json',
}
: {};
const passedHeaders = _.get(apiConfig, 'headers', {});
const headers = {
...contentType,
...passedHeaders,
};
const axiosConfig = {
method,
headers,
url: `${hostname}${path}`,
timeout: 30000,
data: apiConfig.formData || apiConfig.body,
};
return axios(axiosConfig);
};
/**
* Performs an API call for given api config and returns a promise
*/
export const performApiCall = (apiConfig: ApiConfig) => {
const apiCall = createApiCall(apiConfig);
return apiCall();
};

View file

@ -1,3 +0,0 @@
export const api = {
hostname: process.env.REACT_APP_API_URL,
};

View file

@ -1,5 +0,0 @@
export * from './redux';
export { api } from './config';
export { createApiCall, performApiCall } from './apiCall';

View file

@ -1,186 +0,0 @@
import _ from 'lodash';
import urlcat from 'urlcat';
import { createApiCall } from '../apiCall';
import { StartAction, SuccessAction, FailureAction } from './types';
import { ApiConfig } from '../types';
/**
* Function which creates a thunk action for given api config and associated action types
* @param {string|Record<string, unknown>} apiConfig if string it is interprated as path in api endpoint
* @param {string[]} actionTypes action types for start, success and failure
*/
export const createApiAction = (apiConfig: ApiConfig, actionTypes: string[]) => async (dispatch: any) => {
const apiCall = createApiCall(apiConfig);
const startAction = (): StartAction => ({
type: actionTypes[0],
payload: null,
});
const successAction = (payload: any): SuccessAction => ({
type: actionTypes[1],
payload,
});
const failureAction = (error: string): FailureAction => ({
type: actionTypes[2],
payload: { error },
});
dispatch(startAction());
try {
const response = await apiCall();
let res;
const additionalData = apiConfig.additionalData || {};
if (!_.isEmpty(additionalData)) {
res = await dispatch(successAction({ ...response.data, ...additionalData }));
} else {
res = await dispatch(successAction(response.data));
}
return { ok: true, res };
} catch (e) {
const error = _.get(e, 'response.data', {
errorMessage: e.message || 'Undefined error, please try again.',
});
await dispatch(failureAction(error));
return {
ok: false,
errorMessage: error.message,
status: e?.response?.status,
};
}
};
/**
* Creates opinionated CRUD actions
*/
export function createCrudApiActions<T>(
basePath: string,
startActionType: string,
failureActionType: string,
fetchActionType: string,
addActionType: string,
updateActionType: string,
deleteActionType: string,
transformItemForApi: (data: T) => any,
pathTemplates: { load?: string; add?: string; change?: string } = {
load: '/',
add: '/',
change: '/:id',
},
): any {
const { load, add, change } = pathTemplates;
const fetchItems = (
pageNumber?: number,
pageSize?: number,
params: Record<string, unknown> = {},
template: string = load!,
) =>
createApiAction(
{
path: urlcat(basePath, template, { pageNumber, pageSize, ...params }),
method: 'GET',
},
[startActionType, fetchActionType, failureActionType],
);
const addItem = (item: T, params: Record<string, unknown> = {}) =>
createApiAction(
{
path: urlcat(basePath, add!, { ...params }),
method: 'POST',
body: transformItemForApi(item),
},
[startActionType, addActionType, failureActionType],
);
const updateItem = (item: T, params: Record<string, unknown> = {}) =>
createApiAction(
{
// @ts-ignore
path: urlcat(basePath, change!, { id: item.id, ...params }),
method: 'PUT',
body: transformItemForApi(item),
},
[startActionType, updateActionType, failureActionType],
);
const deleteItem = (itemId: number, params: Record<string, unknown> = {}) =>
createApiAction(
{
path: urlcat(basePath, change!, { id: itemId, ...params }),
method: 'DELETE',
},
[startActionType, deleteActionType, failureActionType],
);
return [fetchItems, addItem, updateItem, deleteItem];
}
/**
* Creates opinionated CRUD actions
*/
export function createCrudApiActionsWithoutPaging<T extends { id?: number | string | null }>(
basePath: string,
startActionType: string,
failureActionType: string,
fetchActionType: string,
addActionType: string,
updateActionType: string,
deleteActionType: string,
transformItemForApi: (data: T) => any,
pathTemplates: { load?: string; add?: string; change?: string } = {
load: '/',
add: '/',
change: '/:id',
},
): any {
const { load, add, change } = pathTemplates;
const fetchItems = (params: Record<string, unknown> = {}, template: string = load!) =>
createApiAction(
{
path: urlcat(basePath, template, params),
method: 'GET',
},
[startActionType, fetchActionType, failureActionType],
);
const addItem = (item: T, params: Record<string, unknown> = {}) =>
createApiAction(
{
path: urlcat(basePath, add!, { ...params }),
method: 'POST',
body: transformItemForApi(item),
},
[startActionType, addActionType, failureActionType],
);
const updateItem = (item: T, params: Record<string, unknown> = {}) =>
createApiAction(
{
path: urlcat(basePath, change!, { id: item.id, ...params }),
method: 'PUT',
body: transformItemForApi(item),
},
[startActionType, updateActionType, failureActionType],
);
const deleteItem = (itemId: number, params: Record<string, unknown> = {}) =>
createApiAction(
{
path: urlcat(basePath, change!, { id: itemId, ...params }),
method: 'DELETE',
},
[startActionType, deleteActionType, failureActionType],
);
return [fetchItems, addItem, updateItem, deleteItem];
}

View file

@ -1,14 +0,0 @@
export {
createApiReducer,
createCrudApiReducer,
createCrudApiReducerWithoutPaging,
chainReducers,
INITIAL_API_STATUS,
INITIAL_API_STATE,
} from './reducers';
export { createApiAction, createCrudApiActions, createCrudApiActionsWithoutPaging } from './actions';
export { isEmpty, isLoading, isValid, isError, getError, getErrorMessage } from './traits';
export * from './types';

View file

@ -1,176 +0,0 @@
import { StartAction, SuccessAction, FailureAction, ApiStatus, ApiState } from './types';
export const INITIAL_API_STATUS: ApiStatus = {
isEmpty: true,
isLoading: false,
isValid: false,
isError: false,
dateTime: Date.now(),
error: null,
errorMessage: null,
};
export const INITIAL_API_STATE: ApiState = {
_status: INITIAL_API_STATUS,
items: [],
};
type AllActions = StartAction | SuccessAction | FailureAction;
/**
* Use this function to create any reducer which handles api communication
* @param {Array} actionTypes Array of action types associated with fetchThunk e.g. USER_REQUEST,
* USER_SUCCESS and USER_ERROR
* @param {function} dataAdapter function which will be called to transform response body
* to format you want to use in state
* @param {function} errorAdapter function which will be called to transform error
* to format you want to use in state
* @returns {function} reducer handles api requests, saves response body as data and status
*/
export const createApiReducer =
(
actionTypes: string[],
dataAdapter: (data: any, state?: any) => any = (data: any) => data,
errorAdapter = (data: any) => data,
) =>
(state = INITIAL_API_STATE, action: AllActions) => {
const [REQUEST, SUCCESS, ERROR] = actionTypes;
switch (action.type) {
case REQUEST:
return {
...state,
_status: {
...state._status,
isLoading: true,
dateTime: Date.now(),
},
};
case SUCCESS:
return {
_status: {
isEmpty: false,
isLoading: false,
isValid: true,
isError: false,
dateTime: Date.now(),
error: null,
errorMessage: null,
},
...dataAdapter(action.payload, state),
};
case ERROR:
return {
...state,
_status: {
isEmpty: false,
isLoading: false,
isValid: false,
isError: true,
error: action.payload,
errorMessage: errorAdapter(action.payload),
},
};
default:
return state;
}
};
/**
* This is higher order reducer
* Use this if you want your state is combination of results of multiple reducers
* under the same key, e.g. you can get the same data from multiple endpoints
*/
export const chainReducers =
(initialState: any, ...args: any[]) =>
(state = initialState, action: any) =>
args.reduce((newState, reducer) => reducer(newState, action), state);
/**
* Creates an opinionated CRUD reducer
*/
export function createCrudApiReducer<T>(
startActionType: string,
failureActionType: string,
fetchActionType: string,
addActionType: string,
updateActionType: string,
deleteActionType: string,
transformItem: (data: any) => T,
keyField = 'id',
) {
const fetchReducer = createApiReducer(
[startActionType, fetchActionType, failureActionType],
(data) => ({ _meta: data.meta, items: (data.data || data).map(transformItem) }),
(data) => data,
);
const addReducer = createApiReducer(
[startActionType, addActionType, failureActionType],
(data, state) => ({ items: [...state.items, transformItem(data)] }),
(data) => data,
);
const updateReducer = createApiReducer(
[startActionType, updateActionType, failureActionType],
(data, state) => ({
items: state.items.map((i: any) => (i[keyField] === data[keyField] ? transformItem(data) : i)),
}),
(data) => data,
);
const deleteReducer = createApiReducer(
[startActionType, deleteActionType, failureActionType],
(data, state) => ({
items: state.items.filter((i: any) => i[keyField] !== data[keyField]),
}),
(data) => data,
);
return chainReducers(INITIAL_API_STATE, fetchReducer, addReducer, updateReducer, deleteReducer);
}
/**
* Creates an opinionated CRUD reducer without paging
*/
export function createCrudApiReducerWithoutPaging<T>(
startActionType: string,
failureActionType: string,
fetchActionType: string,
addActionType: string,
updateActionType: string,
deleteActionType: string,
transformItem: (data: any) => T,
keyField = 'id',
) {
const fetchReducer = createApiReducer(
[startActionType, fetchActionType, failureActionType],
(data) => ({ items: data.map(transformItem) }),
(data) => data,
);
const addReducer = createApiReducer(
[startActionType, addActionType, failureActionType],
(data, state) => ({ items: [...state.items, transformItem(data)] }),
(data) => data,
);
const updateReducer = createApiReducer(
[startActionType, updateActionType, failureActionType],
(data, state) => ({
items: state.items.map((i: any) => (i[keyField] === data[keyField] ? transformItem(data) : i)),
}),
(data) => data,
);
const deleteReducer = createApiReducer(
[startActionType, deleteActionType, failureActionType],
(data, state) => ({
items: state.items.filter((i: any) => i[keyField] !== data[keyField]),
}),
(data) => data,
);
return chainReducers(INITIAL_API_STATE, fetchReducer, addReducer, updateReducer, deleteReducer);
}

View file

@ -1,29 +0,0 @@
import _ from 'lodash';
export const isEmpty = (data: any): boolean => {
return _.get(data, '_status.isEmpty', true);
};
export const isLoading = (data: any): boolean => {
return _.get(data, '_status.isLoading', false);
};
export const isValid = (data: any): boolean => {
return _.get(data, '_status.isValid', false);
};
export const isError = (data: any): boolean => {
return _.get(data, '_status.isError', false);
};
export const getError = (data: any): any => {
return _.get(data, '_status.error', null);
};
export const getErrorMessage = (data: any): string | null => {
return _.get(data, '_status.errorMessage', null);
};
export const getTotalPages = (data: any): number | undefined => {
return _.get(data, '_meta.totalPages', undefined);
};

View file

@ -1,29 +0,0 @@
export interface StartAction {
type: string;
payload: any;
}
export interface SuccessAction {
type: string;
payload: any;
}
export interface FailureAction {
type: string;
payload: { error: string };
}
export interface ApiStatus {
isEmpty: boolean;
isLoading: boolean;
isValid: boolean;
isError: boolean;
dateTime: number;
error: any;
errorMessage: string | null;
}
export interface ApiState<T = any> {
_status: ApiStatus;
items: T[];
}

View file

@ -1,29 +0,0 @@
import { Method } from 'axios';
export interface ApiConfig {
hostname?: string;
path: string;
method?: Method;
headers?: any;
formData?: any;
body?: any;
additionalData?: any;
}
export interface SimpleSnackBarConfig {
serviceName: any;
}
export interface SnackBarConfigWithMessages {
getMessage: {
success(response: any): string;
error(error: any): string;
};
}
export type SnackBarConfig = SnackBarConfigWithMessages | SimpleSnackBarConfig;
export enum SortDirection {
Ascending = 'asc',
Descending = 'desc',
}

View file

@ -1,42 +0,0 @@
import { performApiCall } from 'src/services/api';
export const sendPasswordResetLink = async (email: string): Promise<boolean> => {
try {
await performApiCall({
path: `/auth/forgot-password`,
method: 'POST',
body: { email },
});
return true;
} catch {
return false;
}
};
export const resetPassword = async (email: string, code: string, password: string): Promise<boolean> => {
try {
await performApiCall({
path: `/auth/reset-password`,
method: 'POST',
body: { email, code, password },
});
return true;
} catch {
return false;
}
};
export const verifyEmail = async (email: string, activationCode: string): Promise<boolean> => {
try {
await performApiCall({
path: `/auth/activation`,
method: 'POST',
body: { email, activationCode },
});
return true;
} catch {
return false;
}
};

View file

@ -1 +0,0 @@
export { useAuth } from './use-auth';

View file

@ -1,29 +0,0 @@
import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getAuthToken, getCurrentUser, getIsAdmin, signIn, signOut } from '../redux';
export function useAuth() {
const dispatch = useDispatch();
const currentUser = useSelector(getCurrentUser);
const authToken = useSelector(getAuthToken);
const isAdmin = useSelector(getIsAdmin);
const logIn = useCallback(
(params) => {
return dispatch(signIn(params));
},
[dispatch],
);
const logOut = useCallback(() => {
return dispatch(signOut());
}, [dispatch]);
return {
authToken,
currentUser,
isAdmin,
logIn,
logOut,
};
}

View file

@ -1,3 +0,0 @@
export { useAuth } from './hooks';
export { getAuth, reducer, signIn, signOut, AuthActionTypes, getIsAuthLoading } from './redux';
export * from './types';

View file

@ -1,32 +0,0 @@
import { createApiAction } from 'src/services/api';
import { SuccessAction } from 'src/services/api/redux/types';
export enum AuthActionTypes {
SIGN_IN_START = 'auth/sign_in_start',
SIGN_IN_SUCCESS = 'auth/sign_in_success',
SIGN_IN_FAILURE = 'auth/sign_in_failure',
SIGN_OUT = 'auth/SIGN_OUT',
UPDATE_AUTH_USER = 'auth/update_auth_user',
REGISTRATION_START = 'auth/registration_start',
REGISTRATION_FAILURE = 'auth/registration_failure',
}
const signOutAction = (): SuccessAction => ({
type: AuthActionTypes.SIGN_OUT,
payload: null,
});
export const signIn = (params: string) =>
createApiAction(
{
path: `/hydra/callback${params}`,
method: 'GET',
},
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
);
export function signOut() {
return (dispatch: any) => {
dispatch(signOutAction());
};
}

View file

@ -1,4 +0,0 @@
export { signIn, signOut, AuthActionTypes } from './actions';
export { default as reducer } from './reducers';
export { getAuth, getIsAuthLoading, getAuthToken, getCurrentUser, getIsAdmin } from './selectors';
export * from './types';

View file

@ -1,47 +0,0 @@
import { createApiReducer, chainReducers, INITIAL_API_STATUS } from 'src/services/api';
import { User } from 'src/services/users';
import { AuthState } from './types';
import { AuthActionTypes } from './actions';
import { transformAuthUser } from '../transformations';
const initialCurrentUserState: User = {
email: '',
name: '',
id: '',
app_roles: [],
status: '',
preferredUsername: '',
};
const initialState: AuthState = {
token: null,
userInfo: initialCurrentUserState,
_status: INITIAL_API_STATUS,
};
const authLocalReducer = (state: any = initialState, action: any) => {
switch (action.type) {
case AuthActionTypes.UPDATE_AUTH_USER:
return {
...state,
userInfo: action.payload,
};
default:
return state;
}
};
const auth = createApiReducer(
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
(data) => transformAuthUser(data),
(data) => data.error.message,
);
const signOut = createApiReducer(
['', AuthActionTypes.SIGN_OUT, ''],
() => initialState,
() => initialState,
);
export default chainReducers(initialState, auth, signOut, authLocalReducer);

View file

@ -1,28 +0,0 @@
import { State } from 'src/redux';
import { isLoading } from 'src/services/api';
import { UserRole } from 'src/services/users';
export const getAuth = (state: State) => state.auth;
export const getAuthToken = (state: State) => state.auth.token;
export const getCurrentUser = (state: State) => state.auth.userInfo;
export const getIsAdmin = (state: State) => {
// check since old users wont have this
if (state.auth.userInfo) {
if (!state.auth.userInfo.app_roles) {
return false;
}
const isAdmin = state.auth.userInfo.app_roles.find(
(role) => role.name === 'dashboard' && role.role === UserRole.Admin,
);
return !!isAdmin;
}
return false;
};
export const getIsAuthLoading = (state: State) => isLoading(getAuth(state));
export const getToken = (state: State) => state.auth.token;

View file

@ -1,7 +0,0 @@
import { ApiStatus } from 'src/services/api/redux/types';
import { Auth } from '../types';
export interface AuthState extends Auth {
_status: ApiStatus;
}

View file

@ -1,21 +0,0 @@
import { User } from '../users';
import { Auth } from './types';
import { transformAppRoles } from '../users/transformations';
const transformUser = (response: any): User => {
return {
id: response.id ?? '',
app_roles: response.app_roles ? response.app_roles.map(transformAppRoles) : [],
email: response.email ?? '',
name: response.name ?? '',
preferredUsername: response.preferredUsername ?? '',
status: response.state ?? '',
};
};
export const transformAuthUser = (response: any): Auth => {
return {
token: response.accessToken,
userInfo: response.userInfo ? transformUser(response.userInfo) : null,
};
};

View file

@ -1,6 +0,0 @@
import { User } from '../users';
export interface Auth {
token: string | null;
userInfo: User | null;
}

View file

@ -1,11 +0,0 @@
import { performApiCall } from 'src/services/api';
import { User } from './types';
import { transformUser } from './transformations';
export const fetchMemberDetails = async (memberId: number): Promise<User> => {
const res = await performApiCall({
path: `/members/${memberId}`,
});
return transformUser(res.data);
};

View file

@ -1 +0,0 @@
export { useUsers } from './use-users';

View file

@ -1,74 +0,0 @@
import { useDispatch, useSelector } from 'react-redux';
import {
getUsers,
fetchUsers,
fetchUserById,
fetchPersonalInfo,
updateUserById,
updatePersonalInfo,
createUser,
deleteUser,
clearCurrentUser,
createBatchUsers,
} from '../redux';
import { getUserById, getUserModalLoading, getUserslLoading } from '../redux/selectors';
export function useUsers() {
const dispatch = useDispatch();
const users = useSelector(getUsers);
const user = useSelector(getUserById);
const userModalLoading = useSelector(getUserModalLoading);
const userTableLoading = useSelector(getUserslLoading);
function loadUsers() {
return dispatch(fetchUsers());
}
function loadUser(id: string) {
return dispatch(fetchUserById(id));
}
function loadPersonalInfo() {
return dispatch(fetchPersonalInfo());
}
function clearSelectedUser() {
return dispatch(clearCurrentUser());
}
function editUserById(data: any) {
return dispatch(updateUserById(data));
}
function editPersonalInfo(data: any) {
return dispatch(updatePersonalInfo(data));
}
function createNewUser(data: any) {
return dispatch(createUser(data));
}
function createUsers(data: any) {
return dispatch(createBatchUsers(data));
}
function deleteUserById(id: string) {
return dispatch(deleteUser(id));
}
return {
users,
user,
loadUser,
loadUsers,
loadPersonalInfo,
editUserById,
editPersonalInfo,
userModalLoading,
userTableLoading,
createNewUser,
deleteUserById,
clearSelectedUser,
createUsers,
};
}

View file

@ -1,7 +0,0 @@
export * from './types';
export { reducer } from './redux';
export { useUsers } from './hooks';
export { fetchMemberDetails } from './api';

View file

@ -1,255 +0,0 @@
import _ from 'lodash';
import { Dispatch } from 'redux';
import { showToast, ToastType } from 'src/common/util/show-toast';
import { State } from 'src/redux/types';
import { performApiCall } from 'src/services/api';
import { AuthActionTypes } from 'src/services/auth';
import {
transformBatchResponse,
transformRequestMultipleUsers,
transformRequestUser,
transformUser,
} from '../transformations';
export enum UserActionTypes {
FETCH_USERS = 'users/fetch_users',
FETCH_USER = 'users/fetch_user',
UPDATE_USER = 'users/update_user',
CREATE_USER = 'users/create_user',
DELETE_USER = 'users/delete_user',
SET_USER_MODAL_LOADING = 'users/user_modal_loading',
SET_USERS_LOADING = 'users/users_loading',
CREATE_BATCH_USERS = 'users/create_batch_users',
}
export const setUsersLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
dispatch({
type: UserActionTypes.SET_USERS_LOADING,
payload: isLoading,
});
};
export const setUserModalLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
dispatch({
type: UserActionTypes.SET_USER_MODAL_LOADING,
payload: isLoading,
});
};
export const fetchUsers = () => async (dispatch: Dispatch<any>) => {
dispatch(setUsersLoading(true));
try {
const { data } = await performApiCall({
path: '/users',
method: 'GET',
});
dispatch({
type: UserActionTypes.FETCH_USERS,
payload: data.map(transformUser),
});
} catch (err) {
console.error(err);
}
dispatch(setUsersLoading(false));
};
export const fetchUserById = (id: string) => async (dispatch: Dispatch<any>) => {
dispatch(setUserModalLoading(true));
try {
const { data } = await performApiCall({
path: `/users/${id}`,
method: 'GET',
});
dispatch({
type: UserActionTypes.FETCH_USER,
payload: transformUser(data),
});
} catch (err) {
console.error(err);
}
dispatch(setUserModalLoading(false));
};
export const fetchPersonalInfo = () => async (dispatch: Dispatch<any>) => {
dispatch(setUserModalLoading(true));
try {
const { data } = await performApiCall({
path: '/me',
method: 'GET',
});
dispatch({
type: UserActionTypes.FETCH_USER,
payload: transformUser(data),
});
} catch (err) {
console.error(err);
}
dispatch(setUserModalLoading(false));
};
export const updateUserById = (user: any) => async (dispatch: Dispatch<any>, getState: any) => {
dispatch(setUserModalLoading(true));
const state: State = getState();
try {
const { data } = await performApiCall({
path: `/users/${user.id}`,
method: 'PUT',
body: transformRequestUser(user),
});
dispatch({
type: UserActionTypes.UPDATE_USER,
payload: transformUser(data),
});
if (state.auth.userInfo?.id === user.id) {
dispatch({
type: AuthActionTypes.UPDATE_AUTH_USER,
payload: transformUser(data),
});
}
showToast('User updated successfully.', ToastType.Success);
dispatch(fetchUsers());
} catch (err) {
console.error(err);
}
dispatch(setUserModalLoading(false));
};
export const updatePersonalInfo = (user: any) => async (dispatch: Dispatch<any>) => {
dispatch(setUserModalLoading(true));
try {
const { data } = await performApiCall({
path: '/me',
method: 'PUT',
body: transformRequestUser(user),
});
dispatch({
type: UserActionTypes.UPDATE_USER,
payload: transformUser(data),
});
dispatch({
type: AuthActionTypes.UPDATE_AUTH_USER,
payload: transformUser(data),
});
showToast('Personal information updated successfully.', ToastType.Success);
} catch (err) {
console.error(err);
}
dispatch(setUserModalLoading(false));
};
export const createUser = (user: any) => async (dispatch: Dispatch<any>) => {
dispatch(setUserModalLoading(true));
try {
const { data } = await performApiCall({
path: '/users',
method: 'POST',
body: transformRequestUser(user),
});
dispatch({
type: UserActionTypes.CREATE_USER,
payload: transformUser(data),
});
showToast('User created successfully.', ToastType.Success);
dispatch(fetchUsers());
} catch (err: any) {
dispatch(setUserModalLoading(false));
showToast(`${err}`, ToastType.Error);
throw err;
}
dispatch(setUserModalLoading(false));
};
export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => {
dispatch(setUserModalLoading(true));
try {
await performApiCall({
path: `/users/${id}`,
method: 'DELETE',
});
dispatch({
type: UserActionTypes.DELETE_USER,
payload: {},
});
showToast('User deleted successfully.', ToastType.Success);
dispatch(fetchUsers());
} catch (err) {
console.error(err);
}
dispatch(setUserModalLoading(false));
};
export const createBatchUsers = (users: any) => async (dispatch: Dispatch<any>) => {
dispatch(setUserModalLoading(true));
try {
const { data } = await performApiCall({
path: '/users-batch',
method: 'POST',
body: transformRequestMultipleUsers(users),
});
const responseData = transformBatchResponse(data);
dispatch({
type: UserActionTypes.CREATE_BATCH_USERS,
payload: responseData,
});
// show information about created users
if (!_.isEmpty(responseData.success)) {
showToast(responseData.success.message, ToastType.Success, Infinity);
}
if (!_.isEmpty(responseData.existing)) {
showToast(responseData.existing.message, ToastType.Error, Infinity);
}
if (!_.isEmpty(responseData.failed)) {
showToast(responseData.failed.message, ToastType.Error, Infinity);
}
dispatch(fetchUsers());
} catch (err: any) {
dispatch(setUserModalLoading(false));
showToast(`${err}`, ToastType.Error);
throw err;
}
dispatch(setUserModalLoading(false));
};
export const clearCurrentUser = () => (dispatch: Dispatch<any>) => {
dispatch({
type: UserActionTypes.DELETE_USER,
payload: {},
});
};

View file

@ -1,4 +0,0 @@
export * from './actions';
export { default as reducer } from './reducers';
export { getUsers } from './selectors';
export * from './types';

View file

@ -1,46 +0,0 @@
import { UserActionTypes } from './actions';
const initialUsersState: any = {
users: [],
user: {},
userModalLoading: false,
usersLoading: false,
};
const usersReducer = (state: any = initialUsersState, action: any) => {
switch (action.type) {
case UserActionTypes.FETCH_USERS:
return {
...state,
users: action.payload,
};
case UserActionTypes.SET_USER_MODAL_LOADING:
return {
...state,
userModalLoading: action.payload,
};
case UserActionTypes.SET_USERS_LOADING:
return {
...state,
usersLoading: action.payload,
};
case UserActionTypes.FETCH_USER:
case UserActionTypes.UPDATE_USER:
case UserActionTypes.CREATE_USER:
case UserActionTypes.CREATE_BATCH_USERS:
return {
...state,
isModalVisible: false,
user: action.payload,
};
case UserActionTypes.DELETE_USER:
return {
...state,
user: {},
};
default:
return state;
}
};
export default usersReducer;

View file

@ -1,6 +0,0 @@
import { State } from 'src/redux';
export const getUsers = (state: State) => state.users.users;
export const getUserById = (state: State) => state.users.user;
export const getUserModalLoading = (state: State) => state.users.userModalLoading;
export const getUserslLoading = (state: State) => state.users.usersLoading;

View file

@ -1,26 +0,0 @@
import { ApiStatus } from 'src/services/api/redux';
import { User } from '../types';
export interface CurrentUserState extends User {
_status: ApiStatus;
}
export interface UsersState {
currentUser: CurrentUserState;
users: User[];
user: User;
userModalLoading: boolean;
usersLoading: boolean;
}
export interface CurrentUserUpdateAPI {
id: number;
phoneNumber?: string;
email?: string;
language?: string;
country?: string;
firstName?: string;
lastName?: string;
password?: string;
}

View file

@ -1,93 +0,0 @@
import _ from 'lodash';
import { AppRoles, MultipleUsersData, User, UserRole } from './types';
const transformRoleById = (roleId: any): UserRole => {
switch (roleId) {
case 1:
return UserRole.Admin;
case 2:
return UserRole.User;
case 3:
return UserRole.NoAccess;
default:
return UserRole.NoAccess;
}
};
const transformRoleIdByRole = (role: UserRole | null): number | null => {
switch (role) {
case UserRole.Admin:
return 1;
case UserRole.User:
return 2;
case UserRole.NoAccess:
return 3;
default:
return null;
}
};
export const transformAppRoles = (data: any): AppRoles => {
const userRole = transformRoleById(data.role_id);
return {
name: data.name ?? '',
role: userRole,
};
};
export const transformRequestAppRoles = (data: AppRoles): any => {
const resolvedRequestRole = transformRoleIdByRole(data.role) ?? null;
return {
name: data.name ?? '',
role_id: resolvedRequestRole,
};
};
export const transformUser = (response: any): User => {
return {
id: response.id ?? '',
app_roles: response.traits.app_roles ? response.traits.app_roles.map(transformAppRoles) : [],
email: response.traits.email ?? '',
name: response.traits.name ?? '',
preferredUsername: response.preferredUsername ?? '',
status: response.state ?? '',
};
};
export const transformRequestUser = (data: Pick<User, 'app_roles' | 'name' | 'email'>) => {
return {
app_roles: data.app_roles.map(transformRequestAppRoles),
email: data.email ?? '',
name: data.name ?? '',
};
};
const extractUsersFromCsv = (csvData: string) => {
const csvRows = csvData.split('\n');
return _.map(csvRows, (row) => {
const values = row.split(',');
const email = values[0].trim();
const name = !_.isNil(values[1]) ? values[1].trim() : '';
return { email, name, app_roles: [] };
});
};
export const transformRequestMultipleUsers = (data: MultipleUsersData) => {
const batchUsers = extractUsersFromCsv(data.csvUserData);
return {
users: _.map(batchUsers, (user) =>
transformRequestUser({ app_roles: data.appRoles, name: user.name, email: user.email } as User),
),
};
};
export const transformBatchResponse = (response: any): any => {
return {
success: response.success,
existing: response.existing,
failed: response.failed,
};
};

View file

@ -1,36 +0,0 @@
export interface User {
id: string;
app_roles: AppRoles[];
email: string;
name: string;
preferredUsername: string;
status: string;
}
export interface FormUser extends User {
password?: string;
confirmPassword?: string;
}
export enum UserRole {
NoAccess = 'no_access',
Admin = 'admin',
User = 'user',
}
export interface AppRoles {
name: string | null;
role: UserRole | null;
}
export interface UserApiRequest {
id: number | null;
email: string;
name: string;
status: string;
}
export interface MultipleUsersData {
csvUserData: string;
appRoles: AppRoles[];
}