mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-24 19:01:01 -06:00
feat/v2/uptime/create
This commit is contained in:
16
client/package-lock.json
generated
16
client/package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"human-interval": "2.0.1",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"mui-color-input": "^6.0.0",
|
||||
@@ -4118,6 +4119,15 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/human-interval": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz",
|
||||
"integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"numbered": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.4.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz",
|
||||
@@ -4952,6 +4962,12 @@
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/numbered": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz",
|
||||
"integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"human-interval": "2.0.1",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"mui-color-input": "^6.0.0",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react";
|
||||
import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Checkbox Component
|
||||
|
||||
24
client/src/Components/v2/DesignElements/BasePage.tsx
Normal file
24
client/src/Components/v2/DesignElements/BasePage.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
interface BasePageProps extends StackProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BasePage: React.FC<BasePageProps> = ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
spacing={theme.spacing(10)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
68
client/src/Components/v2/DesignElements/SplitBox.tsx
Normal file
68
client/src/Components/v2/DesignElements/SplitBox.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Fragment } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
export const SplitBox = ({
|
||||
left,
|
||||
right,
|
||||
}: {
|
||||
left: React.ReactNode;
|
||||
right: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
return (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
bgcolor={theme.palette.primary.main}
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.spacing(2)}
|
||||
>
|
||||
<Box
|
||||
padding={theme.spacing(15)}
|
||||
borderRight={isSmall ? 0 : 1}
|
||||
borderBottom={isSmall ? 1 : 0}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
flex={0.7}
|
||||
>
|
||||
{left}
|
||||
</Box>
|
||||
<Box
|
||||
flex={1}
|
||||
padding={theme.spacing(15)}
|
||||
>
|
||||
{right}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigBox = ({
|
||||
title,
|
||||
subtitle,
|
||||
rightContent,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
rightContent: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<SplitBox
|
||||
left={
|
||||
<Fragment>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography component="p">{subtitle}</Typography>
|
||||
</Fragment>
|
||||
}
|
||||
right={rightContent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
2
client/src/Components/v2/DesignElements/index.tsx
Normal file
2
client/src/Components/v2/DesignElements/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
|
||||
export { BasePage } from "./BasePage";
|
||||
48
client/src/Components/v2/Inputs/AutoComplete.tsx
Normal file
48
client/src/Components/v2/Inputs/AutoComplete.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import type { AutocompleteProps } from "@mui/material/Autocomplete";
|
||||
import { TextInput } from "@/Components/v2/Inputs/TextInput";
|
||||
import { CheckboxInput } from "@/Components/v2/Inputs/Checkbox";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
type AutoCompleteInputProps = Omit<
|
||||
AutocompleteProps<any, boolean, boolean, boolean>,
|
||||
"renderInput"
|
||||
> & {
|
||||
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
|
||||
};
|
||||
|
||||
export const AutoCompleteInput: React.FC<AutoCompleteInputProps> = ({ ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Autocomplete
|
||||
{...props}
|
||||
disableCloseOnSelect
|
||||
renderInput={(params) => (
|
||||
<TextInput
|
||||
{...params}
|
||||
placeholder="Type to search"
|
||||
/>
|
||||
)}
|
||||
getOptionKey={(option) => option._id}
|
||||
renderTags={() => null}
|
||||
renderOption={(props, option, { selected }) => {
|
||||
const { key, ...optionProps } = props;
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
{...optionProps}
|
||||
>
|
||||
<CheckboxInput checked={selected} />
|
||||
{option.name}
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
||||
padding: `0 ${theme.spacing(5)}`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
23
client/src/Components/v2/Inputs/Checkbox.tsx
Normal file
23
client/src/Components/v2/Inputs/Checkbox.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import type { CheckboxProps } from "@mui/material/Checkbox";
|
||||
import CheckboxOutline from "@/assets/icons/checkbox-outline.svg?react";
|
||||
import CheckboxFilled from "@/assets/icons/checkbox-filled.svg?react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
type CheckboxInputProps = CheckboxProps & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const CheckboxInput: React.FC<CheckboxInputProps> = ({ label, ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Checkbox
|
||||
{...props}
|
||||
icon={<CheckboxOutline />}
|
||||
checkedIcon={<CheckboxFilled />}
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"& svg": { width: theme.spacing(8), height: theme.spacing(8) },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
62
client/src/Components/v2/Inputs/RadioInput.tsx
Normal file
62
client/src/Components/v2/Inputs/RadioInput.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import Radio from "@mui/material/Radio";
|
||||
import type { RadioProps } from "@mui/material/Radio";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import RadioChecked from "@/assets/icons/radio-checked.svg?react";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
interface RadioInputProps extends RadioProps {}
|
||||
|
||||
export const RadioInput: React.FC<RadioInputProps> = ({ ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Radio
|
||||
{...props}
|
||||
checkedIcon={<RadioChecked />}
|
||||
sx={{
|
||||
color: "transparent",
|
||||
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
|
||||
"&:not(.Mui-checked)": {
|
||||
boxShadow: `inset 0 0 0 1px ${theme.palette.primary.contrastText}70`, // Use theme text color for the outline
|
||||
},
|
||||
mt: theme.spacing(0.5),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RadioWithDescription: React.FC<
|
||||
RadioInputProps & { label: string; description: string }
|
||||
> = ({ label, description, ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={<RadioInput {...props} />}
|
||||
label={
|
||||
<>
|
||||
<Typography component="p">{label}</Typography>
|
||||
<Typography
|
||||
component="h6"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
sx={{
|
||||
alignItems: "flex-start",
|
||||
p: theme.spacing(2.5),
|
||||
m: theme.spacing(-2.5),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
},
|
||||
"& .MuiButtonBase-root": {
|
||||
p: 0,
|
||||
mr: theme.spacing(6),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
6
client/src/Components/v2/Inputs/Select.tsx
Normal file
6
client/src/Components/v2/Inputs/Select.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import Select from "@mui/material/Select";
|
||||
import type { SelectProps } from "@mui/material/Select";
|
||||
|
||||
export const SelectInput: React.FC<SelectProps> = ({ ...props }) => {
|
||||
return <Select {...props} />;
|
||||
};
|
||||
@@ -1,5 +1,15 @@
|
||||
import { forwardRef } from "react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type { TextFieldProps } from "@mui/material";
|
||||
export const TextInput = (props: TextFieldProps) => {
|
||||
return <TextField {...props} />;
|
||||
};
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
function TextInput(props, ref) {
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
inputRef={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
TextInput.displayName = "TextInput";
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Outlet } from "react-router";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { SideBar } from "@/Components/v2/Layouts/Sidebar";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
const RootLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
minHeight="100vh"
|
||||
>
|
||||
<SideBar />
|
||||
<Outlet />
|
||||
<Stack
|
||||
flex={1}
|
||||
padding={theme.spacing(12)}
|
||||
>
|
||||
<Outlet />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
262
client/src/Pages/v2/Uptime/Create.tsx
Normal file
262
client/src/Pages/v2/Uptime/Create.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { TextInput } from "@/Components/v2/Inputs/TextInput";
|
||||
import { AutoCompleteInput } from "@/Components/v2/Inputs/AutoComplete";
|
||||
import { ConfigBox, BasePage } from "@/Components/v2/DesignElements";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import { RadioWithDescription } from "@/Components/v2/Inputs/RadioInput";
|
||||
import Button from "@mui/material/Button";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import { Typography } from "@mui/material";
|
||||
import humanInterval from "human-interval";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, Controller, useWatch } from "react-hook-form";
|
||||
import { int, z } from "zod";
|
||||
import { monitorSchema } from "@/Validation/v2/zod";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useGet, usePost } from "@/Hooks/v2/UseApi";
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
|
||||
const CreateUptimePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
type FormValues = z.infer<typeof monitorSchema>;
|
||||
type SubmitValues = Omit<FormValues, "interval"> & { interval: number | undefined };
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(monitorSchema) as any,
|
||||
defaultValues: {
|
||||
type: "http",
|
||||
url: "",
|
||||
n: 3,
|
||||
notificationChannels: [],
|
||||
name: "",
|
||||
interval: "1 minute",
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const { response } = useGet<ApiResponse>("/notification-channels");
|
||||
const { post, loading, error } = usePost<SubmitValues, ApiResponse>("/monitors");
|
||||
const selectedType = useWatch({
|
||||
control,
|
||||
name: "type",
|
||||
});
|
||||
const notificationChannels = useWatch({
|
||||
control,
|
||||
name: "notificationChannels",
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
let interval = humanInterval(data.interval);
|
||||
if (!interval) interval = 60000;
|
||||
const submitData = { ...data, interval };
|
||||
const result = await post(submitData);
|
||||
if (result) {
|
||||
console.log(result);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const notificationOptions = response?.data ?? [];
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<BasePage>
|
||||
<ConfigBox
|
||||
title={t("distributedUptimeCreateChecks")}
|
||||
subtitle={t("distributedUptimeCreateChecksDescription")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl error={!!errors.type}>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
sx={{ gap: theme.spacing(6) }}
|
||||
>
|
||||
<RadioWithDescription
|
||||
value="http"
|
||||
label={"HTTP"}
|
||||
description={"Use HTTP to monitor your website or API endpoint."}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="https"
|
||||
label="HTTPS"
|
||||
description="Use HTTPS to monitor your website or API endpoint.
|
||||
"
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="ping"
|
||||
label={t("pingMonitoring")}
|
||||
description={t("pingMonitoringDescription")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("settingsGeneralSettings")}
|
||||
subtitle={t(`uptimeGeneralInstructions.${selectedType}`)}
|
||||
rightContent={
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("url")}
|
||||
fullWidth
|
||||
error={!!errors.url}
|
||||
helperText={errors.url ? errors.url.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("displayName")}
|
||||
fullWidth
|
||||
error={!!errors.name}
|
||||
helperText={errors.name ? errors.name.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("createMonitorPage.incidentConfigTitle")}
|
||||
subtitle={t("createMonitorPage.incidentConfigDescriptionV2")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="n"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
label={t("createMonitorPage.incidentConfigStatusCheckNumber")}
|
||||
fullWidth
|
||||
error={!!errors.n}
|
||||
helperText={errors.n ? errors.n.message : ""}
|
||||
onChange={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
field.onChange(target.valueAsNumber);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("notificationConfig.title")}
|
||||
subtitle={t("notificationConfig.description")}
|
||||
rightContent={
|
||||
<Stack>
|
||||
<Controller
|
||||
name="notificationChannels"
|
||||
control={control}
|
||||
defaultValue={[]} // important!
|
||||
render={({ field }) => (
|
||||
<AutoCompleteInput
|
||||
multiple
|
||||
options={notificationOptions}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={notificationOptions.filter((o: any) =>
|
||||
(field.value || []).includes(o._id)
|
||||
)}
|
||||
onChange={(_, newValue) => {
|
||||
field.onChange(newValue.map((o: any) => o._id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
mt={theme.spacing(2)}
|
||||
>
|
||||
{notificationChannels.map((notificationId) => {
|
||||
const option = notificationOptions.find(
|
||||
(o: any) => o._id === notificationId
|
||||
);
|
||||
if (!option) return null;
|
||||
return (
|
||||
<Stack
|
||||
width={"100%"}
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
key={notificationId}
|
||||
>
|
||||
<Typography>{option.name}</Typography>
|
||||
<DeleteOutlineRoundedIcon
|
||||
onClick={() => {
|
||||
const updated = notificationChannels.filter(
|
||||
(id) => id !== notificationId
|
||||
);
|
||||
setValue("notificationChannels", updated);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("createMonitorPage.intervalTitle")}
|
||||
subtitle="How often to check the URL"
|
||||
rightContent={
|
||||
<Controller
|
||||
name="interval"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("createMonitorPage.intervalDescription")}
|
||||
fullWidth
|
||||
error={!!errors.interval}
|
||||
helperText={errors.interval ? errors.interval.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
>
|
||||
{t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</BasePage>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUptimePage;
|
||||
@@ -4,6 +4,7 @@ import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme";
|
||||
|
||||
import AuthLoginV2 from "@/Pages/v2/Auth/Login";
|
||||
import AuthRegisterV2 from "@/Pages/v2/Auth/Register";
|
||||
import CreateUptimePage from "@/Pages/v2/Uptime/Create";
|
||||
import RootLayout from "@/Components/v2/Layouts/RootLayout";
|
||||
|
||||
const V2Routes = ({ mode = "light" }) => {
|
||||
@@ -32,6 +33,10 @@ const V2Routes = ({ mode = "light" }) => {
|
||||
path="uptime"
|
||||
element={<h1>Test Page</h1>}
|
||||
/>
|
||||
<Route
|
||||
path="uptime/create"
|
||||
element={<CreateUptimePage />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -66,6 +66,16 @@ export const theme = (mode: string, palette: any) =>
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiRadio: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 0,
|
||||
"& .MuiSvgIcon-root": {
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
|
||||
36
client/src/Validation/v2/zod.ts
Normal file
36
client/src/Validation/v2/zod.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from "zod";
|
||||
import humanInterval from "human-interval";
|
||||
const urlRegex =
|
||||
/^(?:https?:\/\/)?([a-zA-Z0-9.-]+|\d{1,3}(\.\d{1,3}){3}|\[[0-9a-fA-F:]+\])(:\d{1,5})?$/;
|
||||
|
||||
const durationSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val || val.trim() === "") return;
|
||||
const ms = humanInterval(val);
|
||||
|
||||
if (!ms || isNaN(ms)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Invalid duration format",
|
||||
});
|
||||
} else if (ms < 10000) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Minimum duration is 10 seconds",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const monitorSchema = z.object({
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"),
|
||||
n: z.coerce
|
||||
.number({ message: "Number required" })
|
||||
.min(1, "Minimum value is 1")
|
||||
.max(25, "Maximum value is 25"),
|
||||
notificationChannels: z.array(z.string()).optional().default([]),
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
interval: durationSchema,
|
||||
});
|
||||
@@ -484,6 +484,7 @@
|
||||
},
|
||||
"uptimeGeneralInstructions": {
|
||||
"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.",
|
||||
"https": "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.",
|
||||
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
|
||||
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
|
||||
@@ -1094,9 +1095,13 @@
|
||||
"chooseGame": "Choose game",
|
||||
"createMonitorPage": {
|
||||
"incidentConfigDescription": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.",
|
||||
"incidentConfigDescriptionV2": "Number of consecutive checks needed to change monitor status. Maximum 25",
|
||||
"incidentConfigStatusWindowLabel": "How many checks should be in the sliding window?",
|
||||
"incidentConfigStatusCheckNumber": "How many checks before status change?",
|
||||
"incidentConfigStatusWindowThresholdLabel": "What percentage of checks in the sliding window fail/succeed before monitor status changes?",
|
||||
"incidentConfigTitle": "Incidents"
|
||||
"incidentConfigTitle": "Incidents",
|
||||
"intervalTitle": "Check interval",
|
||||
"intervalDescription": "How often the monitor should be checked."
|
||||
},
|
||||
"dataRate": "Data Rate",
|
||||
"dataReceived": "Data Received",
|
||||
|
||||
Reference in New Issue
Block a user