diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/Charts/CustomGauge/index.jsx index d9718d72c..614b3774c 100644 --- a/client/src/Components/Charts/CustomGauge/index.jsx +++ b/client/src/Components/Charts/CustomGauge/index.jsx @@ -17,7 +17,6 @@ const MAXIMUM_VALUE = 100; * @param {number} [props.progress=0] - Progress percentage (0-100) * @param {number} [props.radius=60] - Radius of the gauge circle * @param {number} [props.strokeWidth=15] - Width of the gauge stroke - * @param {number} [props.threshold=50] - Threshold for color change * @param {number} [props.precision=1] - Precision of the progress percentage * @param {string} [props.unit="%"] - Unit of progress * @@ -26,7 +25,6 @@ const MAXIMUM_VALUE = 100; * progress={75} * radius={50} * strokeWidth={10} - * threshold={50} * /> * * @returns {React.ReactElement} Rendered CustomGauge component @@ -36,7 +34,6 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, - threshold = 50, precision = 1, unit = "%", }) => { @@ -64,10 +61,14 @@ const CustomGauge = ({ const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE)); - const fillColor = - progressWithinRange > threshold - ? theme.palette.error.lowContrast - : theme.palette.accent.main; + let fillColor; + if (progressWithinRange < 50) { + fillColor = theme.palette.success.main; + } else if (progressWithinRange < 80) { + fillColor = theme.palette.warning.lowContrast; + } else { + fillColor = theme.palette.error.lowContrast; + } if (isLoading) { return ( @@ -142,7 +143,6 @@ CustomGauge.propTypes = { progress: PropTypes.number, radius: PropTypes.number, strokeWidth: PropTypes.number, - threshold: PropTypes.number, precision: PropTypes.number, unit: PropTypes.string, }; diff --git a/client/src/Components/Inputs/Search/index.jsx b/client/src/Components/Inputs/Search/index.jsx index c3008886c..7202cc2eb 100644 --- a/client/src/Components/Inputs/Search/index.jsx +++ b/client/src/Components/Inputs/Search/index.jsx @@ -4,7 +4,6 @@ import { ListItem, Autocomplete, TextField, - Stack, Typography, Checkbox, } from "@mui/material"; @@ -33,7 +32,6 @@ const SearchAdornment = () => { const theme = useTheme(); return ( diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/Inputs/Select/index.jsx index a438b6bb2..c5c488f91 100644 --- a/client/src/Components/Inputs/Select/index.jsx +++ b/client/src/Components/Inputs/Select/index.jsx @@ -50,6 +50,7 @@ const Select = ({ onChange, onBlur, sx, + error = false, name = "", labelControlSpacing = 6, maxWidth, @@ -93,6 +94,7 @@ const Select = ({ onChange={onChange} onBlur={onBlur} displayEmpty + error={error} name={name} inputProps={{ id: id }} IconComponent={KeyboardArrowDownIcon} @@ -112,7 +114,7 @@ const Select = ({ fill: theme.palette.primary.contrastTextTertiary, }, "& .MuiSelect-select": { - padding: "0 13px", + padding: "0", minHeight: "34px", display: "flex", alignItems: "center", @@ -172,6 +174,7 @@ Select.propTypes = { label: PropTypes.string, placeholder: PropTypes.string, isHidden: PropTypes.bool, + error: PropTypes.bool, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]) .isRequired, items: PropTypes.arrayOf( diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx index 56a9a898d..9b46204e3 100644 --- a/client/src/Components/LanguageSelector.jsx +++ b/client/src/Components/LanguageSelector.jsx @@ -30,7 +30,17 @@ const LanguageSelector = () => { value={language} onChange={handleChange} size="small" - sx={{ minWidth: 80 }} + sx={{ + minWidth: 80, + "& .MuiSelect-select": { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + "& .MuiSelect-icon": { + alignSelf: "center", + }, + }} > {languages.map((lang) => { let parsedLang = lang === "en" ? "gb" : lang; @@ -47,11 +57,17 @@ const LanguageSelector = () => { { {getRoleDisplayText(authState.user, t)} - - - - openPopup(event)} - > - - - - + "& svg": { + width: "22px", + height: "22px", + }, + "& svg path": { + /* Vertical three dots */ + stroke: theme.palette.primary.contrastTextTertiary, + }, + }} + onClick={(event) => openPopup(event)} + > + + + { {t("common.appName")} diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 616a37877..970b5d97f 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -202,6 +202,25 @@ const useFetchStatsByMonitorId = ({ return [monitor, audits, isLoading, networkError]; }; +const useFetchMonitorGames = ({ setGames, updateTrigger }) => { + const [isLoading, setIsLoading] = useState(true); + useEffect(() => { + const fetchGames = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorGames(); + setGames(res.data.data); + } catch (error) { + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchGames(); + }, [setGames, updateTrigger]); + return [isLoading]; +}; + const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -357,7 +376,12 @@ const useUpdateMonitor = () => { expectedValue: monitor.expectedValue, ignoreTlsErrors: monitor.ignoreTlsErrors, jsonPath: monitor.jsonPath, - ...(monitor.type === "port" && { port: monitor.port }), + ...((monitor.type === "port" || monitor.type === "game") && { + port: monitor.port, + }), + ...(monitor.type == "game" && { + gameId: monitor.gameId, + }), ...(monitor.type === "hardware" && { thresholds: monitor.thresholds, secret: monitor.secret, @@ -530,4 +554,5 @@ export { useDeleteMonitorStats, useCreateBulkMonitors, useExportMonitors, + useFetchMonitorGames, }; diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx index 8c827f6fb..d6b281c27 100644 --- a/client/src/Pages/Auth/Login/index.jsx +++ b/client/src/Pages/Auth/Login/index.jsx @@ -1,15 +1,11 @@ // Components import Stack from "@mui/material/Stack"; -import AuthHeader from "../components/AuthHeader"; import Button from "@mui/material/Button"; import TextInput from "../../../Components/Inputs/TextInput"; import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; import { loginCredentials } from "../../../Validation/validation"; import TextLink from "../../../Components/TextLink"; -import Typography from "@mui/material/Typography"; -import Box from "@mui/material/Box"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import Background from "../../../assets/Images/background-grid.svg?react"; +import AuthPageWrapper from "../components/AuthPageWrapper"; // Utils import { login } from "../../../Features/Auth/authSlice"; import { useNavigate } from "react-router-dom"; @@ -50,12 +46,10 @@ const Login = () => { [name]: error?.details?.[0]?.message || "", })); }; - const onSubmit = async (e) => { e.preventDefault(); const toSubmit = { ...form }; const { error } = loginCredentials.validate(toSubmit, { abortEarly: false }); - if (error) { const formErrors = {}; for (const err of error.details) { @@ -64,7 +58,6 @@ const Login = () => { setErrors(formErrors); return; } - const action = await dispatch(login(form)); if (action.payload.success) { navigate("/uptime"); @@ -89,150 +82,69 @@ const Login = () => { } } }; - return ( - - - - - - - - - - - - - - - - {t("auth.login.welcome")} - - {t("auth.login.heading")} - - - - } - /> - - - - } /> + - + + + ); }; diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx index 97b27e5cf..2b45d2d15 100644 --- a/client/src/Pages/Auth/Register/index.jsx +++ b/client/src/Pages/Auth/Register/index.jsx @@ -1,10 +1,10 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import AuthHeader from "../components/AuthHeader"; import TextInput from "../../../Components/Inputs/TextInput"; -import Check from "../../../Components/Check/Check"; import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; +import PasswordTooltip from "../components/PasswordTooltip"; // Utils import { useTheme } from "@emotion/react"; @@ -16,6 +16,7 @@ import { useParams } from "react-router-dom"; import { networkService } from "../../../main"; import { newOrChangedCredentials } from "../../../Validation/validation"; import { register } from "../../../Features/Auth/authSlice"; +import AuthPageWrapper from "../components/AuthPageWrapper"; import { createToast } from "../../../Utils/toastUtils"; import PropTypes from "prop-types"; @@ -195,59 +196,38 @@ const Register = ({ superAdminExists }) => { }; return ( - - - {t("auth.registration.heading.user")} - + + {superAdminExists + ? t("auth.registration.description.user") + : t("auth.registration.description.superAdmin")} + - - {superAdminExists - ? t("auth.registration.heading.user") - : t("auth.registration.heading.superAdmin")} - - - {superAdminExists - ? t("auth.registration.description.user") - : t("auth.registration.description.superAdmin")} - - (e.target.value = e.target.value.toLowerCase())} - onChange={onChange} - error={errors.email ? true : false} - helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js - /> { { error={errors.lastName ? true : false} helperText={errors.lastName ? t(errors.lastName) : ""} // Localization keys are in validation.js /> - - - - - - - - - - - + (e.target.value = e.target.value.toLowerCase())} + onChange={onChange} + error={errors.email ? true : false} + helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js + /> + + + + + + + - + ); }; diff --git a/client/src/Pages/Auth/components/AuthPageWrapper.jsx b/client/src/Pages/Auth/components/AuthPageWrapper.jsx new file mode 100644 index 000000000..b83be19c5 --- /dev/null +++ b/client/src/Pages/Auth/components/AuthPageWrapper.jsx @@ -0,0 +1,107 @@ +import Background from "../../../assets/Images/background-grid.svg?react"; +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import AuthHeader from "../components/AuthHeader"; +import { useTheme } from "@mui/material/styles"; +import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import PropTypes from "prop-types"; + +const AuthPageWrapper = ({ children, heading, welcome }) => { + const theme = useTheme(); + return ( + + + + + + + + + + + + + + + + {welcome} + + {heading} + + {children} + + + ); +}; + +export default AuthPageWrapper; + +AuthPageWrapper.propTypes = { + children: PropTypes.node, + heading: PropTypes.node, + welcome: PropTypes.node, +}; diff --git a/client/src/Pages/Auth/components/PasswordTooltip.jsx b/client/src/Pages/Auth/components/PasswordTooltip.jsx new file mode 100644 index 000000000..93a74403a --- /dev/null +++ b/client/src/Pages/Auth/components/PasswordTooltip.jsx @@ -0,0 +1,116 @@ +import Check from "../../../Components/Check/Check"; +import Stack from "@mui/material/Stack"; +import { Tooltip, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; + +const PasswordTooltip = ({ feedback, form, children }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const hasPassword = form.password.length > 0; + const hasInvalidFeedback = Object.values(feedback).some( + (status) => status !== "success" + ); + const showPasswordTooltip = hasPassword && hasInvalidFeedback; + return ( + + + + + + + + + } + slotProps={{ + tooltip: { + sx: { + backgroundColor: theme.palette.tertiary.background, + border: `0.5px solid ${theme.palette.primary.lowContrast}90`, + borderRadius: theme.spacing(4), + color: theme.palette.primary.contrastText, + width: "auto", + maxWidth: { xs: "25vw", md: "none" }, + whiteSpace: { xs: "normal", md: "nowrap" }, + paddingTop: theme.spacing(8), + px: theme.spacing(8), + }, + }, + arrow: { + sx: { + color: theme.palette.tertiary.background, + }, + }, + }} + > + {children} + + ); +}; + +PasswordTooltip.propTypes = { + feedback: PropTypes.shape({ + length: PropTypes.string.isRequired, + special: PropTypes.string, + number: PropTypes.string, + uppercase: PropTypes.string, + lowercase: PropTypes.string, + confirm: PropTypes.string, + }), + form: PropTypes.shape({ + password: PropTypes.string.isRequired, + }), + children: PropTypes.node, +}; + +export default PasswordTooltip; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx index e9f235112..5bb227b7c 100644 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -1,34 +1,80 @@ import Stack from "@mui/material/Stack"; -import Gauge from "../../../../../Components/Charts/CustomGauge"; +import CustomGauge from "../../../../../Components/Charts/CustomGauge"; import Typography from "@mui/material/Typography"; // Utils import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import { getPercentage } from "../../utils/utils"; +import { getPercentage, formatBytes } from "../../utils/utils"; import { useTranslation } from "react-i18next"; +import { Box } from "@mui/material"; -const GaugeBox = ({ title, subtitle, children }) => { - const theme = useTheme(); - return ( - - {children} - - {title} - {subtitle} - +const BaseContainer = ({children}) => { + const theme = useTheme() + return( + + {children} + ); }; -GaugeBox.propTypes = { - title: PropTypes.string.isRequired, - subtitle: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, +const InfrastructureStyleGauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { + const theme = useTheme(); + + const MetricRow = ({ label, value }) => ( + + {label} + + {value} + + + ); + + return( + + + + + + {heading} + + + + + {metricTwo && valueTwo && ( + + )} + + + + ); }; const Gauges = ({ diagnostics, isLoading }) => { @@ -53,50 +99,41 @@ const Gauges = ({ diagnostics, isLoading }) => { return ( - - - - - - - - - - - - + + + + ); }; @@ -106,4 +143,17 @@ Gauges.propTypes = { isLoading: PropTypes.bool, }; +InfrastructureStyleGauge.propTypes = { + value: PropTypes.number, + heading: PropTypes.string, + metricOne: PropTypes.string, + valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + metricTwo: PropTypes.string, + valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), +}; + +BaseContainer.propTypes = { + children: PropTypes.node.isRequired, +}; + export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx index 7c5d1f5a9..72ee47f48 100644 --- a/client/src/Pages/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -1,14 +1,14 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; import Gauges from "./components/gauges"; -import Stats from "./components/stats"; -import Divider from "@mui/material/Divider"; import Button from "@mui/material/Button"; - +import StatBox from "../../../Components/StatBox"; +import StatusBoxes from "../../../Components/StatusBoxes"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useFetchDiagnostics } from "../../../Hooks/logHooks"; +import { getHumanReadableDuration } from "../../../Utils/timeUtils"; +import { formatBytes, getPercentage } from "./utils/utils"; const Diagnostics = () => { // Local state @@ -19,34 +19,57 @@ const Diagnostics = () => { const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics(); // Setup return ( - + + + + + + + + + + - {t("diagnosticsPage.diagnosticDescription")} + - - - - - - - - ); }; diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx index be255ab52..ee5cf48de 100644 --- a/client/src/Pages/Settings/SettingsTimeZone.jsx +++ b/client/src/Pages/Settings/SettingsTimeZone.jsx @@ -58,7 +58,7 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => { inputValue={rawInput} handleInputChange={(val) => setRawInput(val)} handleChange={handleTimezoneChange} - isAdorned={false} + isAdorned={true} unit="timezone" /> diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx index 4c2842b71..dd4345158 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -130,7 +130,7 @@ const TabSettings = ({ inputValue={rawInput} handleInputChange={(newVal) => setRawInput(newVal)} handleChange={handleTimezoneChange} - isAdorned={false} + isAdorned={true} unit="timezone" /> diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 2d918cb9f..f821ea9e4 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -41,6 +41,7 @@ import { useUpdateMonitor, usePauseMonitor, useFetchMonitorById, + useFetchMonitorGames, } from "../../../Hooks/monitorHooks"; /** @@ -81,6 +82,7 @@ const UptimeCreate = ({ isClone = false }) => { const [isOpen, setIsOpen] = useState(false); const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false); + const [games, setGames] = useState({}); const triggerUpdate = () => { setUpdateTrigger(!updateTrigger); }; @@ -89,12 +91,22 @@ const UptimeCreate = ({ isClone = false }) => { const [notifications, notificationsAreLoading, notificationsError] = useGetNotificationsByTeamId(); const { determineState, statusColor } = useMonitorUtils(); - // Network - const [isLoading] = useFetchMonitorById({ + // Fetch monitor details + const [isFetchingMonitor] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger: true, }); + + // Fetch games + const [isFetchingGames] = useFetchMonitorGames({ + setGames, + triggerUpdate: true, + }); + + // Combine the loading states + const isLoading = isFetchingMonitor || isFetchingGames; + const [createMonitor, isCreating] = useCreateMonitor(); const [pauseMonitor, isPausing] = usePauseMonitor({}); const [deleteMonitor, isDeleting] = useDeleteMonitor(); @@ -113,6 +125,12 @@ const UptimeCreate = ({ isClone = false }) => { { _id: 4, name: t("time.fourMinutes") }, { _id: 5, name: t("time.fiveMinutes") }, ]; + + const GAMELIST = Object.entries(games).map(([key, value]) => ({ + _id: key, + name: value.name, + })); + const CRUMBS = [ { name: "uptime", path: "/uptime" }, ...(isCreate @@ -153,6 +171,11 @@ const UptimeCreate = ({ isClone = false }) => { placeholder: t("monitorType.port.placeholder"), namePlaceholder: t("monitorType.port.namePlaceholder"), }, + game: { + label: t("monitorType.game.label"), + placeholder: t("monitorType.game.placeholder"), + namePlaceholder: t("monitorType.game.namePlaceholder"), + }, }; // Handlers @@ -169,12 +192,14 @@ const UptimeCreate = ({ isClone = false }) => { : monitor.url, name: monitor.name || monitor.url.substring(0, 50), type: monitor.type, - port: monitor.type === "port" ? monitor.port : undefined, + port: + monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, interval: monitor.interval, matchMethod: monitor.matchMethod, expectedValue: monitor.expectedValue, jsonPath: monitor.jsonPath, ignoreTlsErrors: monitor.ignoreTlsErrors, + gameId: monitor.gameId || undefined, }; } else { form = { @@ -188,8 +213,10 @@ const UptimeCreate = ({ isClone = false }) => { interval: monitor.interval, teamId: monitor.teamId, userId: monitor.userId, - port: monitor.type === "port" ? monitor.port : undefined, + port: + monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, ignoreTlsErrors: monitor.ignoreTlsErrors, + gameId: monitor.gameId || undefined, }; } if (!useAdvancedMatching) { @@ -243,6 +270,11 @@ const UptimeCreate = ({ isClone = false }) => { setMonitor((prev) => ({ ...prev, [name]: value })); + if (name === "type") { + setErrors({}); + return; + } + const { error } = monitorValidation.validate( { type: monitor.type, [name]: value }, { abortEarly: false } @@ -250,7 +282,9 @@ const UptimeCreate = ({ isClone = false }) => { setErrors((prev) => ({ ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), + ...(error && error.details[0].path[0] === name + ? { [name]: error.details[0].message } + : { [name]: undefined }), })); }; @@ -486,6 +520,15 @@ const UptimeCreate = ({ isClone = false }) => { checked={monitor.type === "port"} onChange={onChange} /> + {errors["type"] ? ( { onChange={onChange} error={errors["port"] ? true : false} helperText={errors["port"]} - hidden={monitor.type !== "port"} + hidden={monitor.type !== "port" && monitor.type !== "game"} /> + {monitor.type === "game" && ( +