Merge remote-tracking branch 'origin' into Network-tab-implementation-FE
This commit is contained in:
commit
65f437f08e
48 files changed
+1354
-509
No files matched your search
@@ -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,
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ListItem,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Stack,
|
||||
Typography,
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
@@ -33,7 +32,6 @@ const SearchAdornment = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
mr={theme.spacing(4)}
|
||||
height={16}
|
||||
sx={{
|
||||
"& svg": {
|
||||
@@ -284,7 +282,9 @@ const Search = ({
|
||||
}}
|
||||
sx={{
|
||||
/* height: 34,*/
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
||||
padding: `0 ${theme.spacing(5)}`,
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = () => {
|
||||
<MenuItem
|
||||
key={lang}
|
||||
value={lang}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
|
||||
@@ -4,7 +4,6 @@ import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Avatar from "../../Avatar";
|
||||
import ThemeSwitch from "../../ThemeSwitch";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Divider from "@mui/material/Divider";
|
||||
@@ -152,38 +151,30 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
||||
{getRoleDisplayText(authState.user, t)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
columnGap={theme.spacing(2)}
|
||||
<Tooltip
|
||||
title={t("navControls")}
|
||||
disableInteractive
|
||||
>
|
||||
<ThemeSwitch color={theme.palette.primary.contrastTextTertiary} />
|
||||
<Tooltip
|
||||
title={t("navControls")}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
ml: "auto",
|
||||
mr: "-8px",
|
||||
"&:focus": { outline: "none" },
|
||||
alignSelf: "center",
|
||||
padding: "10px",
|
||||
<IconButton
|
||||
sx={{
|
||||
ml: "50px",
|
||||
"&:focus": { outline: "none" },
|
||||
alignSelf: "center",
|
||||
|
||||
"& svg": {
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
},
|
||||
"& svg path": {
|
||||
/* Vertical three dots */
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={(event) => openPopup(event)}
|
||||
>
|
||||
<DotsVertical />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
"& svg": {
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
},
|
||||
"& svg path": {
|
||||
/* Vertical three dots */
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={(event) => openPopup(event)}
|
||||
>
|
||||
<DotsVertical />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Menu
|
||||
className="sidebar-popup"
|
||||
|
||||
@@ -49,9 +49,9 @@ const Logo = ({ collapsed }) => {
|
||||
<Typography
|
||||
lineHeight={1}
|
||||
mt={theme.spacing(2)}
|
||||
color={theme.palette.accent.contrastText}
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontSize={"var(--env-var-font-size-medium-plus)"}
|
||||
sx={{ opacity: 0.8, fontWeight: 500 }}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
minHeight="100vh"
|
||||
position="relative"
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{ overflow: "hidden" }}
|
||||
<AuthPageWrapper
|
||||
welcome={t("auth.login.welcome")}
|
||||
heading={t("auth.login.heading")}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "0%",
|
||||
transform: "translate(-40%, -40%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
transform: "translate(45%, 55%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<AuthHeader hideLogo={true} />
|
||||
|
||||
<Stack
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
component="form"
|
||||
width="100%"
|
||||
padding={theme.spacing(8)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={onSubmit}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(8),
|
||||
boxShadow: theme.palette.tertiary.cardShadow,
|
||||
margin: "auto",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(10),
|
||||
padding: theme.spacing(20),
|
||||
zIndex: 1,
|
||||
position: "relative",
|
||||
width: {
|
||||
sm: "60%",
|
||||
md: "50%",
|
||||
lg: "40%",
|
||||
xl: "30%",
|
||||
sm: "80%",
|
||||
md: "70%",
|
||||
lg: "65%",
|
||||
xl: "65%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
mb={theme.spacing(10)}
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 60, sm: 80, md: 90 },
|
||||
}}
|
||||
/>
|
||||
<Logo style={{ width: "100%", height: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
mb={theme.spacing(12)}
|
||||
textAlign="center"
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
mb={theme.spacing(2)}
|
||||
>
|
||||
{t("auth.login.welcome")}
|
||||
</Typography>
|
||||
<Typography variant="h1">{t("auth.login.heading")}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
component="form"
|
||||
width="100%"
|
||||
padding={theme.spacing(8)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={onSubmit}
|
||||
sx={{
|
||||
width: {
|
||||
sm: "80%",
|
||||
md: "70%",
|
||||
lg: "65%",
|
||||
xl: "65%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password ? true : false}
|
||||
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
<TextLink
|
||||
text={t("auth.login.links.forgotPassword")}
|
||||
linkText={t("auth.login.links.forgotPasswordLink")}
|
||||
href="/forgot-password"
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<TextLink
|
||||
text={t("auth.login.links.register")}
|
||||
linkText={t("auth.login.links.registerLink")}
|
||||
href="/register"
|
||||
<TextInput
|
||||
type="password"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password ? true : false}
|
||||
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<TextLink
|
||||
text={t("auth.login.links.forgotPassword")}
|
||||
linkText={t("auth.login.links.forgotPasswordLink")}
|
||||
href="/forgot-password"
|
||||
/>
|
||||
<TextLink
|
||||
text={t("auth.login.links.register")}
|
||||
linkText={t("auth.login.links.registerLink")}
|
||||
href="/register"
|
||||
/>
|
||||
</AuthPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
minHeight="100vh"
|
||||
<AuthPageWrapper
|
||||
heading={t("auth.registration.heading.user")}
|
||||
welcome={t("auth.registration.welcome")}
|
||||
>
|
||||
<AuthHeader />
|
||||
<Stack
|
||||
margin="auto"
|
||||
component="form"
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(10)}
|
||||
padding={theme.spacing(8)}
|
||||
gap={theme.spacing(8)}
|
||||
onSubmit={onSubmit}
|
||||
sx={{
|
||||
width: {
|
||||
sm: "80%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h1">{t("auth.registration.heading.user")}</Typography>
|
||||
|
||||
<Typography variant="h2">
|
||||
{superAdminExists
|
||||
? t("auth.registration.description.user")
|
||||
: t("auth.registration.description.superAdmin")}
|
||||
</Typography>
|
||||
<Stack
|
||||
component="form"
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
alignSelf="center"
|
||||
justifyContent="center"
|
||||
border={1}
|
||||
borderRadius={theme.spacing(5)}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
padding={theme.spacing(12)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={onSubmit}
|
||||
direction={{ xs: "column", lg: "row" }}
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Typography variant="h1">
|
||||
{superAdminExists
|
||||
? t("auth.registration.heading.user")
|
||||
: t("auth.registration.heading.superAdmin")}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{superAdminExists
|
||||
? t("auth.registration.description.user")
|
||||
: t("auth.registration.description.superAdmin")}
|
||||
</Typography>
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (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
|
||||
/>
|
||||
<TextInput
|
||||
name="firstName"
|
||||
sx={{ flex: 1 }}
|
||||
label={t("auth.common.inputs.firstName.label")}
|
||||
width="100%"
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.firstName.placeholder")}
|
||||
autoComplete="given-name"
|
||||
@@ -259,6 +239,9 @@ const Register = ({ superAdminExists }) => {
|
||||
<TextInput
|
||||
name="lastName"
|
||||
label={t("auth.common.inputs.lastName.label")}
|
||||
sx={{ flex: 1 }}
|
||||
width="100%"
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.lastName.placeholder")}
|
||||
autoComplete="family-name"
|
||||
@@ -267,82 +250,76 @@ const Register = ({ superAdminExists }) => {
|
||||
error={errors.lastName ? true : false}
|
||||
helperText={errors.lastName ? t(errors.lastName) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onPasswordChange}
|
||||
error={errors.password && errors.password[0] ? true : false}
|
||||
helperText={
|
||||
errors.password === "auth.common.inputs.password.errors.empty"
|
||||
? t(errors.password)
|
||||
: ""
|
||||
} // Other errors are related to required password conditions and are visualized below the input
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
name="confirm"
|
||||
label={t("auth.common.inputs.passwordConfirm.label")}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onPasswordChange}
|
||||
error={errors.confirm && errors.confirm[0] ? true : false}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.length.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.length.highlighted")}
|
||||
variant={feedback.length}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.special.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.special.highlighted")}
|
||||
variant={feedback.special}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.number.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.number.highlighted")}
|
||||
variant={feedback.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.uppercase.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.uppercase.highlighted")}
|
||||
variant={feedback.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.lowercase.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.lowercase.highlighted")}
|
||||
variant={feedback.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.match.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.match.highlighted")}
|
||||
variant={feedback.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "30%", alignSelf: "flex-end" }}
|
||||
>
|
||||
{t("auth.common.navigation.continue")}
|
||||
</Button>
|
||||
</Stack>
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
gap={theme.spacing(4)}
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (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
|
||||
/>
|
||||
<PasswordTooltip
|
||||
feedback={feedback}
|
||||
form={form}
|
||||
>
|
||||
<Box>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onPasswordChange}
|
||||
error={errors.password && errors.password[0] ? true : false}
|
||||
helperText={
|
||||
errors.password === "auth.common.inputs.password.errors.empty"
|
||||
? t(errors.password)
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</PasswordTooltip>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
name="confirm"
|
||||
label={t("auth.common.inputs.passwordConfirm.label")}
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onPasswordChange}
|
||||
marginBottom={theme.spacing(4)}
|
||||
error={errors.confirm && errors.confirm[0] ? true : false}
|
||||
/>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
fontWeight: 700,
|
||||
mt: theme.spacing(10),
|
||||
}}
|
||||
>
|
||||
{t("auth.common.navigation.continue")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</AuthPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
minHeight="100vh"
|
||||
position="relative"
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{ overflow: "hidden" }}
|
||||
>
|
||||
<AuthHeader hideLogo={true} />
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "0%",
|
||||
transform: "translate(-40%, -40%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
transform: "translate(45%, 55%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(8),
|
||||
boxShadow: theme.palette.tertiary.cardShadow,
|
||||
margin: "auto",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(10),
|
||||
padding: theme.spacing(20),
|
||||
zIndex: 1,
|
||||
position: "relative",
|
||||
width: {
|
||||
sm: "60%",
|
||||
md: "50%",
|
||||
lg: "40%",
|
||||
xl: "30%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
mb={theme.spacing(10)}
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 60, sm: 70, md: 80 },
|
||||
}}
|
||||
/>
|
||||
<Logo style={{ width: "100%", height: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
mb={theme.spacing(4)}
|
||||
textAlign="center"
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
mb={theme.spacing(2)}
|
||||
>
|
||||
{welcome}
|
||||
</Typography>
|
||||
<Typography variant="h1">{heading}</Typography>
|
||||
</Stack>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthPageWrapper;
|
||||
|
||||
AuthPageWrapper.propTypes = {
|
||||
children: PropTypes.node,
|
||||
heading: PropTypes.node,
|
||||
welcome: PropTypes.node,
|
||||
};
|
||||
@@ -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 (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
arrow
|
||||
open={showPasswordTooltip}
|
||||
title={
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.length.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.length.highlighted")
|
||||
}
|
||||
variant={feedback.length}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.special.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.special.highlighted")
|
||||
}
|
||||
variant={feedback.special}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.number.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.number.highlighted")
|
||||
}
|
||||
variant={feedback.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.uppercase.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.uppercase.highlighted")
|
||||
}
|
||||
variant={feedback.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.lowercase.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.lowercase.highlighted")
|
||||
}
|
||||
variant={feedback.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.match.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.match.highlighted")
|
||||
}
|
||||
variant={feedback.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
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}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
p={theme.spacing(2)}
|
||||
maxWidth={150}
|
||||
width={150}
|
||||
>
|
||||
{children}
|
||||
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
<Typography variant="body2">{subtitle}</Typography>
|
||||
</Stack>
|
||||
const BaseContainer = ({children}) => {
|
||||
const theme = useTheme()
|
||||
return(
|
||||
<Box
|
||||
sx={{
|
||||
padding: theme.spacing(3),
|
||||
borderRadius: theme.spacing(2),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
minWidth: 250,
|
||||
width: "fit-content",
|
||||
}}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
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 }) => (
|
||||
<Stack
|
||||
justifyContent="space-between"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{label}</Typography>
|
||||
<Typography sx={{
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
width: "40%",
|
||||
mb: theme.spacing(2),
|
||||
mt: theme.spacing(2),
|
||||
pr: theme.spacing(2),
|
||||
textAlign: "right",
|
||||
}}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return(
|
||||
<BaseContainer>
|
||||
<Stack direction="column" gap={theme.spacing(2)} alignItems="center">
|
||||
<Box
|
||||
sx = {{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<CustomGauge progress={value} radius={100}/>
|
||||
<Typography component="h2" sx={{fontWeight: 600}}>
|
||||
{heading}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ width:"100%", borderTop:`1px solid ${theme.palette.divider}`}}>
|
||||
<MetricRow label={metricOne} value={valueOne} />
|
||||
{metricTwo && valueTwo && (
|
||||
<MetricRow label={metricTwo} value={valueTwo} />
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</BaseContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const Gauges = ({ diagnostics, isLoading }) => {
|
||||
@@ -53,50 +99,41 @@ const Gauges = ({ diagnostics, isLoading }) => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(4)}
|
||||
spacing={theme.spacing(8)}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapAllocationTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapAllocationSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={heapTotalSize}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapUsageTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapUsageSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={heapUsedSize}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapUtilizationTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapUtilizationSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={actualHeapUsed}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.instantCpuUsageTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.instantCpuUsageSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={diagnostics?.cpuUsage?.usagePercentage}
|
||||
precision={2}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<InfrastructureStyleGauge
|
||||
value={heapTotalSize}
|
||||
heading={t("diagnosticsPage.gauges.heapAllocationTitle")}
|
||||
metricOne={t("diagnosticsPage.gauges.heapAllocationSubtitle")}
|
||||
valueOne={`${heapTotalSize?.toFixed(1) || 0}%`}
|
||||
metricTwo={t("total")}
|
||||
valueTwo={formatBytes(diagnostics?.v8HeapStats?.heapSizeLimitBytes)}
|
||||
/>
|
||||
<InfrastructureStyleGauge
|
||||
value={heapUsedSize}
|
||||
heading={t("diagnosticsPage.gauges.heapUsageTitle")}
|
||||
metricOne={t("diagnosticsPage.gauges.heapUsageSubtitle")}
|
||||
valueOne={`${heapUsedSize?.toFixed(1) || 0}%`}
|
||||
metricTwo={t("used")}
|
||||
valueTwo={formatBytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
|
||||
/>
|
||||
<InfrastructureStyleGauge
|
||||
value={actualHeapUsed}
|
||||
heading={t("diagnosticsPage.gauges.heapUtilizationTitle")}
|
||||
metricOne={t("diagnosticsPage.gauges.heapUtilizationSubtitle")}
|
||||
valueOne={`${actualHeapUsed?.toFixed(1) || 0}%`}
|
||||
metricTwo={t("total")}
|
||||
valueTwo={formatBytes(diagnostics?.v8HeapStats?.totalHeapSizeBytes)}
|
||||
/>
|
||||
<InfrastructureStyleGauge
|
||||
value={diagnostics?.cpuUsage?.usagePercentage}
|
||||
heading={t("diagnosticsPage.gauges.instantCpuUsageTitle")}
|
||||
metricOne={t("diagnosticsPage.gauges.instantCpuUsageSubtitle")}
|
||||
valueOne={`${diagnostics?.cpuUsage?.usagePercentage?.toFixed(1) || 0}%`}
|
||||
metricTwo=""
|
||||
valueTwo=""
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<StatusBoxes flexWrap="wrap">
|
||||
<StatBox
|
||||
gradient={true}
|
||||
status="up"
|
||||
heading={t("status")}
|
||||
subHeading={
|
||||
error
|
||||
? t("logsPage.logLevelSelect.values.error")
|
||||
: isLoading
|
||||
? t("commonSaving")
|
||||
: diagnostics
|
||||
? t("diagnosticsPage.diagnosticDescription")
|
||||
: t("general.noOptionsFound", { unit: "data" })
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.eventLoopDelayTitle")}
|
||||
subHeading={getHumanReadableDuration(diagnostics?.eventLoopDelayMs)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.uptimeTitle")}
|
||||
subHeading={getHumanReadableDuration(diagnostics?.uptimeMs)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.usedHeapSizeTitle")}
|
||||
subHeading={formatBytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.totalHeapSizeTitle")}
|
||||
subHeading={formatBytes(diagnostics?.v8HeapStats?.totalHeapSizeBytes)}
|
||||
/>
|
||||
<StatBox
|
||||
heading={t("diagnosticsPage.stats.osMemoryLimitTitle")}
|
||||
subHeading={formatBytes(diagnostics?.osStats?.totalMemoryBytes)}
|
||||
/>
|
||||
</StatusBoxes>
|
||||
<Gauges
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Box>
|
||||
<Typography variant="h2">{t("diagnosticsPage.diagnosticDescription")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={fetchDiagnostics}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("queuePage.refreshButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
<Divider color={theme.palette.accent.main} />
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Gauges
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Stats
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={fetchDiagnostics}
|
||||
loading={isLoading}
|
||||
>
|
||||
Fetch Diagnostics
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
|
||||
inputValue={rawInput}
|
||||
handleInputChange={(val) => setRawInput(val)}
|
||||
handleChange={handleTimezoneChange}
|
||||
isAdorned={false}
|
||||
isAdorned={true}
|
||||
unit="timezone"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -130,7 +130,7 @@ const TabSettings = ({
|
||||
inputValue={rawInput}
|
||||
handleInputChange={(newVal) => setRawInput(newVal)}
|
||||
handleChange={handleTimezoneChange}
|
||||
isAdorned={false}
|
||||
isAdorned={true}
|
||||
unit="timezone"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<Radio
|
||||
name="type"
|
||||
title={t("gameServerMonitoring")}
|
||||
desc={t("gameServerMonitoringDescription")}
|
||||
size="small"
|
||||
value="game"
|
||||
checked={monitor.type === "game"}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
@@ -547,8 +590,19 @@ const UptimeCreate = ({ isClone = false }) => {
|
||||
onChange={onChange}
|
||||
error={errors["port"] ? true : false}
|
||||
helperText={errors["port"]}
|
||||
hidden={monitor.type !== "port"}
|
||||
hidden={monitor.type !== "port" && monitor.type !== "game"}
|
||||
/>
|
||||
{monitor.type === "game" && (
|
||||
<Select
|
||||
name="gameId"
|
||||
label={t("chooseGame")}
|
||||
value={monitor.gameId || ""}
|
||||
placeholder={t("chooseGame")}
|
||||
onChange={onChange}
|
||||
items={GAMELIST}
|
||||
error={errors["gameId"] ? true : false}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
name="name"
|
||||
type="text"
|
||||
|
||||
@@ -32,6 +32,7 @@ const getTypeOptions = () => [
|
||||
{ value: "ping", label: "Ping" },
|
||||
{ value: "docker", label: "Docker" },
|
||||
{ value: "port", label: "Port" },
|
||||
{ value: "game", label: "Game" },
|
||||
];
|
||||
|
||||
// These functions were moved inline to ensure translations are applied correctly
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from "../../../Hooks/monitorHooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const TYPES = ["http", "ping", "docker", "port"];
|
||||
const TYPES = ["http", "ping", "docker", "port", "game"];
|
||||
const CreateMonitorButton = ({ shouldRender }) => {
|
||||
// Utils
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -74,6 +74,20 @@ class NetworkService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the games associated with a monitor
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*/
|
||||
async getMonitorGames() {
|
||||
return this.axiosInstance.get(`/monitors/games`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
|
||||
@@ -353,7 +353,7 @@ const baseTheme = (palette) => ({
|
||||
},
|
||||
|
||||
"& .MuiInputBase-input.MuiOutlinedInput-input": {
|
||||
padding: "0 var(--env-var-spacing-1-minus) !important",
|
||||
padding: `0 ${theme.spacing(5)}`,
|
||||
},
|
||||
|
||||
"& .MuiOutlinedInput-root": {
|
||||
@@ -408,6 +408,7 @@ const baseTheme = (palette) => ({
|
||||
"& .MuiOutlinedInput-root": {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
paddingRight: theme.spacing(5),
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
@@ -569,7 +570,7 @@ const baseTheme = (palette) => ({
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
padding: "0 13px",
|
||||
padding: `0 ${theme.spacing(5)}`,
|
||||
minHeight: "34px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -184,12 +184,12 @@ const monitorValidation = joi.object({
|
||||
.min(1)
|
||||
.max(65535)
|
||||
.when("type", {
|
||||
is: "port",
|
||||
then: joi.number().messages({
|
||||
is: joi.valid("port", "game"),
|
||||
then: joi.required().messages({
|
||||
"number.base": "Port must be a number.",
|
||||
"number.min": "Port must be at least 1.",
|
||||
"number.max": "Port must be at most 65535.",
|
||||
"any.required": "Port is required for port monitors.",
|
||||
"any.required": "Port is required for port and game monitors.",
|
||||
}),
|
||||
otherwise: joi.optional(),
|
||||
}),
|
||||
@@ -205,6 +205,14 @@ const monitorValidation = joi.object({
|
||||
expectedValue: joi.string().allow(null, ""),
|
||||
jsonPath: joi.string().allow(null, ""),
|
||||
matchMethod: joi.string().allow(null, ""),
|
||||
gameId: joi.when("type", {
|
||||
is: "game",
|
||||
then: joi.string().required().messages({
|
||||
"string.empty": "Game selection is required for game monitors.",
|
||||
"any.required": "Game selection is required for game monitors.",
|
||||
}),
|
||||
otherwise: joi.string().allow(null, ""),
|
||||
}),
|
||||
});
|
||||
|
||||
const imageValidation = joi.object({
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "",
|
||||
"seperateEmails": "",
|
||||
"checkFrequency": "",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "",
|
||||
"expectedValue": "",
|
||||
"deleteDialogTitle": "",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "",
|
||||
"seperateEmails": "",
|
||||
"checkFrequency": "",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "",
|
||||
"expectedValue": "",
|
||||
"deleteDialogTitle": "",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "Checkmate",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "Benachrichtige auch per E-Mail an mehrere Adressen (kommt bald)",
|
||||
"seperateEmails": "Du kannst mehrere E-Mails mit einem Komma trennen",
|
||||
"checkFrequency": "Überprüfungsfrequenz",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "",
|
||||
"expectedValue": "Erwarteter Wert",
|
||||
"deleteDialogTitle": "Möchtest du diesen Monitor wirklich löschen?",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "Checkmate",
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"highlighted": "one lower character"
|
||||
},
|
||||
"match": {
|
||||
"beginning": "Confirm password and password",
|
||||
"beginning": "Passwords",
|
||||
"highlighted": "must match"
|
||||
},
|
||||
"number": {
|
||||
@@ -195,7 +195,8 @@
|
||||
"termsAndPolicies": "By creating an account, you agree to our <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
|
||||
"toasts": {
|
||||
"success": "Welcome! Your account was created successfully."
|
||||
}
|
||||
},
|
||||
"welcome": "Welcome to Checkmate!"
|
||||
}
|
||||
},
|
||||
"avgCpuTemperature": "Average CPU Temperature",
|
||||
@@ -220,6 +221,7 @@
|
||||
"cancel": "Cancel",
|
||||
"checkFormError": "Please check the form for errors.",
|
||||
"checkFrequency": "Check frequency",
|
||||
"chooseGame": "Choose game",
|
||||
"checkHooks": {
|
||||
"failureResolveAll": "Failed to resolve all incidents.",
|
||||
"failureResolveMonitor": "Failed to resolve monitor incidents.",
|
||||
@@ -636,6 +638,11 @@
|
||||
"label": "URL to monitor",
|
||||
"namePlaceholder": "Localhost:5173",
|
||||
"placeholder": "localhost"
|
||||
},
|
||||
"game": {
|
||||
"label": "URL to monitor",
|
||||
"namePlaceholder": "localhost:5173",
|
||||
"placeholder": "localhost"
|
||||
}
|
||||
},
|
||||
"ms": "ms",
|
||||
@@ -762,6 +769,8 @@
|
||||
"pingMonitoringDescription": "Check whether your server is available or not.",
|
||||
"portMonitoring": "Port monitoring",
|
||||
"portMonitoringDescription": "Check whether your port is open or not.",
|
||||
"gameServerMonitoring": "Game Server Monitoring",
|
||||
"gameServerMonitoringDescription": "Check whether your game server is running or not",
|
||||
"portToMonitor": "Port to monitor",
|
||||
"publicLink": "Public link",
|
||||
"publicURL": "Public URL",
|
||||
@@ -1024,7 +1033,8 @@
|
||||
"docker": "Enter the Docker ID of your container. Docker IDs must be the full 64 char Docker ID. You can run docker inspect <short_id> to get the full container ID.",
|
||||
"http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.",
|
||||
"ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
|
||||
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard."
|
||||
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type."
|
||||
},
|
||||
"uptimeMonitor": {
|
||||
"fallback": {
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "",
|
||||
"seperateEmails": "",
|
||||
"checkFrequency": "",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "",
|
||||
"expectedValue": "",
|
||||
"deleteDialogTitle": "",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "",
|
||||
"seperateEmails": "",
|
||||
"checkFrequency": "",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "",
|
||||
"expectedValue": "",
|
||||
"deleteDialogTitle": "",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "",
|
||||
|
||||
@@ -264,6 +264,7 @@
|
||||
"notifyEmails": "Envoyer également une notification par email à plusieurs adresses (bientôt).",
|
||||
"seperateEmails": "Vous pouvez ajouter plusieurs adresses emails en les séparant par une virgule",
|
||||
"checkFrequency": "Vérifier la fréquence",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "Méthode de rapprochement",
|
||||
"expectedValue": "Valeur attendue",
|
||||
"deleteDialogTitle": "Voulez-vous vraiment supprimer ce moniteur ?",
|
||||
@@ -441,7 +442,8 @@
|
||||
"http": "Entrez l'URL ou l'IP du moniteur (par exemple https://exemple.fr ou 192.168.1.100) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.",
|
||||
"ping": "Entrez l'adresse IP ou le nom d'hôte à tester (par exemple, 192.168.1.100 ou exemple.fr) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.",
|
||||
"docker": "Entrer l'ID Docker du container. Les identifiants Docker doivent être les 64 caractères de l'ID Docker. Vous pouvez utiliser la commande docker inspect <short_id> pour avoir l'ID complet.",
|
||||
"port": "Entrez l'URL ou l'adresse IP du serveur, le numéro de port et un nom d'affichage familier qui apparaîtra sur le tableau de bord."
|
||||
"port": "Entrez l'URL ou l'adresse IP du serveur, le numéro de port et un nom d'affichage familier qui apparaîtra sur le tableau de bord.",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "Checkmate",
|
||||
|
||||
@@ -264,6 +264,7 @@
|
||||
"notifyEmails": "複数のアドレスにメールでも通知(近日公開)",
|
||||
"seperateEmails": "複数のメールはカンマで区切ることができます",
|
||||
"checkFrequency": "チェック頻度",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "マッチ方法",
|
||||
"expectedValue": "期待値",
|
||||
"deleteDialogTitle": "本当にこのモニターを削除しますか?",
|
||||
@@ -441,7 +442,8 @@
|
||||
"http": "監視するURLまたはIPを入力(例: https://example.com/ または 192.168.1.100)し、ダッシュボードに表示される明確な表示名を追加。",
|
||||
"ping": "pingするIPアドレスまたはホスト名を入力(例: 192.168.1.100 または example.com)し、ダッシュボードに表示される明確な表示名を追加。",
|
||||
"docker": "コンテナのDocker IDを入力。Docker IDは64文字のフルDocker IDである必要があります。docker inspect <short_id>を実行してフルコンテナIDを取得できます。",
|
||||
"port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。"
|
||||
"port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "Checkmate",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "Também notificar por e-mail para vários endereços (em breve)",
|
||||
"seperateEmails": "Você pode separar vários e-mails com uma vírgula",
|
||||
"checkFrequency": "Verifique a frequência",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "Método de correspondência",
|
||||
"expectedValue": "Valor esperado",
|
||||
"deleteDialogTitle": "Você realmente deseja excluir este monitor?",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "Insira a URL ou IP para monitorar (ex. https://exemplo.com.br/ ou 192.168.1.100) e adicione uma descrição que aparecerá na dashboard.",
|
||||
"ping": "Insira o endereço IP ou nome de domínio para ping (ex. 192.168.1.100 ou exemplo.com.br) e adicione uma descrição que aparecerá na dashboard.",
|
||||
"docker": "Insira o Docker Id do seu container. Docker Ids devem ser todos os 64 caracteres. Você pode executar docker inspect <short_id> para descobrir o Id completo.",
|
||||
"port": "Insira a URL ou o IP do servidor, aporta e uma descrição que aparecerá na dashboard."
|
||||
"port": "Insira a URL ou o IP do servidor, aporta e uma descrição que aparecerá na dashboard.",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "Checkmate",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "Также уведомлять по электронной почте на несколько адресов (скоро)",
|
||||
"seperateEmails": "Вы можете разделить несколько адресов электронной почты запятой.",
|
||||
"checkFrequency": "Проверить частоту",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "Метод сопоставления",
|
||||
"expectedValue": "Ожидаемое значение",
|
||||
"deleteDialogTitle": "Вы действительно хотите удалить этот монитор?",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "Ayrıca birden fazla eposta adresine bildirim gönderebilirsiniz (yakında geliyor)",
|
||||
"seperateEmails": "Birden fazla epostayı virgülle ayırabilirsiniz",
|
||||
"checkFrequency": "Frekansı denetle",
|
||||
"chooseGame": "Oyun seç",
|
||||
"matchMethod": "Eşleşme yöntemi",
|
||||
"expectedValue": "Beklenen değer",
|
||||
"deleteDialogTitle": "Gerçekten bu monitörü silmek istiyor musunuz?",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "Checkmate",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "",
|
||||
"seperateEmails": "",
|
||||
"checkFrequency": "",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "",
|
||||
"expectedValue": "",
|
||||
"deleteDialogTitle": "",
|
||||
@@ -435,7 +436,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "",
|
||||
"seperateEmails": "",
|
||||
"checkFrequency": "",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "",
|
||||
"expectedValue": "",
|
||||
"deleteDialogTitle": "",
|
||||
@@ -435,7 +436,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "",
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
"notifyEmails": "",
|
||||
"seperateEmails": "",
|
||||
"checkFrequency": "",
|
||||
"chooseGame": "",
|
||||
"matchMethod": "",
|
||||
"expectedValue": "",
|
||||
"deleteDialogTitle": "",
|
||||
@@ -437,7 +438,8 @@
|
||||
"http": "",
|
||||
"ping": "",
|
||||
"docker": "",
|
||||
"port": ""
|
||||
"port": "",
|
||||
"game": ""
|
||||
},
|
||||
"common": {
|
||||
"appName": "",
|
||||
|
||||
Reference in new issue
Block a user