diff --git a/README.md b/README.md index 757e70493..4be16fef0 100644 --- a/README.md +++ b/README.md @@ -72,14 +72,20 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb ## 💚 Questions & Ideas -If you have any questions, suggestions or comments, please use our [Discord channel](https://discord.gg/NAb6H3UTjK). We've also launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you! +If you have any questions, suggestions or comments, you have several options: + +- [Discord channel](https://discord.gg/NAb6H3UTjK) +- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) +- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/) + +Feel free to ask questions or share your ideas - we'd love to hear from you! ## 🧩 Features - Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5) - Website monitoring - Page speed monitoring -- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture) +- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent - Docker monitoring - Ping monitoring - SSL monitoring @@ -134,7 +140,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers. -We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 6000+ stars and attracted 80+ contributors from around the globe. +We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 7000+ stars and attracted 90+ contributors from around the globe. Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us! diff --git a/charts/helm/checkmate/templates/secrets.yaml b/charts/helm/checkmate/templates/secrets.yaml index 42e7c6047..0a3f1ace5 100644 --- a/charts/helm/checkmate/templates/secrets.yaml +++ b/charts/helm/checkmate/templates/secrets.yaml @@ -14,6 +14,6 @@ metadata: name: checkmate-secrets type: Opaque stringData: -{{- range $key, $value := := $secrets }} +{{- range $key, $value := $secrets }} {{ $key }}: {{ $value | quote }} {{- end }} \ No newline at end of file diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/Charts/CustomGauge/index.jsx index 3c7e13610..d9718d72c 100644 --- a/client/src/Components/Charts/CustomGauge/index.jsx +++ b/client/src/Components/Charts/CustomGauge/index.jsx @@ -1,8 +1,10 @@ import { useTheme } from "@emotion/react"; import { useEffect, useState, useMemo } from "react"; +import Stack from "@mui/material/Stack"; import { Box, Typography } from "@mui/material"; import PropTypes from "prop-types"; import "./index.css"; +import CircularProgress from "@mui/material/CircularProgress"; const MINIMUM_VALUE = 0; const MAXIMUM_VALUE = 100; @@ -16,6 +18,8 @@ const MAXIMUM_VALUE = 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 * * @example * { +const CustomGauge = ({ + isLoading = false, + progress = 0, + radius = 70, + strokeWidth = 15, + threshold = 50, + precision = 1, + unit = "%", +}) => { const theme = useTheme(); // Calculate the length of the stroke for the circle const { circumference, totalSize, strokeLength } = useMemo( @@ -54,8 +66,22 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = const fillColor = progressWithinRange > threshold - ? theme.palette.error.lowContrast // CAIO_REVIEW - : theme.palette.accent.main; // CAIO_REVIEW + ? theme.palette.error.lowContrast + : theme.palette.accent.main; + + if (isLoading) { + return ( + + + + ); + } return ( - {`${progressWithinRange.toFixed(1)}%`} + {`${progressWithinRange.toFixed(precision)}${unit}`} ); @@ -112,8 +138,11 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = export default CustomGauge; CustomGauge.propTypes = { + isLoading: PropTypes.bool, progress: PropTypes.number, radius: PropTypes.number, strokeWidth: PropTypes.number, threshold: PropTypes.number, + precision: PropTypes.number, + unit: PropTypes.string, }; diff --git a/client/src/Components/ConfigBox/index.jsx b/client/src/Components/ConfigBox/index.jsx index 44ae38472..e26e49e8d 100644 --- a/client/src/Components/ConfigBox/index.jsx +++ b/client/src/Components/ConfigBox/index.jsx @@ -4,15 +4,14 @@ const ConfigBox = styled(Stack)(({ theme }) => ({ display: "flex", flexDirection: "row", justifyContent: "space-between", - gap: theme.spacing(20), backgroundColor: theme.palette.primary.main, border: 1, borderStyle: "solid", borderColor: theme.palette.primary.lowContrast, borderRadius: theme.spacing(2), "& > *": { - paddingTop: theme.spacing(12), - paddingBottom: theme.spacing(18), + paddingTop: theme.spacing(15), + paddingBottom: theme.spacing(15), }, "& > div:first-of-type": { flex: 0.7, @@ -25,7 +24,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({ "& > div:last-of-type": { flex: 1, paddingRight: theme.spacing(20), - paddingLeft: theme.spacing(18), + paddingLeft: theme.spacing(20), }, "& h1, & h2": { color: theme.palette.primary.contrastTextSecondary, diff --git a/client/src/Components/Inputs/FieldWrapper/index.jsx b/client/src/Components/Inputs/FieldWrapper/index.jsx new file mode 100644 index 000000000..cc5061af8 --- /dev/null +++ b/client/src/Components/Inputs/FieldWrapper/index.jsx @@ -0,0 +1,51 @@ +import { Stack, Typography } from "@mui/material"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; + +const DEFAULT_GAP = 6; +const FieldWrapper = ({ + label, + children, + gap, + labelMb, + labelFontWeight = 500, + labelVariant = "h3", + labelSx = {}, + sx = {}, +}) => { + const theme = useTheme(); + return ( + + {label && ( + + {label} + + )} + {children} + + ); +}; + +FieldWrapper.propTypes = { + label: PropTypes.node, + children: PropTypes.node.isRequired, + gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]), + labelMb: PropTypes.number, + labelFontWeight: PropTypes.number, + labelVariant: PropTypes.string, + labelSx: PropTypes.object, + sx: PropTypes.object, +}; + +export default FieldWrapper; diff --git a/client/src/Components/Inputs/Radio/index.jsx b/client/src/Components/Inputs/Radio/index.jsx index 646b38e93..4c687de1e 100644 --- a/client/src/Components/Inputs/Radio/index.jsx +++ b/client/src/Components/Inputs/Radio/index.jsx @@ -24,7 +24,17 @@ import "./index.css"; * @returns {JSX.Element} - The rendered Radio component. */ -const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => { +const Radio = ({ + name, + checked, + value, + id, + size, + title, + desc, + onChange, + labelSpacing, +}) => { const theme = useTheme(); return ( @@ -53,7 +63,14 @@ const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => { onChange={onChange} label={ <> - {title} + + {title} + { ); }; -//TODO keep search state inside of component +//TODO keep search state inside of component. const Search = ({ label, id, @@ -68,6 +70,13 @@ const Search = ({ startAdornment, endAdornment, onBlur, + //FieldWrapper's props + gap, + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + unit = "option", }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -139,15 +148,17 @@ const Search = ({ getOptionLabel={(option) => option[filteredBy]} isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier renderInput={(params) => ( - - - {label} - + )} - + )} filterOptions={(options, { inputValue }) => { if (inputValue.trim() === "" && multiple && isAdorned) { @@ -186,7 +197,12 @@ const Search = ({ ); if (filtered.length === 0) { - return [{ [filteredBy]: "No monitors found", noOptions: true }]; + return [ + { + [filteredBy]: t("general.noOptionsFound", { unit: unit }), + noOptions: true, + }, + ]; } return filtered; }} @@ -281,7 +297,7 @@ Search.propTypes = { options: PropTypes.array.isRequired, filteredBy: PropTypes.string.isRequired, secondaryLabel: PropTypes.string, - value: PropTypes.array, + value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), inputValue: PropTypes.string.isRequired, handleInputChange: PropTypes.func.isRequired, handleChange: PropTypes.func, @@ -292,6 +308,7 @@ Search.propTypes = { startAdornment: PropTypes.object, endAdornment: PropTypes.object, onBlur: PropTypes.func, + unit: PropTypes.string, }; export default Search; diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/Inputs/Select/index.jsx index 679a5b3b5..a438b6bb2 100644 --- a/client/src/Components/Inputs/Select/index.jsx +++ b/client/src/Components/Inputs/Select/index.jsx @@ -2,6 +2,7 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import FieldWrapper from "../FieldWrapper"; import "./index.css"; @@ -50,8 +51,14 @@ const Select = ({ onBlur, sx, name = "", - labelControlSpacing = 2, + labelControlSpacing = 6, maxWidth, + //FieldWrapper's props + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + fieldWrapperSx = {}, }) => { const theme = useTheme(); const itemStyles = { @@ -69,20 +76,17 @@ const Select = ({ }; return ( - - {label && ( - - {label} - - )} { @@ -151,7 +162,7 @@ const Select = ({ ))} - + ); }; diff --git a/client/src/Components/Inputs/TextInput/index.jsx b/client/src/Components/Inputs/TextInput/index.jsx index bb2df5d21..469791704 100644 --- a/client/src/Components/Inputs/TextInput/index.jsx +++ b/client/src/Components/Inputs/TextInput/index.jsx @@ -2,6 +2,7 @@ import { Stack, TextField, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { forwardRef, useState, cloneElement } from "react"; import PropTypes from "prop-types"; +import FieldWrapper from "../FieldWrapper"; const getSx = (theme, type, maxWidth) => { const sx = { @@ -85,31 +86,43 @@ const TextInput = forwardRef( marginLeft, disabled = false, hidden = false, + //FieldWrapper's props + gap, + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + sx = {}, }, ref ) => { const [fieldType, setFieldType] = useState(type); const theme = useTheme(); + const labelContent = label && ( + <> + {label} + {isRequired && } + {isOptional && } + + ); return ( - + ); } ); diff --git a/client/src/Components/NotificationConfig/index.jsx b/client/src/Components/NotificationConfig/index.jsx index d3707eae4..c37889eff 100644 --- a/client/src/Components/NotificationConfig/index.jsx +++ b/client/src/Components/NotificationConfig/index.jsx @@ -10,7 +10,18 @@ import { useState, useEffect } from "react"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => { +const NotificationConfig = ({ + notifications, + setMonitor, + setNotifications, + //FieldWrapper's props + gap, + labelMb, + labelFontWeight, + labelVariant, + labelSx = {}, + sx = {}, +}) => { // Local state const [notificationsSearch, setNotificationsSearch] = useState(""); const [selectedNotifications, setSelectedNotifications] = useState([]); @@ -66,6 +77,14 @@ const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => handleChange={(value) => { handleSearch(value); }} + labelMb={labelMb} + labelVariant={labelVariant} + labelFontWeight={labelFontWeight} + labelSx={labelSx} + gap={gap} + sx={{ + ...sx, + }} /> { + const theme = useTheme(); + + if (Array.isArray(body)) { + return ( + + {body.map((item, idx) => ( + + {item} + + ))} + + ); + } else if (typeof body === "string") { + return {body}; + } + + return null; +}; + +ToastBody.propTypes = { + body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), +}; + +export default ToastBody; diff --git a/client/src/Components/Toast/index.jsx b/client/src/Components/Toast/index.jsx new file mode 100644 index 000000000..36df0e631 --- /dev/null +++ b/client/src/Components/Toast/index.jsx @@ -0,0 +1,96 @@ +import Stack from "@mui/material/Stack"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import ToastBody from "./body"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; +import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; +import CloseIcon from "@mui/icons-material/Close"; + +// Utils +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +const icons = { + info: , + error: , + warning: , +}; + +const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => { + const theme = useTheme(); + const icon = icons[variant]; + + return ( + + + {hasIcon && icon} + {title && ( + + {title} + + )} + {title && ( + + + + )} + + + + + {!title && ( + + + + )} + + {hasDismiss && ( + + )} + + ); +}; + +export default Toast; + +Toast.propTypes = { + variant: PropTypes.string.isRequired, + title: PropTypes.string, + body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + hasDismiss: PropTypes.bool, + hasIcon: PropTypes.bool, + onClick: PropTypes.func, +}; diff --git a/client/src/Hooks/logHooks.js b/client/src/Hooks/logHooks.js index 000e4d0f0..b954efb4b 100644 --- a/client/src/Hooks/logHooks.js +++ b/client/src/Hooks/logHooks.js @@ -32,4 +32,83 @@ const useFetchLogs = () => { return [logs, isLoading, error]; }; -export { useFetchLogs }; +const useFetchQueueData = (trigger) => { + const [jobs, setJobs] = useState(undefined); + const [metrics, setMetrics] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + useEffect(() => { + const fetchJobs = async () => { + try { + setIsLoading(true); + const response = await networkService.getQueueData(); + if (response.status === 200) { + setJobs(response.data.data.jobs); + setMetrics(response.data.data.metrics); + } + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + + fetchJobs(); + }, [trigger]); + + return [jobs, metrics, isLoading, error]; +}; + +const useFlushQueue = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + const flushQueue = async (trigger, setTrigger) => { + try { + setIsLoading(true); + await networkService.flushQueue(); + createToast({ + body: "Queue flushed", + }); + } catch (error) { + setError(error); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + setTrigger(!trigger); + } + }; + return [flushQueue, isLoading, error]; +}; + +const useFetchDiagnostics = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const [diagnostics, setDiagnostics] = useState(undefined); + + const fetchDiagnostics = async () => { + try { + setIsLoading(true); + const response = await networkService.getDiagnostics(); + setDiagnostics(response.data.data); + } catch (error) { + createToast({ + body: error.message, + }); + setError(error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchDiagnostics(); + }, []); + + return [diagnostics, fetchDiagnostics, isLoading, error]; +}; + +export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics }; diff --git a/client/src/Hooks/queueHooks.js b/client/src/Hooks/queueHooks.js deleted file mode 100644 index f7a25b3d3..000000000 --- a/client/src/Hooks/queueHooks.js +++ /dev/null @@ -1,57 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; - -const useFetchQueueData = (trigger) => { - const [jobs, setJobs] = useState(undefined); - const [metrics, setMetrics] = useState(undefined); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - useEffect(() => { - const fetchJobs = async () => { - try { - setIsLoading(true); - const response = await networkService.getQueueData(); - if (response.status === 200) { - setJobs(response.data.data.jobs); - setMetrics(response.data.data.metrics); - } - } catch (error) { - setError(error); - } finally { - setIsLoading(false); - } - }; - - fetchJobs(); - }, [trigger]); - - return [jobs, metrics, isLoading, error]; -}; - -const useFlushQueue = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - const flushQueue = async (trigger, setTrigger) => { - try { - setIsLoading(true); - await networkService.flushQueue(); - createToast({ - body: "Queue flushed", - }); - } catch (error) { - setError(error); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - setTrigger(!trigger); - } - }; - return [flushQueue, isLoading, error]; -}; - -export { useFetchQueueData, useFlushQueue }; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx index d91e899c5..865698f44 100644 --- a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx +++ b/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx @@ -62,14 +62,15 @@ export const CustomThreshold = ({ const theme = useTheme(); return ( - + { /> - + { disabled={!isCreate} /> {isCreate && ( - - {t("infrastructureProtocol")} + - + )} { setPage(0); }; + useEffect(() => { + if (isSearching) { + setPage(0); + } + }, [isSearching]); + const handleReset = () => { setSelectedStatus(undefined); setToFilterStatus(undefined); diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx new file mode 100644 index 000000000..e9f235112 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -0,0 +1,109 @@ +import Stack from "@mui/material/Stack"; +import Gauge 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 { useTranslation } from "react-i18next"; + +const GaugeBox = ({ title, subtitle, children }) => { + const theme = useTheme(); + return ( + + {children} + + {title} + {subtitle} + + ); +}; + +GaugeBox.propTypes = { + title: PropTypes.string.isRequired, + subtitle: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +const Gauges = ({ diagnostics, isLoading }) => { + const heapTotalSize = getPercentage( + diagnostics?.v8HeapStats?.totalHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes + ); + + const heapUsedSize = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes + ); + + const actualHeapUsed = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.totalHeapSizeBytes + ); + + const theme = useTheme(); + const { t } = useTranslation(); + + return ( + + + + + + + + + + + + + + + ); +}; + +Gauges.propTypes = { + diagnostics: PropTypes.object, + isLoading: PropTypes.bool, +}; + +export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx new file mode 100644 index 000000000..103d1c698 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx @@ -0,0 +1,97 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import CircularProgress from "@mui/material/CircularProgress"; + +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; +import { formatBytes } from "../../utils/utils"; +import { useTranslation } from "react-i18next"; + +const StatsCard = ({ title, value, unit = "", isLoading }) => { + const theme = useTheme(); + return ( + + {isLoading ? ( + + + + ) : ( + + + {title} + + + {value} {unit} + + + )} + + ); +}; + +StatsCard.propTypes = { + title: PropTypes.string, + value: PropTypes.string, + unit: PropTypes.string, + isLoading: PropTypes.bool, +}; + +const Stats = ({ diagnostics, isLoading }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + + + + + + + + + ); +}; + +Stats.propTypes = { + diagnostics: PropTypes.object, + isLoading: PropTypes.bool, +}; + +export default Stats; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx new file mode 100644 index 000000000..7c5d1f5a9 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -0,0 +1,54 @@ +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 { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useFetchDiagnostics } from "../../../Hooks/logHooks"; + +const Diagnostics = () => { + // Local state + + // Hooks + const theme = useTheme(); + const { t } = useTranslation(); + const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics(); + // Setup + return ( + + + {t("diagnosticsPage.diagnosticDescription")} + + + + + + + + + + + ); +}; + +export default Diagnostics; diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/Logs/Diagnostics/utils/utils.js new file mode 100644 index 000000000..a3b75631f --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/utils/utils.js @@ -0,0 +1,16 @@ +export const getPercentage = (value, total) => { + if (!value || !total) return 0; + return (value / total) * 100; +}; + +export const formatBytes = (bytes) => { + if (!bytes) return "N/A"; + + if (bytes === 0) return "0 Bytes"; + + if (bytes >= 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + } + + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +}; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index 6c5e1a301..b65f9c0fd 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Select from "../../../Components/Inputs/Select"; import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; import { useFetchLogs } from "../../../Hooks/logHooks"; import { useTheme } from "@emotion/react"; @@ -51,12 +52,15 @@ const Logs = () => { return ( - {t("logsPage.description")} + {t("logsPage.description")} + + {t("logsPage.logLevelSelect.title")} setRawInput(val)} + handleChange={handleTimezoneChange} + isAdorned={false} + 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 a1ba117b0..4c2842b71 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -4,7 +4,7 @@ import { TabPanel } from "@mui/lab"; import ConfigBox from "../../../../../Components/ConfigBox"; import Checkbox from "../../../../../Components/Inputs/Checkbox"; import TextInput from "../../../../../Components/Inputs/TextInput"; -import Select from "../../../../../Components/Inputs/Select"; +import Search from "../../../../../Components/Inputs/Search"; import ImageUpload from "../../../../../Components/Inputs/ImageUpload"; import ColorPicker from "../../../../../Components/Inputs/ColorPicker"; import Progress from "../Progress"; @@ -14,6 +14,7 @@ import { useTheme } from "@emotion/react"; import timezones from "../../../../../Utils/timezones.json"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; +import { useMemo, useState, useCallback } from "react"; const TabSettings = ({ isCreate, @@ -28,6 +29,25 @@ const TabSettings = ({ // Utils const theme = useTheme(); const { t } = useTranslation(); + const [rawInput, setRawInput] = useState(""); + + const selectedTimezone = useMemo( + () => timezones.find((tz) => tz._id === form.timezone) ?? null, + [form.timezone] + ); + + const handleTimezoneChange = useCallback( + (newValue) => { + setRawInput(""); + handleFormChange({ + target: { + name: "timezone", + value: newValue?._id ?? "", + }, + }); + }, + [handleFormChange] + ); return ( @@ -101,13 +121,17 @@ const TabSettings = ({ -