From 2fdaa6d61d963522e0c07676f3d10d1da3a8cb53 Mon Sep 17 00:00:00 2001 From: JAN VOJACEK <vojacekjan@hotmail.com> Date: Mon, 30 May 2022 12:19:25 +0200 Subject: [PATCH] Display errors from manage pages properly through error reducer --- client/src/components/Error.tsx | 58 +-- .../src/routes/AdminRoutes/ManageGateways.tsx | 374 +++++++++++------- client/src/routes/AdminRoutes/ManageUsers.tsx | 311 +++++++++------ client/src/store/reducers/errorReducer.ts | 22 +- 4 files changed, 466 insertions(+), 299 deletions(-) diff --git a/client/src/components/Error.tsx b/client/src/components/Error.tsx index bb50ef9..558bb2b 100644 --- a/client/src/components/Error.tsx +++ b/client/src/components/Error.tsx @@ -1,29 +1,39 @@ import { useDispatch } from "react-redux"; -import { cleanError } from "../store/reducers/errorReducer" -const Error = ({ errorMessage }: { errorMessage: [] }) => { - const dispatch = useDispatch(); - console.log(errorMessage) - return ( - <div style={{ - display: "flex", - justifyContent: "center", - alignItems: "center", - position: "fixed", - zIndex: 99999, - width: "100%", - height: "100%", - background: "#e5e5e5d1" - }} - onClick={() => { - dispatch(cleanError()); - }} - > - { - // errorMessage.map((err, index) => <p key={index} style={{ color: "red", padding: 40, background: "white", borderRadius: 12 }}>{err}</p>) - } +import { cleanError, ErrorStateAlert } from "../store/reducers/errorReducer"; - </div> - ); +const Error = ({ errorMessage }: { errorMessage: Array<ErrorStateAlert> }) => { + const dispatch = useDispatch(); + return ( + <div + style={{ + display: "flex", + justifyContent: "center", + alignItems: "center", + position: "fixed", + zIndex: 99999, + width: "100%", + height: "100%", + background: "#e5e5e5d1", + }} + onClick={() => { + dispatch(cleanError()); + }} + > + {errorMessage.map((err, index) => ( + <p + key={index} + style={{ + color: "red", + padding: 40, + background: "white", + borderRadius: 12, + }} + > + {err.message} + </p> + ))} + </div> + ); }; export default Error; diff --git a/client/src/routes/AdminRoutes/ManageGateways.tsx b/client/src/routes/AdminRoutes/ManageGateways.tsx index 7db3d56..58bdc5f 100644 --- a/client/src/routes/AdminRoutes/ManageGateways.tsx +++ b/client/src/routes/AdminRoutes/ManageGateways.tsx @@ -1,167 +1,237 @@ -import React, { useState, useEffect } from 'react' -import AdminContainer from "../../admin-components/AdminContainer" -import { GLOBAL_URL } from '../../GLOBAL_URL' +import React, { useState, useEffect } from "react"; +import AdminContainer from "../../admin-components/AdminContainer"; +import { GLOBAL_URL } from "../../GLOBAL_URL"; +import { useDispatch } from "react-redux"; +import { setError } from "../../store/reducers/errorReducer"; + interface GateWayCreateInterface { - name: string; - password?: string; - confirmedPassword?: string; - description: string; - creator?: string - createdAt?: any, - _id?: string + name: string; + password?: string; + confirmedPassword?: string; + description: string; + creator?: string; + createdAt?: any; + _id?: string; } const AdminCreateGateway = () => { - const [listOfGateways, setListOfGateways] = useState<GateWayCreateInterface[]>([]) - const [fetchChange, setFetchChange] = useState<boolean>(false) - const [gatewayData, setGatewayData] = useState<GateWayCreateInterface>({ + const [listOfGateways, setListOfGateways] = useState< + GateWayCreateInterface[] + >([]); + const dispatch = useDispatch(); + const [fetchChange, setFetchChange] = useState<boolean>(false); + const [gatewayData, setGatewayData] = useState<GateWayCreateInterface>({ + name: "", + password: "", + confirmedPassword: "", + description: "", + }); + + const formData = (e: React.ChangeEvent<HTMLInputElement>) => { + setGatewayData({ ...gatewayData, [e.target.name]: e.target.value }); + }; + const createGateWay = async (e: any) => { + e.preventDefault(); + const { + name, + password, + confirmedPassword, + description, + }: GateWayCreateInterface = gatewayData; + try { + if (!name) throw new Error("Unique name is mising"); + if (!description) throw new Error("Description is mandatory"); + if (!password) throw new Error("Password is mandatory"); + if (!description) throw new Error("Description is mandatory"); + if (password !== confirmedPassword) + throw new Error("Passwords must match!"); + + //Fetch call na backend + const token: string | null = localStorage.getItem("token"); + const response: Response = await fetch(`${GLOBAL_URL}/gateway/create/`, { + method: "POST", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${token}_dr_dick`, + }, + body: JSON.stringify({ + name, + description, + password, + }), + }); + const data: any = await response.json(); + if (data.statusCode >= 400) throw new Error(data.message); + setFetchChange(!fetchChange); + setGatewayData({ name: "", password: "", confirmedPassword: "", - description: "" - }) - const [showError, setShowError] = useState<boolean>(false) - const [errorMessage, setErrorMessage] = useState<string>("") - - const formData = (e: React.ChangeEvent<HTMLInputElement>) => { - setGatewayData({ ...gatewayData, [e.target.name]: e.target.value }) + description: "", + }); + } catch (error: any) { + if (error) { + dispatch(setError(error.message)); + } } - const createGateWay = async (e: any) => { - e.preventDefault(); - const { name, password, confirmedPassword, description }: GateWayCreateInterface = gatewayData; - try { - if (!name) throw new Error("Unique name is mising"); - if (!description) throw new Error("Description is mandatory"); - if (!password) throw new Error("Password is mandatory"); - if (!description) throw new Error("Description is mandatory"); - if (password !== confirmedPassword) throw new Error("Passwords must match!") + }; - //Fetch call na backend - const token: string | null = localStorage.getItem("token"); - const response: Response = await fetch(`${GLOBAL_URL}/gateway/create/`, { - method: "POST", - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - name, - description, - password - }) - }) - const data: any = await response.json(); - console.log(data) - if (data.status) throw new Error(data.message); - setFetchChange(!fetchChange) - setGatewayData({ - name: "", - password: "", - confirmedPassword: "", - description: "" - }) - } catch (error: any) { - if (error) { - setShowError(true); - setErrorMessage(error.message) - setTimeout(() => { - setShowError(false); - setErrorMessage(""); - }, 2000) - } - } - } + const getAllGateways = async () => { + const token: string | null = localStorage.getItem("token"); + const response: Response = await fetch(`${GLOBAL_URL}/gateway/all/`, { + method: "GET", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + const data: GateWayCreateInterface[] = await response.json(); + setListOfGateways(data); + }; - const getAllGateways = async () => { - - const token: string | null = localStorage.getItem("token"); - const response: Response = await fetch(`${GLOBAL_URL}/gateway/all/`, { - method: "GET", + const deleteGateWay = async (index: number) => { + const conf = window.confirm( + `Do you really want to delete ${listOfGateways[index].name}` + ); + if (conf) { + const token: string | null = localStorage.getItem("token"); + try { + const response: Response = await fetch( + GLOBAL_URL + `/gateway/delete/${listOfGateways[index]._id}`, + { + method: "DELETE", headers: { - "Content-type": "application/json", - Authorization: `Bearer ${token}`, - } - }) - const data: GateWayCreateInterface[] = await response.json(); - setListOfGateways(data); - } - - const deleteGateWay = async (index: number) => { - const conf = window.confirm(`Do you really want to delete ${listOfGateways[index].name}`); - if (conf) { - const token: string | null = localStorage.getItem("token"); - try { - const response: Response = await fetch(GLOBAL_URL + `/gateway/delete/${listOfGateways[index]._id}`, { - method: "DELETE", - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${token}`, - }, - }) - const data: any = await response.json(); - if (data.error) throw new Error(data.message); - setFetchChange(!fetchChange); - } catch (error: any) { - console.log(error); - // dispatch(setError([error])) - } - } + "Content-type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + const data: any = await response.json(); + if (data.error) throw new Error(data.message); + setFetchChange(!fetchChange); + } catch (error: any) { + dispatch(setError(error.message)); + } } - useEffect(() => { - getAllGateways() - }, [fetchChange]); + }; + useEffect(() => { + getAllGateways(); + }, [fetchChange]); - return ( - <AdminContainer> - <h1>Create new GateWay</h1> - <form autoComplete="false" onSubmit={createGateWay} > - <div className="form-group row"> - <label htmlFor="uniqueName" className="col-sm-2 col-form-label">Unique Name</label> - <div className="col-sm-10"> - <input onChange={formData} value={gatewayData.name} type="text" name="name" placeholder="Some really cool gateway" /> - </div> - </div> - <div className="form-group row"> - <label htmlFor="inputPassword3" className="col-sm-2 col-form-label">Password</label> - <div className="col-sm-10"> - <input onChange={formData} value={gatewayData.password} type="password" className="form-control" name="password" placeholder="Password" /> - </div> - </div> - <div className="form-group row"> - <label htmlFor="inputPassword3" className="col-sm-2 col-form-label">Confirm password</label> - <div className="col-sm-10"> - <input onChange={formData} value={gatewayData.confirmedPassword} type="password" className="form-control" name="confirmedPassword" placeholder="Password" /> - </div> - </div> - <div className="form-group row"> - <label htmlFor="inputPassword3" className="col-sm-2 col-form-label">Detailed purpose of Gateway / Description</label> - <div className="col-sm-10"> - <input onChange={formData} value={gatewayData.description} className="form-control" name="description" placeholder={"Please, be super descriptive what is the purpose of the new gateway"}></input> - </div> - </div> + return ( + <AdminContainer> + <h1>Create new GateWay</h1> + <form autoComplete="false" onSubmit={createGateWay}> + <div className="form-group row"> + <label htmlFor="uniqueName" className="col-sm-2 col-form-label"> + Unique Name + </label> + <div className="col-sm-10"> + <input + onChange={formData} + value={gatewayData.name} + type="text" + name="name" + placeholder="Some really cool gateway" + /> + </div> + </div> + <div className="form-group row"> + <label htmlFor="inputPassword3" className="col-sm-2 col-form-label"> + Password + </label> + <div className="col-sm-10"> + <input + onChange={formData} + value={gatewayData.password} + type="password" + className="form-control" + name="password" + placeholder="Password" + /> + </div> + </div> + <div className="form-group row"> + <label htmlFor="inputPassword3" className="col-sm-2 col-form-label"> + Confirm password + </label> + <div className="col-sm-10"> + <input + onChange={formData} + value={gatewayData.confirmedPassword} + type="password" + className="form-control" + name="confirmedPassword" + placeholder="Password" + /> + </div> + </div> + <div className="form-group row"> + <label htmlFor="inputPassword3" className="col-sm-2 col-form-label"> + Detailed purpose of Gateway / Description + </label> + <div className="col-sm-10"> + <input + onChange={formData} + value={gatewayData.description} + className="form-control" + name="description" + placeholder={ + "Please, be super descriptive what is the purpose of the new gateway" + } + ></input> + </div> + </div> - <div className="form-group row"> - <div className="col-sm-10"> - <button type="submit" className="btn btn-primary">Create new GateWay {gatewayData.name}</button> - </div> - {showError && <p className='btn-danger'>{errorMessage}</p>} - </div> - </form> - <hr /> - <div className="listOfUsers" style={{ display: "flex", flexDirection: "row", flexWrap: "wrap" }}> - {listOfGateways.map((gateway: GateWayCreateInterface, index) => ( - <div key={index} className="card" style={{ width: "23rem", margin: 10 }}> - <div className="card-body"> - <h5 className="card-header">{gateway.name}</h5> - <p className="card-text"> <strong>Description: </strong> {gateway.description}</p> - <p className="card-text"><strong>Created by:</strong>{gateway.creator} <span style={{ fontSize: 10 }}>{new Date(gateway.createdAt).toDateString()}</span></p> - <p className="card-text"><strong>Status: </strong> <span style={{ color: "green" }}>Online</span></p> - <p className="btn btn-danger" onClick={() => { deleteGateWay(index) }}>Delete</p> - </div> - </div> - ))} + <div className="form-group row"> + <div className="col-sm-10"> + <button type="submit" className="btn btn-primary"> + Create new GateWay {gatewayData.name} + </button> + </div> + </div> + </form> + <hr /> + <div + className="listOfUsers" + style={{ display: "flex", flexDirection: "row", flexWrap: "wrap" }} + > + {listOfGateways.map((gateway: GateWayCreateInterface, index) => ( + <div + key={index} + className="card" + style={{ width: "23rem", margin: 10 }} + > + <div className="card-body"> + <h5 className="card-header">{gateway.name}</h5> + <p className="card-text"> + {" "} + <strong>Description: </strong> {gateway.description} + </p> + <p className="card-text"> + <strong>Created by:</strong> + {gateway.creator}{" "} + <span style={{ fontSize: 10 }}> + {new Date(gateway.createdAt).toDateString()} + </span> + </p> + <p className="card-text"> + <strong>Status: </strong>{" "} + <span style={{ color: "green" }}>Online</span> + </p> + <p + className="btn btn-danger" + onClick={() => { + deleteGateWay(index); + }} + > + Delete + </p> </div> - </AdminContainer> - ) -} + </div> + ))} + </div> + </AdminContainer> + ); +}; -export default AdminCreateGateway \ No newline at end of file +export default AdminCreateGateway; diff --git a/client/src/routes/AdminRoutes/ManageUsers.tsx b/client/src/routes/AdminRoutes/ManageUsers.tsx index 417291d..705072d 100644 --- a/client/src/routes/AdminRoutes/ManageUsers.tsx +++ b/client/src/routes/AdminRoutes/ManageUsers.tsx @@ -7,136 +7,205 @@ import { authUserFailed } from "../../store/reducers/auth"; import { setError } from "../../store/reducers/errorReducer"; import { GLOBAL_URL } from "../../GLOBAL_URL"; const AdminPanel = () => { - const dispatch = useDispatch(); - let user: UserInterface = useSelector((data: any) => { - return data.auth.user; - }) - const [listenForChange, setListenForChange] = useState<boolean>(false) - const [users, setUsers] = useState<UserInterface[]>([]) + const dispatch = useDispatch(); + let user: UserInterface = useSelector((data: any) => { + return data.auth.user; + }); + const [listenForChange, setListenForChange] = useState<boolean>(false); + const [users, setUsers] = useState<UserInterface[]>([]); - useEffect(() => { - getAllUsers(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [listenForChange]); - //Fetch all users - Admin required - const getAllUsers = async () => { - try { - const token: string | null = localStorage.getItem("token"); - const response: Response = await fetch(GLOBAL_URL + "/users/all", { - method: "get", - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${token}`, - }, - }) - const data: any = await response.json(); - if (data.statusCode === 401) { - dispatch(authUserFailed()); - } - setUsers(data); - - } catch (error: any) { - if (error) { - dispatch(setError(error.message)); - } - } + useEffect(() => { + getAllUsers(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [listenForChange]); + //Fetch all users - Admin required + const getAllUsers = async () => { + try { + const token: string | null = localStorage.getItem("token"); + const response: Response = await fetch(GLOBAL_URL + "/users/all", { + method: "get", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + const data: any = await response.json(); + if (data.statusCode === 401) { + dispatch(authUserFailed()); + } + setUsers(data); + } catch (error: any) { + if (error) { + dispatch(setError(error.message)); + } } + }; - const updateUser = async (index: number, action: boolean) => { - const token: string | null = localStorage.getItem("token"); - try { - const response: Response = await fetch(GLOBAL_URL + `/users/update/${users[index]._id}`, { - method: "PATCH", - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - isUserApproved: action - }) - }) - const data: any = await response.json(); - if (!data) throw new Error("User could not be updated"); - setListenForChange(!listenForChange); - } catch (error: any) { - dispatch(setError([error])) + const updateUser = async (index: number, action: boolean) => { + const token: string | null = localStorage.getItem("token"); + try { + const response: Response = await fetch( + GLOBAL_URL + `/users/update/${users[index]._id}`, + { + method: "PATCH", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + isUserApproved: action, + }), } + ); + const data: any = await response.json(); + if (!data) throw new Error("User could not be updated"); + setListenForChange(!listenForChange); + } catch (error: any) { + dispatch(setError(error.message)); } - const deleteUser = async (index: number) => { - const conf = window.confirm(`Do you really want to delete ${users[index].email}`); - if (conf) { - const token: string | null = localStorage.getItem("token"); - try { - const response: Response = await fetch(GLOBAL_URL + `/users/delete/${users[index]._id}`, { - method: "DELETE", - headers: { - "Content-type": "application/json", - Authorization: `Bearer ${token}`, - }, - }) - const data: any = await response.json(); - if (data.error) throw new Error(data.message); - setListenForChange(!listenForChange); - } catch (error: any) { - dispatch(setError([error])) - } - } + }; + const deleteUser = async (index: number) => { + const conf = window.confirm( + `Do you really want to delete ${users[index].email}` + ); + if (conf) { + const token: string | null = localStorage.getItem("token"); + try { + const response: Response = await fetch( + GLOBAL_URL + `/users/delete/${users[index]._id}`, + { + method: "DELETE", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + const data: any = await response.json(); + if (data.error) throw new Error(data.message); + setListenForChange(!listenForChange); + } catch (error: any) { + dispatch(setError(error.message)); + } } + }; - - return ( - <AdminContainer> - <div className="listOfUsers" style={{ display: "flex", flexDirection: "row", flexWrap: "wrap" }}> - { - users.map((userRegular: UserInterface, index) => ( - <div key={index} className="card text-center" style={{ width: "23rem", margin: 10 }}> - <div className="card-body"> - <h5 className="card-header">{userRegular.name} {userRegular.surname}</h5> - <ul className="list-group list-group-flush"> - <li className="list-group-item">@ {userRegular.email}</li> - <li className="list-group-item" style={{ fontSize: 11 }}>{new Date(userRegular.lastLoggedIn).toLocaleString()}</li> - <li className="list-group-item">Approved: <span style={userRegular.isUserApproved ? { color: "green" } : { color: "red" }}>{userRegular.isUserApproved ? "Approved" : "Not approved"}</span></li> - <li className="list-group-item">Auth Level: <span style={userRegular.authLevel === "iotadmin" ? { color: "#ff8227", fontWeight: "bold" } : { color: "green" }}>{userRegular.authLevel === "iotadmin" ? "Administrator" : "Regular user"}</span></li> - </ul> - <br /> - <div className="row"> - {user.email !== userRegular.email && - <> - {!userRegular.isUserApproved && - <div className="col-sm-4"> - <div className="btn btn-primary btn-success" onClick={() => { updateUser(index, true) }}>Approve</div> - </div>} - {userRegular.isUserApproved && <div className="col-sm-4"> - <div className="btn btn-primary btn-warning" onClick={() => { updateUser(index, false) }}>Disapprove</div> - </div>} - - </> - } - { - user.email === userRegular.email && - <div> - - <ul className="list-group list-group-flush"> - <li className="list-group-item" onClick={() => { - dispatch(authUserFailed()) - }}> - <span style={{ color: "red" }} className="nav-link btn btn-sm btn-outline-secondary">Log-out</span> - </li> - </ul> - - </div> - } - {user.email !== userRegular.email && <div className="col-sm-4"> - <div className="btn btn-primary btn-danger" onClick={() => { deleteUser(index) }}>Delete</div> - </div>} - - </div> - </div> + return ( + <AdminContainer> + <div + className="listOfUsers" + style={{ display: "flex", flexDirection: "row", flexWrap: "wrap" }} + > + {users.map((userRegular: UserInterface, index) => ( + <div + key={index} + className="card text-center" + style={{ width: "23rem", margin: 10 }} + > + <div className="card-body"> + <h5 className="card-header"> + {userRegular.name} {userRegular.surname} + </h5> + <ul className="list-group list-group-flush"> + <li className="list-group-item">@ {userRegular.email}</li> + <li className="list-group-item" style={{ fontSize: 11 }}> + {new Date(userRegular.lastLoggedIn).toLocaleString()} + </li> + <li className="list-group-item"> + Approved:{" "} + <span + style={ + userRegular.isUserApproved + ? { color: "green" } + : { color: "red" } + } + > + {userRegular.isUserApproved ? "Approved" : "Not approved"} + </span> + </li> + <li className="list-group-item"> + Auth Level:{" "} + <span + style={ + userRegular.authLevel === "iotadmin" + ? { color: "#ff8227", fontWeight: "bold" } + : { color: "green" } + } + > + {userRegular.authLevel === "iotadmin" + ? "Administrator" + : "Regular user"} + </span> + </li> + </ul> + <br /> + <div className="row"> + {user.email !== userRegular.email && ( + <> + {!userRegular.isUserApproved && ( + <div className="col-sm-4"> + <div + className="btn btn-primary btn-success" + onClick={() => { + updateUser(index, true); + }} + > + Approve + </div> + </div> + )} + {userRegular.isUserApproved && ( + <div className="col-sm-4"> + <div + className="btn btn-primary btn-warning" + onClick={() => { + updateUser(index, false); + }} + > + Disapprove </div> - )) - } + </div> + )} + </> + )} + {user.email === userRegular.email && ( + <div> + <ul className="list-group list-group-flush"> + <li + className="list-group-item" + onClick={() => { + dispatch(authUserFailed()); + }} + > + <span + style={{ color: "red" }} + className="nav-link btn btn-sm btn-outline-secondary" + > + Log-out + </span> + </li> + </ul> + </div> + )} + {user.email !== userRegular.email && ( + <div className="col-sm-4"> + <div + className="btn btn-primary btn-danger" + onClick={() => { + deleteUser(index); + }} + > + Delete + </div> + </div> + )} + </div> </div> - </AdminContainer>); + </div> + ))} + </div> + </AdminContainer> + ); }; export default AdminPanel; diff --git a/client/src/store/reducers/errorReducer.ts b/client/src/store/reducers/errorReducer.ts index 25d0a8a..72110b6 100644 --- a/client/src/store/reducers/errorReducer.ts +++ b/client/src/store/reducers/errorReducer.ts @@ -1,5 +1,20 @@ import { createSlice } from "@reduxjs/toolkit"; -export const errorSlice = createSlice({ + +export interface ErrorStateAlert { + message: string; +} + +export interface ErrorState { + alerts: Array<ErrorStateAlert>; + show: boolean; +} + +export type ErrorReducers = { + setError: (state: ErrorState, action: { payload: string }) => void; + cleanError: (state: ErrorState) => void; +}; + +export const errorSlice = createSlice<ErrorState, ErrorReducers, string>({ name: "alert", initialState: { alerts: [], @@ -7,7 +22,10 @@ export const errorSlice = createSlice({ }, reducers: { setError: (state, action) => { - state.alerts = action.payload; + console.log(state, action); + state.alerts.push({ + message: action.payload ?? "Unknown error occurred", + }); state.show = true; }, cleanError: (state) => {