Merge remote-tracking branch 'origin' into Network-tab-implementation-FE

This commit is contained in:
Owaise committed 2025-08-09 12:21:21 +05:30
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(
+17 -1
View File
@@ -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>
+26 -1
View File
@@ -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,
};
+53 -141
View File
@@ -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>
);
};
+97 -120
View File
@@ -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;
+53 -30
View File
@@ -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>
+60 -6
View File
@@ -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
+1 -1
View File
@@ -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();
+14
View File
@@ -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",
},
});
}
/**
*
* ************************************
+3 -2
View File
@@ -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",
+11 -3
View File
@@ -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({
+3 -1
View File
@@ -260,6 +260,7 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -437,7 +438,8 @@
"http": "",
"ping": "",
"docker": "",
"port": ""
"port": "",
"game": ""
},
"common": {
"appName": "",
+3 -1
View File
@@ -260,6 +260,7 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -437,7 +438,8 @@
"http": "",
"ping": "",
"docker": "",
"port": ""
"port": "",
"game": ""
},
"common": {
"appName": "Checkmate",
+3 -1
View File
@@ -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",
+13 -3
View File
@@ -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": {
+3 -1
View File
@@ -260,6 +260,7 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -437,7 +438,8 @@
"http": "",
"ping": "",
"docker": "",
"port": ""
"port": "",
"game": ""
},
"common": {
"appName": "",
+3 -1
View File
@@ -260,6 +260,7 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -437,7 +438,8 @@
"http": "",
"ping": "",
"docker": "",
"port": ""
"port": "",
"game": ""
},
"common": {
"appName": "",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -260,6 +260,7 @@
"notifyEmails": "Также уведомлять по электронной почте на несколько адресов (скоро)",
"seperateEmails": "Вы можете разделить несколько адресов электронной почты запятой.",
"checkFrequency": "Проверить частоту",
"chooseGame": "",
"matchMethod": "Метод сопоставления",
"expectedValue": "Ожидаемое значение",
"deleteDialogTitle": "Вы действительно хотите удалить этот монитор?",
@@ -437,7 +438,8 @@
"http": "",
"ping": "",
"docker": "",
"port": ""
"port": "",
"game": ""
},
"common": {
"appName": "",
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -260,6 +260,7 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -435,7 +436,8 @@
"http": "",
"ping": "",
"docker": "",
"port": ""
"port": "",
"game": ""
},
"common": {
"appName": "",
+3 -1
View File
@@ -260,6 +260,7 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -435,7 +436,8 @@
"http": "",
"ping": "",
"docker": "",
"port": ""
"port": "",
"game": ""
},
"common": {
"appName": "",
+3 -1
View File
@@ -260,6 +260,7 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -437,7 +438,8 @@
"http": "",
"ping": "",
"docker": "",
"port": ""
"port": "",
"game": ""
},
"common": {
"appName": "",