monitors
This commit is contained in:
1 parent
f2363a7a09
commit
54d71bef61
4 files changed
+328
-6
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import { useMemo } from "react";
|
||||
import { statusPageSchema, type StatusPageFormData } from "@/Validation/statusPage";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
interface UseStatusPageFormOptions {
|
||||
data?: StatusPage | null;
|
||||
}
|
||||
|
||||
export const useStatusPageForm = ({ data = null }: UseStatusPageFormOptions = {}) => {
|
||||
return useMemo(() => {
|
||||
const defaults: StatusPageFormData = {
|
||||
companyName: data?.companyName || "",
|
||||
url: data?.url || "",
|
||||
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
color: data?.color || "#4169E1",
|
||||
monitors: data?.monitors || [],
|
||||
isPublished: data?.isPublished ?? false,
|
||||
showCharts: data?.showCharts ?? true,
|
||||
showUptimePercentage: data?.showUptimePercentage ?? true,
|
||||
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
||||
customCSS: data?.customCSS || "",
|
||||
logo: data?.logo || null,
|
||||
};
|
||||
|
||||
return { schema: statusPageSchema, defaults };
|
||||
}, [data]);
|
||||
};
|
||||
@@ -1,16 +1,68 @@
|
||||
import { BasePage, ConfigBox } from "@/Components/v2/design-elements";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { ImageUpload, SwitchComponent } from "@/Components/v2/inputs";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import {
|
||||
ImageUpload,
|
||||
SwitchComponent,
|
||||
Button,
|
||||
TextField,
|
||||
Autocomplete,
|
||||
} from "@/Components/v2/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
|
||||
import type { StatusPageFormData } from "@/Validation/statusPage";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import timezones from "@/Utils/timezones.json";
|
||||
|
||||
interface TimezoneOption {
|
||||
_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const CreateStatusPage = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { schema, defaults } = useStatusPageForm();
|
||||
|
||||
const { data: monitorsResponse } = useGet<Monitor[]>(
|
||||
"/monitors/team?type=http&type=ping&type=port&type=docker"
|
||||
);
|
||||
const monitors = monitorsResponse ?? [];
|
||||
|
||||
const form = useForm<StatusPageFormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaults,
|
||||
});
|
||||
|
||||
const { control, reset, handleSubmit } = form;
|
||||
|
||||
useEffect(() => {
|
||||
reset(defaults);
|
||||
}, [defaults, reset]);
|
||||
|
||||
const onError = (errors: any) => {
|
||||
console.log(errors);
|
||||
};
|
||||
|
||||
const onSubmit = (data: StatusPageFormData) => {
|
||||
console.log("Form Data Submitted: ", data);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<BasePage
|
||||
component="form"
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.access.title")}
|
||||
subtitle={t("pages.statusPages.form.access.description")}
|
||||
@@ -20,9 +72,15 @@ const CreateStatusPage = () => {
|
||||
alignItems="center"
|
||||
spacing={theme.spacing(2)}
|
||||
>
|
||||
<SwitchComponent
|
||||
// checked={field.value ?? false}
|
||||
// onChange={(e) => field.onChange(e.target.checked)}
|
||||
<Controller
|
||||
name="isPublished"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SwitchComponent
|
||||
checked={field.value ?? false}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Typography>
|
||||
{t("pages.statusPages.form.access.option.published.name")}
|
||||
@@ -30,15 +88,184 @@ const CreateStatusPage = () => {
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.basicInfo.title")}
|
||||
subtitle={t("pages.statusPages.form.basicInfo.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="companyName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.statusPages.form.basicInfo.option.name.label")}
|
||||
placeholder={t(
|
||||
"pages.statusPages.form.basicInfo.option.name.placeholder"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.statusPages.form.basicInfo.option.url.label")}
|
||||
placeholder={t(
|
||||
"pages.statusPages.form.basicInfo.option.url.placeholder"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.monitors.title")}
|
||||
subtitle={t("pages.statusPages.form.monitors.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="monitors"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const selectedMonitors = monitors.filter((m) => field.value.includes(m.id));
|
||||
return (
|
||||
<Stack spacing={theme.spacing(4)}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={monitors}
|
||||
getOptionLabel={(option: Monitor) => option.name}
|
||||
value={selectedMonitors}
|
||||
onChange={(_, newValue: Monitor[]) => {
|
||||
field.onChange(newValue.map((m) => m.id));
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
fieldLabel={t(
|
||||
"pages.statusPages.form.monitors.option.monitors.label"
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder={t(
|
||||
"pages.statusPages.form.monitors.option.monitors.placeholder"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{selectedMonitors.length > 0 && (
|
||||
<Stack
|
||||
flex={1}
|
||||
width="100%"
|
||||
>
|
||||
{selectedMonitors.map((monitor, index) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
key={monitor.id}
|
||||
width="100%"
|
||||
>
|
||||
<Typography flexGrow={1}>{monitor.name}</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
field.onChange(
|
||||
field.value.filter((id: string) => id !== monitor.id)
|
||||
);
|
||||
}}
|
||||
aria-label="Remove monitor"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</IconButton>
|
||||
{index < selectedMonitors.length - 1 && <Divider />}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.timezone.title")}
|
||||
subtitle={t("pages.statusPages.form.timezone.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="timezone"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Autocomplete
|
||||
options={timezones}
|
||||
getOptionLabel={(option: TimezoneOption) => option.name}
|
||||
value={
|
||||
timezones.find((tz: TimezoneOption) => tz._id === field.value) ?? null
|
||||
}
|
||||
onChange={(_, newValue: TimezoneOption | null) => {
|
||||
field.onChange(newValue?._id ?? "");
|
||||
}}
|
||||
fieldLabel={t("pages.statusPages.form.timezone.option.timezone.label")}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder={t(
|
||||
"pages.statusPages.form.timezone.option.timezone.placeholder"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.appearance.title")}
|
||||
subtitle={t("pages.statusPages.form.appearance.description")}
|
||||
rightContent={
|
||||
<Stack alignItems={"center"}>
|
||||
<ImageUpload />
|
||||
<Controller
|
||||
name="logo"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<ImageUpload
|
||||
src={field.value?.data}
|
||||
onChange={(file) => {
|
||||
if (file) {
|
||||
field.onChange({
|
||||
data: file.src,
|
||||
contentType: file.file.type,
|
||||
});
|
||||
} else {
|
||||
field.onChange(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
// loading={isSubmitting}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const statusPageSchema = z.object({
|
||||
companyName: z
|
||||
.string()
|
||||
.min(1, "Company name is required")
|
||||
.max(100, "Company name must be at most 100 characters"),
|
||||
url: z
|
||||
.string()
|
||||
.min(1, "URL is required")
|
||||
.max(50, "URL must be at most 50 characters")
|
||||
.regex(
|
||||
/^[a-z0-9-]+$/,
|
||||
"URL can only contain lowercase letters, numbers, and hyphens"
|
||||
),
|
||||
timezone: z.string().optional(),
|
||||
color: z.string().min(1, "Color is required"),
|
||||
monitors: z.array(z.string()),
|
||||
isPublished: z.boolean(),
|
||||
showCharts: z.boolean(),
|
||||
showUptimePercentage: z.boolean(),
|
||||
showAdminLoginLink: z.boolean(),
|
||||
customCSS: z.string().optional(),
|
||||
logo: z
|
||||
.object({
|
||||
data: z.string(),
|
||||
contentType: z.string(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type StatusPageFormData = z.infer<typeof statusPageSchema>;
|
||||
@@ -508,6 +508,41 @@
|
||||
"title": "Appearance",
|
||||
"description": "Define the default look and feel of your public status page",
|
||||
"option": {}
|
||||
},
|
||||
"basicInfo": {
|
||||
"title": "Basic information",
|
||||
"description": "Name and address of your status page",
|
||||
"option": {
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "My status page"
|
||||
},
|
||||
"url": {
|
||||
"label": "URL",
|
||||
"placeholder": "my-status-page"
|
||||
}
|
||||
}
|
||||
},
|
||||
"monitors": {
|
||||
"title": "Monitors",
|
||||
"description": "Select which monitors to display on your status page",
|
||||
"noMonitors": "No monitors available. Create uptime monitors first.",
|
||||
"option": {
|
||||
"monitors": {
|
||||
"label": "Monitors",
|
||||
"placeholder": "Select monitors..."
|
||||
}
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"title": "Timezone",
|
||||
"description": "Select the timezone for your status page",
|
||||
"option": {
|
||||
"timezone": {
|
||||
"label": "Timezone",
|
||||
"placeholder": "Search timezones..."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
Reference in new issue
Block a user