Merge branch 'monitors' of https://github.com/MuhammadKhalilzadeh/bluewave-uptime into monitors
This commit is contained in:
commit
4cc59e6728
42 files changed
+1897
-565
No files matched your search
@@ -0,0 +1,3 @@
|
||||
Client/node_modules/
|
||||
Server/node_modules/
|
||||
*.env
|
||||
Generated
+647
-219
File diff suppressed because it is too large.
Load diff
+34
-7
@@ -7,6 +7,7 @@ import HomeLayout from "./Layouts/HomeLayout";
|
||||
import Demo from "./Pages/Demo/Demo";
|
||||
import PlayGround from "./Pages/PlayGround/PlayGround";
|
||||
import Monitors from "./Pages/Monitors";
|
||||
import CreateNewMonitor from "./Pages/CreateNewMonitor";
|
||||
import Incidents from "./Pages/Incidents";
|
||||
import Status from "./Pages/Status";
|
||||
import Integrations from "./Pages/Integrations";
|
||||
@@ -16,22 +17,48 @@ import CheckEmail from "./Pages/CheckEmail";
|
||||
import SetNewPassword from "./Pages/SetNewPassword";
|
||||
import NewPasswordConfirmed from "./Pages/NewPasswordConfirmed";
|
||||
import ToastComponent from "./Components/Toast";
|
||||
import ProtectedRoute from "./Components/ProtectedRoute";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
<Route exact path="/" element={<HomeLayout />}>
|
||||
<Route path="monitors" element={<Monitors />} />
|
||||
<Route path="incidents" element={<Incidents />} />
|
||||
<Route path="status" element={<Status />} />
|
||||
<Route path="integrations" element={<Integrations />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
element={<ProtectedRoute Component={Monitors} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors"
|
||||
element={<ProtectedRoute Component={Monitors} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/create"
|
||||
element={<ProtectedRoute Component={CreateNewMonitor} />}
|
||||
/>
|
||||
<Route
|
||||
path="incidents"
|
||||
element={<ProtectedRoute Component={Incidents} />}
|
||||
/>
|
||||
<Route
|
||||
path="status"
|
||||
element={<ProtectedRoute Component={Status} />}
|
||||
/>
|
||||
<Route
|
||||
path="integrations"
|
||||
element={<ProtectedRoute Component={Integrations} />}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={<ProtectedRoute Component={Settings} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route exact path="/register" element={<Register />} />
|
||||
|
||||
<Route exact path="/login" element={<Login />} />
|
||||
<Route exact path="/register" element={<Register />} />
|
||||
<Route exact path="/demo" element={<Demo />} />
|
||||
<Route path="/toast" element={<ToastComponent />} />
|
||||
{/* <Route path="/toast" element={<ToastComponent />} /> */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
<Route path="/playground" element={<PlayGround />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
|
||||
@@ -8,13 +8,14 @@ import PropTypes from "prop-types";
|
||||
* @param {string} props.firstName - The users first name
|
||||
* @param {string} props.lastName - The users last name
|
||||
* @param {boolean} props.small - Specifies if avatar should be large
|
||||
* @param {Object} [props.sx] - Additional styles to apply to the button.
|
||||
* @returns {JSX.Element}
|
||||
* @example
|
||||
* // Render a red label
|
||||
* <Avatar src="assets/img" first="Alex" last="Holliday" small />
|
||||
*/
|
||||
|
||||
const Avatar = ({ src, firstName, lastName, small }) => {
|
||||
const Avatar = ({ src, firstName, lastName, small, sx }) => {
|
||||
const borderColor = "#F0F2F4";
|
||||
const smallStyle = small ? { width: 32, height: 32 } : {};
|
||||
return (
|
||||
@@ -25,6 +26,7 @@ const Avatar = ({ src, firstName, lastName, small }) => {
|
||||
display: "inline-flex",
|
||||
border: `0.2rem solid ${borderColor}`,
|
||||
...smallStyle,
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
{src === undefined ? `${firstName[0]}${lastName[0]}` : null}
|
||||
@@ -37,6 +39,7 @@ Avatar.propTypes = {
|
||||
firstName: PropTypes.string.isRequired,
|
||||
lastName: PropTypes.string.isRequired,
|
||||
small: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
@@ -1,17 +1,57 @@
|
||||
import React from "react";
|
||||
import "./barChart.css";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const RANGE_MAX = 100;
|
||||
const RANGE_MIN = 1;
|
||||
|
||||
/**
|
||||
* Normalizes the response times of a set of checks to a specified range.
|
||||
* This function calculates the minimum and maximum response times from the input,
|
||||
* then scales each check's response time linearly between `RANGE_MIN` and `RANGE_MAX`.
|
||||
*
|
||||
* @param {Array<Checks>} checks - An array of check objects. Each check should have a `responseTime` property.
|
||||
* @returns {Array<Checks>} An array of check objects with normalized `responseTime` values.
|
||||
*/
|
||||
const normalizeData = (checks) => {
|
||||
const min = checks.reduce((accum, cur) => {
|
||||
return Math.min(accum, cur.responseTime);
|
||||
}, Infinity);
|
||||
|
||||
const max = checks.reduce((accum, cur) => {
|
||||
return Math.max(accum, cur.responseTime);
|
||||
}, 0);
|
||||
|
||||
const normalizedChecks = checks.map((check) => {
|
||||
let normailzedResponseTime =
|
||||
RANGE_MIN +
|
||||
((check.responseTime - min) * (RANGE_MAX - RANGE_MIN)) / (max - min);
|
||||
const normalizedCheck = { ...check, responseTime: normailzedResponseTime };
|
||||
return normalizedCheck;
|
||||
});
|
||||
return normalizedChecks;
|
||||
};
|
||||
|
||||
/**
|
||||
* BarChart renders a bar chart visualization for a set of checks. Each bar represents a check's response time,
|
||||
* with the color indicating the check's status (green for successful checks, red for failed ones).
|
||||
*
|
||||
* @component
|
||||
* @param {Array<Checks>} checks - An array of check objects to be visualized. Each check object should have an `_id`,
|
||||
* a `status` indicating success (true) or failure (false), and a `responseTime` representing the height of the bar in percentage.
|
||||
*/
|
||||
const BarChart = ({ checks = [] }) => {
|
||||
const normailzedChecks = normalizeData(checks);
|
||||
return (
|
||||
<div className="bar-chart">
|
||||
{checks.map((value, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`bar ${value.status ? "green" : "red"}`}
|
||||
style={{ height: `${value.responseTime / 3}%` }}
|
||||
/>
|
||||
))}
|
||||
{normailzedChecks.map((check) => {
|
||||
return (
|
||||
<div
|
||||
key={check._id}
|
||||
className={`bar ${check.status ? "green" : "red"}`}
|
||||
style={{ height: `${check.responseTime}%` }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTheme } from "@mui/material";
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {string} props.title - The title text for the server status (required)
|
||||
* @param {string} props.value - The value text to be displayed (required)
|
||||
* @param {number} props.value - The value text to be displayed (required)
|
||||
* @param {string} props.state - The state of the server (e.g., "online", "offline", "warning") (required)
|
||||
* @returns {JSX.Element} - Renders the server status component
|
||||
*/
|
||||
@@ -30,7 +30,7 @@ const ServerStatus = ({ title, value, state }) => {
|
||||
|
||||
ServerStatus.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
state: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import SearchTextField from "../TextFields/Search/SearchTextField";
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
import HostsTable from "../HostsTable";
|
||||
import Pagination from "../Pagination";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* CurrentMonitors displays the current status of monitor
|
||||
*
|
||||
* @component
|
||||
* @param {Array<Monitor>} monitors - An array of monitor objects to be displayed.
|
||||
*/
|
||||
|
||||
const CurrentMonitors = ({ monitors }) => {
|
||||
return (
|
||||
@@ -10,7 +17,7 @@ const CurrentMonitors = ({ monitors }) => {
|
||||
<div className="current-monitors-bar">
|
||||
<div className="current-monitors-title-holder">
|
||||
<div className="current-monitors-title">Current monitors</div>
|
||||
<div className="current-monitors-counter">5</div>
|
||||
<div className="current-monitors-counter">{monitors.length}</div>
|
||||
</div>
|
||||
<div className="current-monitors-search-bar">
|
||||
<SearchTextField />
|
||||
@@ -24,4 +31,8 @@ const CurrentMonitors = ({ monitors }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CurrentMonitors.propTypes = {
|
||||
monitors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default CurrentMonitors;
|
||||
@@ -1,4 +1,32 @@
|
||||
import React from "react";
|
||||
import BarChart from "../Charts/BarChart/BarChart";
|
||||
import PropTypes from "prop-types";
|
||||
import Host from "../Host/";
|
||||
import HostStatus from "../HostStatus";
|
||||
/**
|
||||
* HostsTable displays the current status of monitor
|
||||
*
|
||||
* @component
|
||||
* @param {Array<Monitor>} monitors - An array of monitor objects to be displayed.
|
||||
*/
|
||||
|
||||
const getLastCheckStatus = (monitor) => {
|
||||
if (monitor.checks.length === 0) {
|
||||
// Default values for when there are no checks
|
||||
return {
|
||||
color: "var(--env-var-color-21)",
|
||||
statusText: "No Data",
|
||||
dotColor: "var(--env-var-color-19)",
|
||||
};
|
||||
}
|
||||
|
||||
const lastCheck = monitor.checks[monitor.checks.length - 1];
|
||||
const isUp = lastCheck.status === true;
|
||||
return {
|
||||
color: isUp ? "var(--env-var-color-20)" : "var(--env-var-color-21)",
|
||||
statusText: isUp ? "Up" : "Down",
|
||||
dotColor: isUp ? "var(--env-var-color-17)" : "var(--env-var-color-19)",
|
||||
};
|
||||
};
|
||||
|
||||
const HostsTable = ({ monitors }) => {
|
||||
return (
|
||||
@@ -8,23 +36,45 @@ const HostsTable = ({ monitors }) => {
|
||||
<tr className="theader-row">
|
||||
<td className="theader-row-cell">Host</td>
|
||||
<td className="theader-row-cell">Status</td>
|
||||
<td className="theader-row-cell">Team</td>
|
||||
<td className="theader-row-cell">Response</td>
|
||||
<td className="theader-row-cell">Actions</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{monitors.map((monitor, index) => (
|
||||
<tr className="tbody-row" key={index}>
|
||||
<td className="tbody-row-cell">{monitor.host}</td>
|
||||
<td className="tbody-row-cell">{monitor.status}</td>
|
||||
<td className="tbody-row-cell">{monitor.team}</td>
|
||||
<td className="tbody-row-cell actions-cell">{monitor.actions}</td>
|
||||
</tr>
|
||||
))}
|
||||
{monitors.map((monitor) => {
|
||||
const host = Host(
|
||||
monitor.name,
|
||||
100,
|
||||
"var(--env-var-color-17)",
|
||||
monitor.url
|
||||
);
|
||||
|
||||
const { color, currentStatus, dotColor } =
|
||||
getLastCheckStatus(monitor);
|
||||
|
||||
const status = HostStatus(color, currentStatus, dotColor);
|
||||
|
||||
return (
|
||||
<tr className="tbody-row" key={monitor._id}>
|
||||
<td className="tbody-row-cell">{host}</td>
|
||||
<td className="tbody-row-cell">{status}</td>
|
||||
<td className="tbody-row-cell">
|
||||
<BarChart checks={monitor.checks} />
|
||||
</td>
|
||||
<td className="tbody-row-cell actions-cell">
|
||||
{monitor.actions}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
HostsTable.propTypes = {
|
||||
monitors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default HostsTable;
|
||||
@@ -13,6 +13,11 @@ import Tooltip from "@mui/material/Tooltip";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import ChevronDown from "../../assets/Images/Icon-chevron-down.png";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { clearMonitorState } from "../../Features/Monitors/monitorsSlice";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const settings = ["Profile", "Team", "Invite Colleagues", "Logout"];
|
||||
|
||||
@@ -30,6 +35,9 @@ const settings = ["Profile", "Team", "Invite Colleagues", "Logout"];
|
||||
function NavBar() {
|
||||
const theme = useTheme();
|
||||
const [anchorElUser, setAnchorElUser] = useState(null);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
/**
|
||||
* Handles opening the user menu.
|
||||
@@ -40,11 +48,38 @@ function NavBar() {
|
||||
setAnchorElUser(event.currentTarget);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles logging out the user
|
||||
*
|
||||
*/
|
||||
const logout = () => {
|
||||
// Clear auth state
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearMonitorState());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles closing the user menu.
|
||||
*/
|
||||
const handleCloseUserMenu = () => {
|
||||
const handleCloseUserMenu = (setting) => {
|
||||
setAnchorElUser(null);
|
||||
switch (setting) {
|
||||
case "Profile":
|
||||
console.log("Profile");
|
||||
break;
|
||||
case "Team":
|
||||
console.log("Team");
|
||||
break;
|
||||
case "Invite Colleagues":
|
||||
console.log("Invite Colleagues");
|
||||
break;
|
||||
case "Logout":
|
||||
logout();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -122,7 +157,9 @@ function NavBar() {
|
||||
className="icon-button-avatar"
|
||||
style={{ width: "25px", height: "25px" }}
|
||||
/>
|
||||
<div className="icon-button-toggle-title">Jackie Dawn</div>
|
||||
<div className="icon-button-toggle-title">
|
||||
{authState.user.firstname} {authState.user.lastname}
|
||||
</div>
|
||||
<img
|
||||
className="icon-button-toggle-pic"
|
||||
src={ChevronDown}
|
||||
@@ -151,7 +188,7 @@ function NavBar() {
|
||||
<MenuItem
|
||||
id="menu-item"
|
||||
key={setting}
|
||||
onClick={handleCloseUserMenu}
|
||||
onClick={() => handleCloseUserMenu(setting)}
|
||||
>
|
||||
<Typography
|
||||
fontSize="var(--env-var-font-size-medium)"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ProtectedRoute is a higher-order component that wraps the `Route` component from `react-router-dom`
|
||||
* to create a protected route. It uses Redux to check if the user is authenticated. If the user is
|
||||
* authenticated, it renders the component passed to it; otherwise, it redirects the user to the login page.
|
||||
*
|
||||
* @param {Object} props - The props passed to the ProtectedRoute component.
|
||||
* @param {React.ComponentType} props.Component - The component to render if the user is authenticated.
|
||||
* @param {Object} rest - The remaining props passed to the Route component.
|
||||
* @returns {React.ReactElement} A Route component that conditionally renders the passed Component or redirects to the login page.
|
||||
*/
|
||||
|
||||
const ProtectedRoute = ({ Component, ...rest }) => {
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
return authState.authToken ? (
|
||||
<Component {...rest} />
|
||||
) : (
|
||||
<Navigate to="/login" replace />
|
||||
);
|
||||
};
|
||||
|
||||
ProtectedRoute.propTypes = {
|
||||
Component: PropTypes.elementType.isRequired,
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
@@ -2,21 +2,12 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import React, { useState } from "react";
|
||||
import AnnouncementsDualButtonWithIcon from "../../Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
import { Box, Divider, Stack, Typography } from "@mui/material";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import PasswordTextField from "../../TextFields/Password/PasswordTextField";
|
||||
import { editPasswordValidation } from "../../../Validation/validation";
|
||||
|
||||
/**
|
||||
* PasswordPanel component manages the form for editing password.
|
||||
@@ -31,20 +22,85 @@ const PasswordPanel = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
//TODO - implement save password function
|
||||
const handleSavePassword = () => {
|
||||
//for testing, will tweak when I implement redux slice
|
||||
const idToName = {
|
||||
"edit-current-password": "password",
|
||||
//TBD - form field naming
|
||||
"edit-new-password": "newpassword",
|
||||
"edit-confirm-password": "confirm",
|
||||
};
|
||||
const [localData, setLocalData] = useState({
|
||||
password: "",
|
||||
newpassword: "",
|
||||
confirm: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = editPasswordValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { newpassword: localData.newpassword } }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[name];
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
const { error } = editPasswordValidation.validate(localData, {
|
||||
abortEarly: false,
|
||||
context: { newpassword: localData.newpassword },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
//TODO - submit logic
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsOpen(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<TabPanel value="1">
|
||||
<form className="edit-password-form" noValidate spellCheck="false">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="edit-password-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<AnnouncementsDualButtonWithIcon
|
||||
icon={<InfoOutlinedIcon style={{ fill: theme.palette.secondary.main }} />}
|
||||
icon={
|
||||
<InfoOutlinedIcon
|
||||
style={{ fill: theme.palette.secondary.main }}
|
||||
/>
|
||||
}
|
||||
subject="SSO login"
|
||||
body="Since you logged in via SSO, you cannot reset or modify your password."
|
||||
/>
|
||||
@@ -52,64 +108,54 @@ const PasswordPanel = () => {
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack>
|
||||
<Typography variant="h4" component="h1">
|
||||
Current Password
|
||||
Current password
|
||||
</Typography>
|
||||
</Stack>
|
||||
<FormControl sx={{ flex: 1 }}>
|
||||
<OutlinedInput
|
||||
<Stack>
|
||||
<PasswordTextField
|
||||
id="edit-current-password"
|
||||
value="RandomPasswordLol"
|
||||
type={showPassword ? "text" : "password"}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setShowPassword((show) => !show)}
|
||||
sx={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
></OutlinedInput>
|
||||
</FormControl>
|
||||
label={null}
|
||||
placeholder="Enter your current password"
|
||||
autoComplete="current-password"
|
||||
visibility={showPassword}
|
||||
setVisibility={setShowPassword}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-current-password"]] ? true : false}
|
||||
/>
|
||||
{errors[idToName["edit-current-password"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors[idToName["edit-current-password"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack>
|
||||
<Typography variant="h4" component="h1">
|
||||
Password
|
||||
New password
|
||||
</Typography>
|
||||
</Stack>
|
||||
<FormControl sx={{ flex: 1 }}>
|
||||
<OutlinedInput
|
||||
id="edit-password"
|
||||
value="RandomPasswordLol"
|
||||
type={showPassword ? "text" : "password"}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setShowPassword((show) => !show)}
|
||||
sx={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
></OutlinedInput>
|
||||
</FormControl>
|
||||
<Stack>
|
||||
<PasswordTextField
|
||||
id="edit-new-password"
|
||||
label={null}
|
||||
placeholder="Enter your new password"
|
||||
autoComplete="new-password"
|
||||
visibility={showPassword}
|
||||
setVisibility={setShowPassword}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-new-password"]] ? true : false}
|
||||
/>
|
||||
{errors[idToName["edit-new-password"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors[idToName["edit-new-password"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack>
|
||||
@@ -117,6 +163,28 @@ const PasswordPanel = () => {
|
||||
Confirm new password
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<PasswordTextField
|
||||
id="edit-confirm-password"
|
||||
label={null}
|
||||
placeholder="Reenter your new password"
|
||||
autoComplete="new-password"
|
||||
visibility={showPassword}
|
||||
setVisibility={setShowPassword}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-confirm-password"]] ? true : false}
|
||||
/>
|
||||
{errors[idToName["edit-confirm-password"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors[idToName["edit-confirm-password"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack></Stack>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<AnnouncementsDualButtonWithIcon
|
||||
icon={<WarningAmberOutlinedIcon style={{ fill: "#f79009" }} />}
|
||||
@@ -130,9 +198,10 @@ const PasswordPanel = () => {
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Save"
|
||||
onClick={handleSavePassword}
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
sx={{
|
||||
paddingX: "40px",
|
||||
}}
|
||||
|
||||
@@ -2,16 +2,24 @@ import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Divider,
|
||||
IconButton,
|
||||
Modal,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import Button from "../../Button";
|
||||
import EmailTextField from "../../TextFields/Email/EmailTextField";
|
||||
import StringTextField from "../../TextFields/Text/TextField";
|
||||
import Avatar from "../../Avatar";
|
||||
import { editProfileValidation } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { update } from "../../../Features/Auth/authSlice";
|
||||
import ImageField from "../../TextFields/Image";
|
||||
import DoDisturbAltIcon from "@mui/icons-material/DoDisturbAlt";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
/**
|
||||
* ProfilePanel component displays a form for editing user profile information
|
||||
@@ -23,34 +31,78 @@ import Button from "../../Button";
|
||||
|
||||
const ProfilePanel = () => {
|
||||
const theme = useTheme();
|
||||
//TODO - use redux loading state
|
||||
//!! - currently all loading buttons are tied to the same state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { user, authToken, isLoading } = useSelector((state) => state.auth);
|
||||
const idToName = {
|
||||
"edit-first-name": "firstname",
|
||||
"edit-last-name": "lastname",
|
||||
//Disabled for now, will revisit in the future
|
||||
// "edit-email": "email",
|
||||
};
|
||||
const [localData, setLocalData] = useState({
|
||||
firstname: user.firstname,
|
||||
lastname: user.lastname,
|
||||
//Disabled for now, will revisit in the future
|
||||
// email: user.email,
|
||||
//TODO - upload picture
|
||||
profilePicUrl: "placeholder",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isOpen, setIsOpen] = useState("");
|
||||
const isModalOpen = (name) => isOpen === name;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = editProfileValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[name];
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
|
||||
//TODO - implement delete profile picture function
|
||||
const handleDeletePicture = () => {
|
||||
setIsLoading(true);
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
//TODO - implement update profile function
|
||||
const handleUpdatePicture = () => {};
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
//TODO - implement delete account function
|
||||
const handleDeleteAccount = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsOpen(false);
|
||||
}, 2000);
|
||||
};
|
||||
const handleDeleteAccount = () => {};
|
||||
//TODO - implement save profile function
|
||||
const handleSaveProfile = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsOpen(false);
|
||||
}, 2000);
|
||||
const handleSaveProfile = (event) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
localData.firstname === user.firstname &&
|
||||
localData.lastname === user.lastname &&
|
||||
localData.profilePicUrl === user.profilePicUrl
|
||||
) {
|
||||
//TODO - add toast(profile data is unchanged) and maybe disable button
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(update({ authToken, localData }));
|
||||
//TODO - add toast confirmation
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -62,14 +114,24 @@ const ProfilePanel = () => {
|
||||
First Name
|
||||
</Typography>
|
||||
</Stack>
|
||||
{/* TODO - use existing textfield components */}
|
||||
<TextField
|
||||
id="edit-first-name"
|
||||
placeholder="Enter your first name"
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
<Stack>
|
||||
<StringTextField
|
||||
id="edit-first-name"
|
||||
label={null}
|
||||
value={localData.firstname}
|
||||
placeholder="Enter your first name"
|
||||
autoComplete="given-name"
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-first-name"]] ? true : false}
|
||||
/>
|
||||
{errors[idToName["edit-first-name"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors[idToName["edit-first-name"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className="edit-profile-form__wrapper">
|
||||
<Stack>
|
||||
@@ -77,14 +139,24 @@ const ProfilePanel = () => {
|
||||
Last Name
|
||||
</Typography>
|
||||
</Stack>
|
||||
{/* TODO - use existing textfield components */}
|
||||
<TextField
|
||||
id="edit-last-name"
|
||||
placeholder="Enter your last name"
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
<Stack>
|
||||
<StringTextField
|
||||
id="edit-last-name"
|
||||
label={null}
|
||||
value={localData.lastname}
|
||||
placeholder="Enter your last name"
|
||||
autoComplete="family-name"
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-last-name"]] ? true : false}
|
||||
/>
|
||||
{errors[idToName["edit-last-name"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors[idToName["edit-last-name"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className="edit-profile-form__wrapper">
|
||||
<Stack>
|
||||
@@ -92,17 +164,28 @@ const ProfilePanel = () => {
|
||||
Email
|
||||
</Typography>
|
||||
<Typography variant="h5" component="p">
|
||||
After updating, you'll receive a confirmation email.
|
||||
This is your current email address — it cannot be changed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
{/* TODO - use existing textfield components */}
|
||||
<TextField
|
||||
id="edit-email"
|
||||
placeholder="Enter your email"
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
<Stack>
|
||||
<EmailTextField
|
||||
id="edit-email"
|
||||
label={null}
|
||||
value={user.email}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
// onChange={handleChange}
|
||||
// error={errors[idToName["edit-email"]] ? true : false}
|
||||
disabled={true}
|
||||
/>
|
||||
{errors[idToName["edit-email"]] ? (
|
||||
<Typography variant="h5" component="p" className="input-error">
|
||||
{errors[idToName["edit-email"]]}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
<div className="edit-profile-form__wrapper">
|
||||
<Stack>
|
||||
@@ -114,24 +197,29 @@ const ProfilePanel = () => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack className="row-stack" direction="row" alignItems="center">
|
||||
{/* TODO - Use Avatar component instead of @mui */}
|
||||
{/* TODO - Update placeholder values with redux data */}
|
||||
<Avatar
|
||||
alt="Remy Sharp"
|
||||
src="/static/images/avatar/2.jpg"
|
||||
className="icon-button-avatar"
|
||||
style={{ width: "64px", height: "64px" }}
|
||||
firstName={localData.firstname}
|
||||
lastName={localData.lastname}
|
||||
sx={{
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
border: "none",
|
||||
mr: "8px",
|
||||
}}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
level="tertiary"
|
||||
label="Delete"
|
||||
onClick={handleDeletePicture}
|
||||
isLoading={isLoading}
|
||||
isLoading={loading}
|
||||
/>
|
||||
{/* TODO - modal popup for update pfp? */}
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Update"
|
||||
onClick={handleUpdatePicture}
|
||||
onClick={() => setIsOpen("picture")}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
}}
|
||||
@@ -152,6 +240,7 @@ const ProfilePanel = () => {
|
||||
onClick={handleSaveProfile}
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
sx={{
|
||||
paddingX: "40px",
|
||||
}}
|
||||
@@ -174,7 +263,7 @@ const ProfilePanel = () => {
|
||||
<Button
|
||||
level="error"
|
||||
label="Delete account"
|
||||
onClick={() => setIsOpen(true)}
|
||||
onClick={() => setIsOpen("delete")}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -184,8 +273,8 @@ const ProfilePanel = () => {
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-account"
|
||||
aria-describedby="delete-account-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
open={isModalOpen("delete")}
|
||||
onClose={() => setIsOpen("")}
|
||||
disablePortal
|
||||
>
|
||||
<Stack
|
||||
@@ -222,7 +311,7 @@ const ProfilePanel = () => {
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
onClick={() => setIsOpen(false)}
|
||||
onClick={() => setIsOpen("")}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
level="error"
|
||||
@@ -233,6 +322,71 @@ const ProfilePanel = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<Modal
|
||||
aria-labelledby="modal-update-picture"
|
||||
aria-describedby="update-profile-picture"
|
||||
open={isModalOpen("picture")}
|
||||
onClose={() => setIsOpen("")}
|
||||
disablePortal
|
||||
>
|
||||
<Stack
|
||||
gap="20px"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: "white",
|
||||
border: "solid 1px #f2f2f2",
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
boxShadow: 24,
|
||||
p: "30px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
mt="-10px"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography id="modal-update-picture" variant="h4" component="h1">
|
||||
Upload Image
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={() => setIsOpen("")}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<ImageField id="update-profile-picture" />
|
||||
<Stack direction="row" mt="10px" justifyContent="space-between">
|
||||
<Button level="secondary" label="Edit" disabled />
|
||||
<Stack direction="row" gap="10px" justifyContent="flex-end">
|
||||
<ButtonSpinner
|
||||
level="tertiary"
|
||||
label="Remove"
|
||||
onClick={handleDeletePicture}
|
||||
isLoading={loading}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Update"
|
||||
onClick={handleUpdatePicture}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,12 +13,14 @@ import { useTheme } from "@mui/material";
|
||||
* @param {function} props.onChange - The function to call when the text field changes.
|
||||
* @param {string} props.id - The id of the text field.
|
||||
* @param {string} [props.label="Email"] - The label of the text field.
|
||||
* @param {string} props.value - The value of the text field.
|
||||
* @param {string} props.variant - The variant of the text field.
|
||||
* @param {string} props.placeholder - The placeholder of the text field.
|
||||
* @param {React.Element} props.icon - The icon to be displayed in the text field.
|
||||
* @param {string} props.helperText - The helper text to be displayed below the text field.
|
||||
* @param {boolean} props.error - If true, the text field will indicate an error state.
|
||||
*
|
||||
* @param {boolean} props.disabled - If true, the text field will be disabled and cannot be interacted with.
|
||||
*
|
||||
* @example
|
||||
* // To use this component, import it and use it in your JSX like this:
|
||||
* <EmailTextField id="email" variant="outlined" placeholder="Enter your email" />
|
||||
@@ -30,12 +32,14 @@ const EmailTextField = ({
|
||||
onChange,
|
||||
id,
|
||||
label = "Email",
|
||||
value = undefined,
|
||||
variant,
|
||||
placeholder,
|
||||
autoComplete,
|
||||
icon,
|
||||
helperText,
|
||||
error,
|
||||
disabled
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -59,9 +63,11 @@ const EmailTextField = ({
|
||||
error={error}
|
||||
className="email-text-field-input"
|
||||
id={id}
|
||||
value={value}
|
||||
variant={variant}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoComplete}
|
||||
disabled={disabled}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
@@ -78,7 +84,7 @@ const EmailTextField = ({
|
||||
style={{ fill: "red", width: "16px", height: "16px" }}
|
||||
/>
|
||||
)}
|
||||
{!error && showIcon && (
|
||||
{!error && showIcon && !disabled && (
|
||||
<HelpOutlineIcon
|
||||
className="text-field-icon"
|
||||
style={{ width: "16px", height: "16px" }}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.image-field-wrapper h2.MuiTypography-root,
|
||||
.image-field-wrapper p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.image-field-wrapper h2.MuiTypography-root {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.image-field-wrapper p.MuiTypography-root {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.image-field-wrapper h2.MuiTypography-root span {
|
||||
color: var(--env-var-color-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, IconButton, Stack, TextField, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* @param {Object} props - The component props.
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
const ImageField = ({ id, onChange }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handleDragEnter = () => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="image-field-wrapper"
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "fit-content",
|
||||
height: "fit-content",
|
||||
border: "dashed",
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
borderSpacing: "10px",
|
||||
borderColor: isDragging
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.otherColors.graishWhite,
|
||||
borderWidth: "2px",
|
||||
transition: "0.2s",
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
}}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDragLeave}
|
||||
>
|
||||
<TextField
|
||||
id={id}
|
||||
type="file"
|
||||
sx={{
|
||||
"& .MuiInputBase-input[type='file']": {
|
||||
opacity: 0,
|
||||
cursor: "pointer",
|
||||
padding: 10,
|
||||
maxHeight: "300px",
|
||||
height: "100%",
|
||||
},
|
||||
"& fieldset": {
|
||||
padding: 0,
|
||||
border: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
className="custom-file-text"
|
||||
alignItems="center"
|
||||
gap="10px"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: "-1",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
borderRadius: "2px",
|
||||
border: `solid 2px ${theme.palette.otherColors.graishWhite}`,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h4" component="h2">
|
||||
<span>Click to upload</span> or drag and drop
|
||||
</Typography>
|
||||
<Typography variant="h4" component="p">
|
||||
(max. 800x400px)
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageField;
|
||||
@@ -1,9 +1,11 @@
|
||||
import "./passwordTextField.css";
|
||||
import React, { useState } from "react";
|
||||
import { InputAdornment, TextField } from "@mui/material";
|
||||
import { IconButton, InputAdornment, TextField } from "@mui/material";
|
||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||
import { useTheme } from "@mui/material";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
|
||||
/**
|
||||
* @component
|
||||
@@ -16,6 +18,8 @@ import { useTheme } from "@mui/material";
|
||||
* @param {"error" | "help"} [props.icon] - The type of icon to display (error or help) (optional)
|
||||
* @param {string} props.helperText - The helper text displayed below the text field (optional)
|
||||
* @param {boolean} props.error - A flag indicating if the text field has an error (required)
|
||||
* @param {boolean} props.visibility - A flag indicating if the password visibility is toggled (optional)
|
||||
* @param {function} props.setVisibility - Function to toggle password visibility (optional)
|
||||
* @returns {JSX.Element} - Renders the password text field component with dynamic icon display
|
||||
*/
|
||||
const PasswordTextField = ({
|
||||
@@ -28,6 +32,8 @@ const PasswordTextField = ({
|
||||
icon,
|
||||
helperText,
|
||||
error,
|
||||
visibility,
|
||||
setVisibility,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -48,7 +54,7 @@ const PasswordTextField = ({
|
||||
<div className="password-text-field">
|
||||
<TextField
|
||||
onChange={onChange}
|
||||
type="password"
|
||||
type={visibility ? "text" : "password"}
|
||||
autoComplete={autoComplete}
|
||||
error={error}
|
||||
className="password-text-field-input"
|
||||
@@ -58,26 +64,43 @@ const PasswordTextField = ({
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<div className="icon-holder">
|
||||
{error && showIcon && (
|
||||
<ErrorOutlineIcon
|
||||
className="text-field-icon"
|
||||
style={{ fill: "red", width: "16px", height: "16px" }}
|
||||
/>
|
||||
)}
|
||||
{error && !showIcon && (
|
||||
<ErrorOutlineIcon
|
||||
className="text-field-icon"
|
||||
style={{ fill: "red", width: "16px", height: "16px" }}
|
||||
/>
|
||||
)}
|
||||
{!error && showIcon && (
|
||||
<HelpOutlineIcon
|
||||
className="text-field-icon"
|
||||
style={{ width: "16px", height: "16px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{visibility !== undefined ? (
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setVisibility((show) => !show)}
|
||||
tabIndex={-1}
|
||||
sx={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!visibility ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
) : (
|
||||
<div className="icon-holder">
|
||||
{error && showIcon && (
|
||||
<ErrorOutlineIcon
|
||||
className="text-field-icon"
|
||||
style={{ fill: "red", width: "16px", height: "16px" }}
|
||||
/>
|
||||
)}
|
||||
{error && !showIcon && (
|
||||
<ErrorOutlineIcon
|
||||
className="text-field-icon"
|
||||
style={{ fill: "red", width: "16px", height: "16px" }}
|
||||
/>
|
||||
)}
|
||||
{!error && showIcon && (
|
||||
<HelpOutlineIcon
|
||||
className="text-field-icon"
|
||||
style={{ width: "16px", height: "16px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTheme } from "@mui/material";
|
||||
* @param {function} props.onChange - The function to call when the text field changes.
|
||||
* @param {string} props.id - The ID of the text field.
|
||||
* @param {string} props.label - The label text for the text field.
|
||||
* @param {string} props.value - The value of the text field.
|
||||
* @param {string} props.variant - The variant of the text field.
|
||||
* @param {string} props.placeholder - The placeholder text for the text field.
|
||||
* @param {ReactNode} props.icon - The icon to display in the text field.
|
||||
@@ -22,6 +23,7 @@ const StringTextField = ({
|
||||
autoComplete,
|
||||
id,
|
||||
label,
|
||||
value = undefined,
|
||||
variant,
|
||||
placeholder,
|
||||
icon,
|
||||
@@ -51,6 +53,7 @@ const StringTextField = ({
|
||||
className="email-text-field-input"
|
||||
id={id}
|
||||
variant={variant}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoComplete}
|
||||
InputProps={{
|
||||
|
||||
@@ -18,7 +18,10 @@ export const register = createAsyncThunk(
|
||||
const res = await axios.post(`${BASE_URL}/auth/register`, form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -28,10 +31,47 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||
const res = await axios.post(`${BASE_URL}/auth/login`, form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
export const update = createAsyncThunk(
|
||||
"auth/update",
|
||||
async (data, thunkApi) => {
|
||||
const { authToken: token, localData: form } = data;
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
//1.5s delay to show loading spinner
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
const res = await axios.post(`${BASE_URL}/auth/user/${user._id}`, form, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAccount = createAsyncThunk(
|
||||
"auth/delete",
|
||||
async (form, thunkApi) => {
|
||||
try {
|
||||
//TODO
|
||||
} catch (error) {}
|
||||
}
|
||||
);
|
||||
|
||||
const handleAuthFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
@@ -42,30 +82,60 @@ const handleAuthFulfilled = (state, action) => {
|
||||
state.user = user;
|
||||
};
|
||||
const handleAuthRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to login or register";
|
||||
};
|
||||
const handleUpdateFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.user = action.payload.data;
|
||||
};
|
||||
const handleUpdateRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update profile data.";
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {},
|
||||
reducers: {
|
||||
clearAuthState: (state) => {
|
||||
state.authToken = "";
|
||||
state.user = "";
|
||||
state.isLoading = false;
|
||||
state.success = true;
|
||||
state.msg = "Logged out successfully";
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// Register thunk
|
||||
.addCase(register.pending, (state, action) => {
|
||||
.addCase(register.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(register.fulfilled, handleAuthFulfilled)
|
||||
.addCase(register.rejected, handleAuthRejected)
|
||||
// Login thunk
|
||||
.addCase(login.pending, (state, action) => {
|
||||
.addCase(login.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(login.fulfilled, handleAuthFulfilled)
|
||||
.addCase(login.rejected, handleAuthRejected);
|
||||
.addCase(login.rejected, handleAuthRejected)
|
||||
// Update thunk
|
||||
.addCase(update.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(update.fulfilled, handleUpdateFulfilled)
|
||||
.addCase(update.rejected, handleUpdateRejected);
|
||||
},
|
||||
});
|
||||
|
||||
export default authSlice.reducer;
|
||||
export const { clearAuthState } = authSlice.actions;
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from "axios";
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
@@ -17,7 +18,10 @@ export const getMonitors = createAsyncThunk(
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -25,11 +29,22 @@ export const getMonitors = createAsyncThunk(
|
||||
export const getMonitorsByUserId = createAsyncThunk(
|
||||
"montiors/getMonitorsByUserId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
// TODO get userId and make request
|
||||
console.log(token);
|
||||
const res = await axios.get(
|
||||
import.meta.env.VITE_APP_API_BASE_URL + "/monitors/user/" + user._id,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -37,7 +52,14 @@ export const getMonitorsByUserId = createAsyncThunk(
|
||||
const monitorsSlice = createSlice({
|
||||
name: "monitors",
|
||||
initialState,
|
||||
reducers: {},
|
||||
reducers: {
|
||||
clearMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitors = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
@@ -50,13 +72,14 @@ const monitorsSlice = createSlice({
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
console.log(action.payload);
|
||||
state.monitors = action.payload.data;
|
||||
})
|
||||
.addCase(getMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting montiors failed";
|
||||
})
|
||||
// *****************************************************
|
||||
// Monitors by userId
|
||||
@@ -68,17 +91,18 @@ const monitorsSlice = createSlice({
|
||||
.addCase(getMonitorsByUserId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
console.log(action.payload);
|
||||
state.monitors = action.payload.data;
|
||||
})
|
||||
.addCase(getMonitorsByUserId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting montiors failed";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMonitors } = monitorsSlice.actions;
|
||||
export const { setMonitors, clearMonitorState } = monitorsSlice.actions;
|
||||
|
||||
export default monitorsSlice.reducer;
|
||||
@@ -3,24 +3,12 @@ import { Outlet } from "react-router";
|
||||
import DashboardSidebar from "../../Components/DashboardSidebar";
|
||||
import "./index.css";
|
||||
|
||||
// Demo, remove me
|
||||
import { useSelector } from "react-redux";
|
||||
// Emd Demo
|
||||
|
||||
const HomeLayout = () => {
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const { user, msg } = authState;
|
||||
return (
|
||||
<div className="home-layout">
|
||||
<NavBar />
|
||||
<div className="main-content">
|
||||
<DashboardSidebar />
|
||||
{/* Remove me later */}
|
||||
{user && <p>{user.firstname}</p>}
|
||||
{user && <p>{user.lastname}</p>}
|
||||
{user && <p>{user.email}</p>}
|
||||
{msg && <p>Msg: {msg}</p>}
|
||||
{/* I am a demo */}
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
.monitors,
|
||||
.new-monitor {
|
||||
width: 70vw;
|
||||
padding: 4vw;
|
||||
}
|
||||
|
||||
.monitors-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monitors-bar-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.monitors-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.monitors-gaps-medium {
|
||||
height: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.monitors-gaps-small-plus {
|
||||
height: var(--env-var-spacing-1-plus);
|
||||
}
|
||||
|
||||
.new-monitor-header {
|
||||
color: var(--env-var-color-1);
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.service-check-list-title {
|
||||
color: var(--env-var-color-5);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.service-check-list-desc {
|
||||
color: var(--env-var-color-25);
|
||||
font-size: var(--env-var-font-size-small);
|
||||
}
|
||||
|
||||
.host-status-box-text {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.monitors-dropdown-holder {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
#ports-list {
|
||||
width: 172.92px;
|
||||
height: 12px;
|
||||
font-size: var(--env-var-font-size-small);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.incident-notif-config-title,
|
||||
.adv-setting-title {
|
||||
color: var(--env-var-color-5);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.adv-setting-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-box-inputs,
|
||||
.service-check-list,
|
||||
.incident-notif-config,
|
||||
.advanced-setting-config,
|
||||
.proxy-setting-config {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.monitors-create-button-holder {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
File renamed without changes.
@@ -35,6 +35,7 @@ import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { getMonitors } from "../../Features/Monitors/monitorsSlice";
|
||||
import { getMonitorsByUserId } from "../../Features/Monitors/monitorsSlice";
|
||||
import ImageField from "../../Components/TextFields/Image";
|
||||
|
||||
const cols = [
|
||||
{
|
||||
@@ -415,6 +416,10 @@ const Demo = () => {
|
||||
/>
|
||||
</Stack>
|
||||
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
|
||||
<Stack justifyContent="center" alignItems="center">
|
||||
<ImageField />
|
||||
</Stack>
|
||||
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,9 +52,7 @@ const Login = () => {
|
||||
await loginValidation.validateAsync(form, { abortEarly: false });
|
||||
const action = await dispatch(login(form));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
const token = action.payload.data;
|
||||
localStorage.setItem("token", token);
|
||||
navigate("/");
|
||||
navigate("/monitors");
|
||||
}
|
||||
if (action.meta.requestStatus === "rejected") {
|
||||
const error = new Error("Request rejected");
|
||||
@@ -82,6 +80,10 @@ const Login = () => {
|
||||
setForm(newForm);
|
||||
};
|
||||
|
||||
const handleSignupClick = () => {
|
||||
navigate("/register");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<BackgroundPattern></BackgroundPattern>
|
||||
@@ -139,7 +141,14 @@ const Login = () => {
|
||||
<div className="login-form-v3-spacing" />
|
||||
<div className="new-account-option">
|
||||
Don’t have an account?
|
||||
<span className="new-account-option-span">Sign up</span>
|
||||
<span
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
className="new-account-option-span"
|
||||
>
|
||||
Sign up
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,64 +1,56 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Host from "../../Components/Host";
|
||||
import HostStatus from "../../Components/HostStatus";
|
||||
import HostActions from "../../Components/HostActions";
|
||||
import BarChart from "../../Components/Charts/BarChart/BarChart";
|
||||
import Button from "../../Components/Button";
|
||||
import ServerStatus from "../../Components/Charts/Servers/ServerStatus";
|
||||
import CurrentMonitors from "../../Components/CurrentMonitors";
|
||||
import MockData from "../../Mock/sample_data.json";
|
||||
import PropTypes from "prop-types";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const CurrentStats = (mockdata = true) => {
|
||||
// The hardcoded part is passed as default value for the purpose of demonstration.
|
||||
// Pass an empty array, [], before fetching the data
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
/**
|
||||
* CurrentStats displays the current status of monitor
|
||||
*
|
||||
* @component
|
||||
* @param {Array<Monitor>} monitors - An array of monitor objects to be displayed.
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
if (mockdata) {
|
||||
console.log(MockData.data);
|
||||
setMonitors(
|
||||
MockData.data.map((item) => ({
|
||||
host: Host(item.name, 100, "var(--env-var-color-17)", item.url),
|
||||
status: HostStatus(
|
||||
item.isActive
|
||||
? "var(--env-var-color-20)"
|
||||
: "var(--env-var-color-21)",
|
||||
item.isActive ? "Up" : "Down",
|
||||
item.isActive
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-19)"
|
||||
),
|
||||
team: <BarChart checks={item.checks} />,
|
||||
actions: HostActions(),
|
||||
}))
|
||||
);
|
||||
const CurrentStats = ({ monitors }) => {
|
||||
const navigate = useNavigate();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const up = monitors.reduce((acc, cur) => {
|
||||
if (cur.checks.length > 0) {
|
||||
return cur.checks[cur.checks.length - 1].status === true ? acc + 1 : acc;
|
||||
}
|
||||
}, [mockdata]);
|
||||
return 0;
|
||||
}, 0);
|
||||
|
||||
// useEffect(() => {
|
||||
// fetch("API_URL")
|
||||
// .then((response) => response.json())
|
||||
// .then((data) => setMonitors(data))
|
||||
// .catch((error) => console.error(error));
|
||||
// }, []);
|
||||
const down = monitors.reduce((acc, cur) => {
|
||||
if (cur.checks.length > 0) {
|
||||
return cur.checks[cur.checks.length - 1].status === false ? acc + 1 : acc;
|
||||
}
|
||||
return 0;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<div className="monitors-bar">
|
||||
<div className="monitors-bar-title">Hello, Jackie</div>
|
||||
<div className="monitors-bar-title">
|
||||
Hello, {authState.user.firstname}
|
||||
</div>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create new monitor"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ padding: "10px 20px", fontSize: "13px" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<div className="monitors-stats">
|
||||
<ServerStatus title="Up" value="4" state="up" />
|
||||
<ServerStatus title="Down" value="0" state="down" />
|
||||
<ServerStatus title="Paused" value="0" state="pause" />
|
||||
<ServerStatus title="Up" value={up} state="up" />
|
||||
<ServerStatus title="Down" value={down} state="down" />
|
||||
<ServerStatus title="Paused" value={0} state="pause" />
|
||||
</div>
|
||||
<div className="monitors-gaps-medium"></div>
|
||||
<CurrentMonitors monitors={monitors} />
|
||||
@@ -66,4 +58,8 @@ const CurrentStats = (mockdata = true) => {
|
||||
);
|
||||
};
|
||||
|
||||
CurrentStats.propTypes = {
|
||||
monitors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default CurrentStats;
|
||||
@@ -1,4 +1,3 @@
|
||||
import CreateNewMonitor from "./CreateNewMonitor";
|
||||
import CurrentStats from "./CurrentStats";
|
||||
import "./index.css";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
.announcement-tile {
|
||||
border-color: var(--env-var-color-29);
|
||||
}
|
||||
[class$="-form"] {
|
||||
.settings [class$="-form"] {
|
||||
width: inherit;
|
||||
}
|
||||
[class$="__wrapper"],
|
||||
[class$="__wrapper compact"] {
|
||||
.settings [class$="__wrapper"],
|
||||
.settings [class$="__wrapper compact"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@@ -64,8 +64,9 @@
|
||||
margin-top: var(--env-var-spacing-4);
|
||||
}
|
||||
.edit-team-form__wrapper.compact {
|
||||
margin-top: 30px;
|
||||
color: var(--env-var-color-29);
|
||||
margin-top: var(--env-var-spacing-3);
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
.edit-profile-form__wrapper input,
|
||||
.edit-password-form__wrapper input,
|
||||
@@ -81,7 +82,8 @@
|
||||
.announcement-content-subject {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
#edit-password + div > button > svg,
|
||||
#edit-new-password + div > button > svg,
|
||||
#edit-confirm-password + div > button > svg,
|
||||
#edit-current-password + div > button > svg {
|
||||
color: var(--env-var-color-29);
|
||||
}
|
||||
@@ -95,10 +97,12 @@
|
||||
border-color: var(--env-var-color-27);
|
||||
background-color: var(--lighter-env-var-color-27);
|
||||
}
|
||||
.MuiBox-root > .announcement-tile .announcement-content-body,
|
||||
.MuiBox-root > .announcement-tile .announcement-close svg {
|
||||
.MuiBox-root > .announcement-tile .announcement-content-body {
|
||||
color: var(--env-var-color-28);
|
||||
fill: var(--env-var-color-28) !important;
|
||||
}
|
||||
.MuiBox-root > .announcement-tile .announcement-close svg,
|
||||
.edit-password-form__wrapper > .announcement-tile .announcement-close svg {
|
||||
display: none;
|
||||
}
|
||||
#org-name-flex-container .Mui-disabled {
|
||||
color: var(--env-var-color-2) !important;
|
||||
@@ -116,8 +120,7 @@
|
||||
padding-left: 20px;
|
||||
}
|
||||
.edit-team-form__wrapper td {
|
||||
font-size: (--env-var-font-size-medium);
|
||||
color: var(--env-var-color-29);
|
||||
color: var(--env-var-color-25);
|
||||
}
|
||||
|
||||
#select-actions,
|
||||
@@ -155,3 +158,47 @@
|
||||
min-height: 22px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* for testing, will remove later */
|
||||
@media only screen and (max-width: 1600px) {
|
||||
.settings {
|
||||
max-width: calc(90vw - 250px) !important;
|
||||
}
|
||||
.edit-profile-form__wrapper .MuiStack-root:not(.row-stack),
|
||||
.edit-password-form__wrapper .MuiStack-root:not(.row-stack),
|
||||
.edit-organization-form__wrapper .MuiStack-root:not(.row-stack) {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings .MuiStack-root:has(p.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.settings .text-field-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.settings p.MuiTypography-root.input-error {
|
||||
color: var(--env-var-color-24);
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
top: calc(100% + 5px);
|
||||
}
|
||||
.settings .email-text-field,
|
||||
.settings .password-text-field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-profile-form__wrapper .MuiFormControl-root,
|
||||
.edit-password-form__wrapper .MuiFormControl-root {
|
||||
width: 100%;
|
||||
}
|
||||
.edit-profile-form__wrapper .MuiStack-root:has(.MuiFormControl-root),
|
||||
.edit-password-form__wrapper .MuiStack-root:has(.MuiFormControl-root) {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -27,6 +27,9 @@ const otherColorsGraishWhiteLight = "#f2f2f2";
|
||||
const otherColorsStrongBlue = "#f2f2f2";
|
||||
const otherColorsSlateGray = "#667085";
|
||||
|
||||
//box shadow
|
||||
const shadow =
|
||||
"0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)";
|
||||
|
||||
// Global Font Family
|
||||
const fontFamilyDefault = '"Roboto", "Helvetica", "Arial", sans-serif';
|
||||
@@ -73,13 +76,14 @@ const theme = createTheme({
|
||||
slateGray: otherColorsSlateGray,
|
||||
},
|
||||
},
|
||||
font :{
|
||||
font: {
|
||||
default: {
|
||||
font: fontFamilyDefault,
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 4,
|
||||
boxShadow: shadow,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -63,9 +63,82 @@ const newPasswordValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const editProfileValidation = joi.object({
|
||||
firstname: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({
|
||||
"string.empty": "*First name is required.",
|
||||
"string.pattern.base": "*First name must contain only letters.",
|
||||
}),
|
||||
lastname: joi.string().trim().pattern(new RegExp("^[A-Za-z]+$")).messages({
|
||||
"string.empty": "*Last name is required.",
|
||||
"string.pattern.base": "*Last name must contain only letters.",
|
||||
}),
|
||||
email: joi
|
||||
.string()
|
||||
.trim()
|
||||
.email({ tlds: { allow: false } })
|
||||
.messages({
|
||||
"string.empty": "*Email is required.",
|
||||
"string.email": "*Invalid email address.",
|
||||
}),
|
||||
});
|
||||
|
||||
const editPasswordValidation = joi.object({
|
||||
// TBD - validation for current password ?
|
||||
password : joi
|
||||
.string().trim()
|
||||
.messages({
|
||||
"string.empty": "*Current password is required.",
|
||||
}),
|
||||
newpassword: joi
|
||||
.string()
|
||||
.trim()
|
||||
.min(8)
|
||||
.messages({
|
||||
"string.empty": "*Password is required.",
|
||||
"string.min": "*Password must be at least 8 characters long.",
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
if (!/[A-Z]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one uppercase letter."
|
||||
);
|
||||
}
|
||||
if (!/[a-z]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one lowercase letter."
|
||||
);
|
||||
}
|
||||
if (!/\d/.test(value)) {
|
||||
return helpers.message("*Password must contain at least one number.");
|
||||
}
|
||||
if (!/[!@#$%^&*]/.test(value)) {
|
||||
return helpers.message(
|
||||
"*Password must contain at least one special character."
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}),
|
||||
confirm: joi
|
||||
.string()
|
||||
.trim()
|
||||
.messages({
|
||||
"string.empty": "*Password confirmation is required.",
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
const { password } = helpers.prefs.context;
|
||||
if (value !== password) {
|
||||
return helpers.message("*Passwords do not match.");
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
});
|
||||
|
||||
export {
|
||||
registerValidation,
|
||||
loginValidation,
|
||||
recoveryValidation,
|
||||
newPasswordValidation,
|
||||
editPasswordValidation,
|
||||
editProfileValidation,
|
||||
};
|
||||
@@ -3,9 +3,9 @@
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
/* color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
background-color: #242424; */
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
@@ -47,7 +47,7 @@
|
||||
--env-var-color-27: #fecf60;
|
||||
--lighter-env-var-color-27: rgba(254, 207, 96, 0.1);
|
||||
--env-var-color-28: #f79009;
|
||||
--env-var-color-29: #667085;
|
||||
--env-var-color-29: #D0D5DD;
|
||||
|
||||
--env-var-radius-1: 4px;
|
||||
--env-var-radius-2: 8px;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
*.sh
|
||||
!build_images.sh
|
||||
mongo/data/*
|
||||
redis/data/*
|
||||
*.env
|
||||
Regular → Executable
+7
-3
@@ -4,15 +4,19 @@
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd ..
|
||||
|
||||
#Client
|
||||
client="./Docker/client.Dockerfile"
|
||||
|
||||
# MongoDB
|
||||
mongoDB="./docker/mongoDB.Dockerfile"
|
||||
mongoDB="./Docker/mongoDB.Dockerfile"
|
||||
|
||||
# Redis
|
||||
redis="./docker/redis.Dockerfile"
|
||||
redis="./Docker/redis.Dockerfile"
|
||||
|
||||
# Server
|
||||
server="./docker/server.Dockerfile"
|
||||
server="./Docker/server.Dockerfile"
|
||||
|
||||
docker build -f $client -t uptime_client .
|
||||
docker build -f $mongoDB -t uptime_database_mongo .
|
||||
docker build -f $redis -t uptime_redis .
|
||||
docker build -f $server -t uptime_server .
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM node:20
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./Client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ./Client .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
RUN npm install -g serve
|
||||
|
||||
CMD ["serve","-s", "dist", "-l", "5173"]
|
||||
|
||||
EXPOSE 5000
|
||||
@@ -1,11 +1,19 @@
|
||||
version: "3"
|
||||
services:
|
||||
client:
|
||||
image: uptime_client:latest
|
||||
ports:
|
||||
- "5173:5173"
|
||||
env_file:
|
||||
- client.env
|
||||
depends_on:
|
||||
- server
|
||||
server:
|
||||
image: uptime_server:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
env_file:
|
||||
- .env
|
||||
- server.env
|
||||
depends_on:
|
||||
- redis
|
||||
- mongodb
|
||||
File renamed without changes.
File renamed without changes.
@@ -2,11 +2,11 @@ FROM node:20
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../../package*.json ./
|
||||
COPY ./Server/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ../../ ./
|
||||
COPY ./Server/ ./
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
@@ -27,18 +27,17 @@ You can see the designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgD
|
||||
|
||||
- Clone this repository to your local machine
|
||||
|
||||
1. [Docker Quickstart](#docker-compose)
|
||||
1. [Installation (Client)](#client)
|
||||
2. [Configuration(Client)](#config-client)
|
||||
1. [Configuration(Client)](#config-client)
|
||||
- [Environment](#env-vars-client)
|
||||
3. [Getting Started (Server)](#server)
|
||||
- [Docker Compose Quickstart](#docker-quickstart)
|
||||
- [Manual Installation](#manual-install)
|
||||
- [Install Server](#install-server)
|
||||
- [Environment](#env-vars-server)
|
||||
- [Database](#databases)
|
||||
- [(Optional) Dockerised Databases](#optional-docker-databases)
|
||||
- [Start Server](#start-server)
|
||||
4. [Endpoints](#endpoints)
|
||||
1. [Getting Started (Server)](#server)
|
||||
- [Install Server](#install-server)
|
||||
- [Environment](#env-vars-server)
|
||||
- [Database](#databases)
|
||||
- [(Optional) Dockerised Databases](#optional-docker-databases)
|
||||
- [Start Server](#start-server)
|
||||
1. [Endpoints](#endpoints)
|
||||
###### Auth
|
||||
- <code>POST</code> [/api/v1/auth/register](#post-register)
|
||||
- <code>POST</code> [/api/v1/auth/login](#post-login)
|
||||
@@ -64,11 +63,45 @@ You can see the designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgD
|
||||
- <code>GET</code> [/api/v1/alerts/{alertId}](#get-alert-alert-id)
|
||||
- <code>POST</code> [/api/v1/alerts/edit/{alertId}](#edit-alert)
|
||||
- <code>POST</code> [/api/v1/alerts/delete/{alertId}](#delete-alert)
|
||||
5. [Error Handling](#error-handling)
|
||||
6. [Contributors](#contributors)
|
||||
1. [Error Handling](#error-handling)
|
||||
1. [Contributors](#contributors)
|
||||
|
||||
---
|
||||
|
||||
### Docker Quick Start
|
||||
|
||||
#### <u>Docker Quickstart</u> <a id="docker-quickstart"></a>
|
||||
|
||||
The fastest way to start the application is to use our Dockerfiles and [Docker Compose](https://docs.docker.com/compose/).
|
||||
|
||||
To get the application up and running you need to:
|
||||
|
||||
1. In the `Docker` directory run the build script `build_images.sh` to build docker images for the client, server, Redis database, and MongoDB database.
|
||||
2. In the `Docker` directory, create a `server.env` file with the [requried environtmental variables](#env-vars-server) for the server. Sample file:
|
||||
|
||||
```
|
||||
CLIENT_HOST="http://localhost:5173"
|
||||
JWT_SECRET="my_secret"
|
||||
DB_TYPE="MongoDB"
|
||||
DB_CONNECTION_STRING="mongodb://mongodb:27017/uptime_db"
|
||||
REDIS_HOST="redis"
|
||||
REDIS_PORT=6379
|
||||
SYSTEM_EMAIL_ADDRESS="<email>"
|
||||
SENDGRID_API_KEY="<api_key>"
|
||||
LOGIN_PAGE_URL="<login_page"
|
||||
```
|
||||
|
||||
3. In the `Docker` directory, create a `client.env` file with the [required environtmental variables](#env-vars-cllient) for the client. Sample file:
|
||||
|
||||
```
|
||||
VITE_APP_API_BASE_URL="http://localhost:5000/api/v1"
|
||||
```
|
||||
|
||||
4. In the `Docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all four images.
|
||||
|
||||
That's it, the application is ready to use on port 5173.
|
||||
<br/>
|
||||
|
||||
### Client
|
||||
|
||||
#### <u>Installation</u>
|
||||
@@ -94,33 +127,7 @@ You can see the designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgD
|
||||
|
||||
---
|
||||
|
||||
### Getting Started (Server)
|
||||
|
||||
#### <u>Docker Quickstart</u> <a id="docker-quickstart"></a>
|
||||
|
||||
The fastest way to start the server is to use our Dockerfiles and [Docker Compose](https://docs.docker.com/compose/).
|
||||
|
||||
To get the server up and running you need to:
|
||||
|
||||
1. In the `Server/docker` directory run the build script `build_images.sh` to build docker images for the server, Redis database, and MongoDB database.
|
||||
2. In the `Server/docker` directory, create a `.env` file with the [requried environtmental variables](#env-vars-server). Sample file:
|
||||
|
||||
```
|
||||
CLIENT_HOST="http://localhost:5173"
|
||||
JWT_SECRET="my_secret"
|
||||
DB_TYPE="MongoDB"
|
||||
DB_CONNECTION_STRING="mongodb://mongodb:27017/uptime_db"
|
||||
REDIS_HOST="redis"
|
||||
REDIS_PORT=6379
|
||||
SYSTEM_EMAIL_ADDRESS="<email>"
|
||||
SENDGRID_API_KEY="<api_key>"
|
||||
LOGIN_PAGE_URL="<login_page"
|
||||
```
|
||||
|
||||
3. In the `Server/docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all three images.
|
||||
|
||||
That's it, the server is ready to use.
|
||||
<br/>
|
||||
### Getting Started (Server) <a id="server"></a>
|
||||
|
||||
#### <u>Manual Install</u> <a id="manual-install"></a>
|
||||
|
||||
@@ -135,18 +142,19 @@ That's it, the server is ready to use.
|
||||
|
||||
Configure the server with the following environmental variables:
|
||||
|
||||
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
|
||||
| -------------------- | ----------------- | --------- | ------------------------------------------------------------------------------------------- | ------------------- |
|
||||
| CLIENT_HOST | Required | `string` | Frontend Host | |
|
||||
| JWT_SECRET | Required | `string` | JWT secret | |
|
||||
| DB_TYPE | Optional | `string` | Specify DB to use | `MongoDB \| FakeDB` |
|
||||
| DB_CONNECTION_STRING | Required | `string` | Specifies URL for MongoDB Database | |
|
||||
| PORT | Optional | `integer` | Specifies Port for Server | |
|
||||
| SENDGRID_API_KEY | Required | `string` | Specifies API KEY for SendGrid email service | |
|
||||
| SYSTEM_EMAIL_ADDRESS | Required | `string` | Specifies System email to be used in emailing service, must be a verified email by sendgrid | |
|
||||
| LOGIN_PAGE_URL | Required | `string` | Login url to be used in emailing service | |
|
||||
| REDIS_HOST | Required | `string` | Host address for Redis database | |
|
||||
| REDIS_PORT | Required | `integer` | Port for Redis database | |
|
||||
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
|
||||
| -------------------- | ----------------- | --------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| CLIENT_HOST | Required | `string` | Frontend Host | |
|
||||
| JWT_SECRET | Required | `string` | JWT secret | |
|
||||
| DB_TYPE | Optional | `string` | Specify DB to use | `MongoDB \| FakeDB` |
|
||||
| DB_CONNECTION_STRING | Required | `string` | Specifies URL for MongoDB Database | |
|
||||
| PORT | Optional | `integer` | Specifies Port for Server | |
|
||||
| SENDGRID_API_KEY | Required | `string` | Specifies API KEY for SendGrid email service | |
|
||||
| SYSTEM_EMAIL_ADDRESS | Required | `string` | Specifies System email to be used in emailing service, must be a verified email by sendgrid | |
|
||||
| LOGIN_PAGE_URL | Required | `string` | Login url to be used in emailing service | |
|
||||
| REDIS_HOST | Required | `string` | Host address for Redis database | |
|
||||
| REDIS_PORT | Required | `integer` | Port for Redis database | |
|
||||
| TOKEN_TTL | Optional | string | Time for token to live | In vercel/ms format https://github.com/vercel/ms |
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -161,15 +169,15 @@ You may use the included Dockerfiles to spin up databases quickly if you wish.
|
||||
|
||||
###### (Optional) Dockerised Databases <a id="optional-docker-databases"></a>
|
||||
|
||||
Dockerfiles for the server and databases are located in the `./Server/docker` directory
|
||||
Dockerfiles for the server and databases are located in the `Docker` directory
|
||||
|
||||
<details>
|
||||
<summary><b>MongoDB Image</b></summary>
|
||||
Location: `./Server/docker/mongoDB.Dockerfile`
|
||||
|
||||
The `./Server/docker/mongo/data` directory should be mounted to the MongoDB container in order to persist data.
|
||||
The `Docker/mongo/data` directory should be mounted to the MongoDB container in order to persist data.
|
||||
|
||||
From the `Server/docker` directory run
|
||||
From the `Docker` directory run
|
||||
|
||||
1. Build the image: `docker build -f mongoDB.Dockerfile -t uptime_database_mongo .`
|
||||
2. Run the docker image: `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
|
||||
@@ -179,9 +187,9 @@ From the `Server/docker` directory run
|
||||
<summary><b>Redis Image</b></summary>
|
||||
Location `./Server/docker/redislDockerfile`
|
||||
|
||||
the `./Server/docker/redis/data` directory should be mounted to the Redis container in order to persist data.
|
||||
the `Docker/redis/data` directory should be mounted to the Redis container in order to persist data.
|
||||
|
||||
From the `Server/docker` directory run
|
||||
From the `Docker` directory run
|
||||
|
||||
1. Build the image: `docker build -f redis.Dockerfile -t uptime_redis .`
|
||||
2. Run the image: `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
./docker
|
||||
@@ -2,6 +2,3 @@ node_modules
|
||||
.env
|
||||
*.log
|
||||
*.sh
|
||||
docker/mongo/data/*
|
||||
docker/redis/data/*
|
||||
!docker/build_images.sh
|
||||
@@ -26,7 +26,8 @@ const {
|
||||
*/
|
||||
const issueToken = (payload) => {
|
||||
//TODO Add proper expiration date
|
||||
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "99d" });
|
||||
const tokenTTL = process.env.TOKEN_TTL ? process.env.TOKEN_TTL : "2h";
|
||||
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: tokenTTL });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -223,13 +224,11 @@ const resetPasswordController = async (req, res, next) => {
|
||||
try {
|
||||
await newPasswordValidation.validateAsync(req.body);
|
||||
user = await req.db.resetPassword(req, res);
|
||||
res
|
||||
.status(200)
|
||||
.json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_RESET_PASSWORD,
|
||||
data: user,
|
||||
});
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_RESET_PASSWORD,
|
||||
data: user,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
|
||||
@@ -2,6 +2,7 @@ const axios = require("axios");
|
||||
const ping = require("ping");
|
||||
const logger = require("../utils/logger");
|
||||
const Check = require("../models/Check");
|
||||
const { Console } = require("winston/lib/winston/transports");
|
||||
|
||||
class NetworkService {
|
||||
constructor(db) {
|
||||
@@ -149,9 +150,9 @@ class NetworkService {
|
||||
const insertedCheck = await check.save();
|
||||
return insertedCheck.status;
|
||||
} catch (error) {
|
||||
logger.error(`Error wrtiting check for ${job.id}`, {
|
||||
logger.error(`Error wrtiting check for ${check.monitorId}`, {
|
||||
service: this.SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
monitorId: check.monitorId,
|
||||
error: error,
|
||||
});
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user