remove stuff
This commit is contained in:
parent
24724c1f90
commit
b43552c5eb
53 changed files with 26 additions and 2316 deletions
|
@ -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
|
|
@ -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."
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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: '',
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export { UserModal } from './UserModal';
|
|
|
@ -1,6 +0,0 @@
|
||||||
export type UserModalProps = {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
userId: string | null;
|
|
||||||
setUserId: any;
|
|
||||||
};
|
|
|
@ -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';
|
||||||
|
|
|
@ -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'),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { Login } from './Login';
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export { MultipleUsersModal } from './MultipleUsersModal';
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { initialAppRoles } from 'src/components/UserModal/consts';
|
|
||||||
|
|
||||||
export type MultipleUsersModalProps = {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initialMultipleUsersForm = {
|
|
||||||
appRoles: initialAppRoles,
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export { MultipleUsersModal } from './MultipleUsersModal';
|
|
|
@ -1 +0,0 @@
|
||||||
export { Users } from './Users';
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { configureStore } from './store';
|
|
||||||
export * from './types';
|
|
|
@ -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 };
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
export const api = {
|
|
||||||
hostname: process.env.REACT_APP_API_URL,
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
export * from './redux';
|
|
||||||
|
|
||||||
export { api } from './config';
|
|
||||||
|
|
||||||
export { createApiCall, performApiCall } from './apiCall';
|
|
|
@ -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];
|
|
||||||
}
|
|
|
@ -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';
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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[];
|
|
||||||
}
|
|
|
@ -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',
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export { useAuth } from './use-auth';
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export { useAuth } from './hooks';
|
|
||||||
export { getAuth, reducer, signIn, signOut, AuthActionTypes, getIsAuthLoading } from './redux';
|
|
||||||
export * from './types';
|
|
|
@ -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());
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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';
|
|
|
@ -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);
|
|
|
@ -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;
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { ApiStatus } from 'src/services/api/redux/types';
|
|
||||||
|
|
||||||
import { Auth } from '../types';
|
|
||||||
|
|
||||||
export interface AuthState extends Auth {
|
|
||||||
_status: ApiStatus;
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { User } from '../users';
|
|
||||||
|
|
||||||
export interface Auth {
|
|
||||||
token: string | null;
|
|
||||||
userInfo: User | null;
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export { useUsers } from './use-users';
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export * from './types';
|
|
||||||
|
|
||||||
export { reducer } from './redux';
|
|
||||||
|
|
||||||
export { useUsers } from './hooks';
|
|
||||||
|
|
||||||
export { fetchMemberDetails } from './api';
|
|
|
@ -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: {},
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './actions';
|
|
||||||
export { default as reducer } from './reducers';
|
|
||||||
export { getUsers } from './selectors';
|
|
||||||
export * from './types';
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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[];
|
|
||||||
}
|
|
Loading…
Reference in a new issue