general
This commit is contained in:
1 parent
d74d79c332
commit
f0b2292f0a
14 files changed
+371
-14
No files matched your search
@@ -0,0 +1,91 @@
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import type { DatePickerProps } from "@mui/x-date-pickers/DatePicker";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { FieldLabel } from "./FieldLabel";
|
||||
import { Calendar } from "lucide-react";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
|
||||
interface DatePickerComponentProps extends Omit<DatePickerProps<Dayjs>, "label"> {
|
||||
fieldLabel?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const DatePickerComponent = ({
|
||||
fieldLabel,
|
||||
required,
|
||||
...props
|
||||
}: DatePickerComponentProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const picker = (
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DatePicker
|
||||
{...props}
|
||||
slots={{
|
||||
openPickerIcon: () => (
|
||||
<Calendar
|
||||
size={20}
|
||||
stroke={theme.palette.text.secondary}
|
||||
/>
|
||||
),
|
||||
...props.slots,
|
||||
}}
|
||||
slotProps={{
|
||||
switchViewButton: { sx: { display: "none" } },
|
||||
nextIconButton: { sx: { ml: theme.spacing(2) } },
|
||||
day: {
|
||||
sx: {
|
||||
"&.MuiPickersDay-root.Mui-disabled": {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
field: {
|
||||
sx: {
|
||||
width: "fit-content",
|
||||
"& > .MuiOutlinedInput-root": {
|
||||
flexDirection: "row-reverse",
|
||||
},
|
||||
"& input": {
|
||||
minHeight: 34,
|
||||
p: 0,
|
||||
pl: theme.spacing(3),
|
||||
pr: theme.spacing(5),
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.divider,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
|
||||
{
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
},
|
||||
},
|
||||
inputAdornment: { sx: { ml: 0 } },
|
||||
openPickerButton: {
|
||||
sx: {
|
||||
py: 0,
|
||||
mr: 0,
|
||||
},
|
||||
},
|
||||
...props.slotProps,
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
|
||||
if (fieldLabel) {
|
||||
return (
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||
{picker}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return picker;
|
||||
};
|
||||
@@ -14,3 +14,4 @@ export * from "./Switch";
|
||||
export * from "./Slider";
|
||||
export * from "./ImageUpload";
|
||||
export * from "./ColorPicker";
|
||||
export { DatePickerComponent as DatePicker } from "./DatePicker";
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useMemo } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
maintenanceWindowSchema,
|
||||
repeatOptions,
|
||||
durationUnitOptions,
|
||||
type MaintenanceWindowFormData,
|
||||
} from "@/Validation/maintenanceWindow";
|
||||
import type { MaintenanceWindow } from "@/Types/MaintenanceWindow";
|
||||
|
||||
interface UseMaintenanceWindowFormOptions {
|
||||
data?: MaintenanceWindow | null;
|
||||
}
|
||||
|
||||
const getRepeatId = (repeatMs: number): string => {
|
||||
const option = repeatOptions.find((opt) => opt.value === repeatMs);
|
||||
return option?.id ?? "none";
|
||||
};
|
||||
|
||||
const getDurationAndUnit = (
|
||||
startMs: number,
|
||||
endMs: number
|
||||
): { duration: number; durationUnit: string } => {
|
||||
const diffMs = endMs - startMs;
|
||||
|
||||
for (const unit of [...durationUnitOptions].reverse()) {
|
||||
if (diffMs >= unit.multiplier && diffMs % unit.multiplier === 0) {
|
||||
return {
|
||||
duration: diffMs / unit.multiplier,
|
||||
durationUnit: unit.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
duration: diffMs / 1000,
|
||||
durationUnit: "seconds",
|
||||
};
|
||||
};
|
||||
|
||||
export const useMaintenanceWindowForm = ({
|
||||
data = null,
|
||||
}: UseMaintenanceWindowFormOptions = {}) => {
|
||||
return useMemo(() => {
|
||||
let defaults: MaintenanceWindowFormData;
|
||||
|
||||
if (data) {
|
||||
const startDate = dayjs(data.start);
|
||||
const endDate = dayjs(data.end);
|
||||
const { duration, durationUnit } = getDurationAndUnit(
|
||||
startDate.valueOf(),
|
||||
endDate.valueOf()
|
||||
);
|
||||
|
||||
defaults = {
|
||||
name: data.name,
|
||||
repeat: getRepeatId(data.repeat),
|
||||
startDate: startDate.format("YYYY-MM-DD"),
|
||||
startTime: startDate.format("HH:mm"),
|
||||
duration,
|
||||
durationUnit,
|
||||
monitors: [data.monitorId],
|
||||
};
|
||||
} else {
|
||||
const now = dayjs();
|
||||
defaults = {
|
||||
name: "",
|
||||
repeat: "none",
|
||||
startDate: now.format("YYYY-MM-DD"),
|
||||
startTime: now.format("HH:mm"),
|
||||
duration: 0,
|
||||
durationUnit: "minutes",
|
||||
monitors: [],
|
||||
};
|
||||
}
|
||||
|
||||
return { schema: maintenanceWindowSchema, defaults };
|
||||
}, [data]);
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,120 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BasePage, ConfigBox } from "@/Components/v2/design-elements";
|
||||
import { TextField, Select, DatePicker } from "@/Components/v2/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import dayjs from "dayjs";
|
||||
import type { MaintenanceWindow } from "@/Types/MaintenanceWindow";
|
||||
import type { MaintenanceWindowFormData } from "@/Validation/maintenanceWindow";
|
||||
import { repeatOptions } from "@/Validation/maintenanceWindow";
|
||||
import { useMaintenanceWindowForm } from "@/Hooks/useMaintenanceWindowForm";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
|
||||
|
||||
const CreateMaintenanceWindowPage = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { maintenanceWindowId } = useParams<{ maintenanceWindowId: string }>();
|
||||
const isEditMode = Boolean(maintenanceWindowId);
|
||||
|
||||
const { data: existingMaintenanceWindow } = useGet<MaintenanceWindow>(
|
||||
isEditMode ? `/maintenance-window/${maintenanceWindowId}` : null
|
||||
);
|
||||
|
||||
const { schema, defaults } = useMaintenanceWindowForm({
|
||||
data: existingMaintenanceWindow,
|
||||
});
|
||||
|
||||
const form = useForm<MaintenanceWindowFormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaults,
|
||||
});
|
||||
|
||||
const { control } = form;
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.general.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.general.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
defaultValue={defaults.name}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.maintenanceWindow.form.general.option.name.label")}
|
||||
placeholder={t(
|
||||
"pages.maintenanceWindow.form.general.option.name.placeholder"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="repeat"
|
||||
control={control}
|
||||
defaultValue={defaults.repeat}
|
||||
render={({ field, fieldState }) => (
|
||||
<Select
|
||||
value={field.value}
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.general.option.repeat.label"
|
||||
)}
|
||||
error={!!fieldState.error}
|
||||
onChange={field.onChange}
|
||||
>
|
||||
{repeatOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
>
|
||||
<Typography>{option.name}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.maintenanceWindow.form.startTime.title")}
|
||||
subtitle={t("pages.maintenanceWindow.form.startTime.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
defaultValue={defaults.startDate}
|
||||
render={({ field }) => (
|
||||
<DatePicker
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startTime.option.startDate.label"
|
||||
)}
|
||||
value={field.value ? dayjs(field.value, "YYYY-MM-DD") : null}
|
||||
onChange={(date) => {
|
||||
field.onChange(date ? date.format("YYYY-MM-DD") : "");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateMaintenanceWindowPage;
|
||||
File renamed without changes.
@@ -49,7 +49,7 @@ import EditUser from "../Pages/Account/EditUser/index.jsx";
|
||||
import Settings from "../Pages/Settings";
|
||||
|
||||
import Maintenance from "../Pages/Maintenance";
|
||||
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance/index.jsx";
|
||||
import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create";
|
||||
|
||||
import ProtectedRoute from "../Components/v1/ProtectedRoute";
|
||||
import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute";
|
||||
@@ -313,7 +313,13 @@ const Routes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="/maintenance/create/:maintenanceWindowId?"
|
||||
element={<CreateNewMaintenanceWindow />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<CreateNewMaintenanceWindow />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const repeatOptions = [
|
||||
{ id: "none", name: "Don't repeat", value: 0 },
|
||||
{ id: "daily", name: "Repeat daily", value: 86400000 },
|
||||
{ id: "weekly", name: "Repeat weekly", value: 604800000 },
|
||||
] as const;
|
||||
|
||||
export const durationUnitOptions = [
|
||||
{ id: "seconds", name: "seconds", multiplier: 1000 },
|
||||
{ id: "minutes", name: "minutes", multiplier: 60000 },
|
||||
{ id: "hours", name: "hours", multiplier: 3600000 },
|
||||
{ id: "days", name: "days", multiplier: 86400000 },
|
||||
] as const;
|
||||
|
||||
export const maintenanceWindowSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, "Name is required")
|
||||
.max(100, "Name must be at most 100 characters"),
|
||||
repeat: z.string(),
|
||||
startDate: z.string().min(1, "Start date is required"),
|
||||
startTime: z.string().min(1, "Start time is required"),
|
||||
duration: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, "Duration must be at least 1"),
|
||||
durationUnit: z.string(),
|
||||
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
||||
});
|
||||
|
||||
export type MaintenanceWindowFormData = z.infer<typeof maintenanceWindowSchema>;
|
||||
+40
-12
@@ -792,6 +792,45 @@
|
||||
"nextWindow": "Next window",
|
||||
"repeat": "Repeat"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"general": {
|
||||
"title": "General settings",
|
||||
"description": "Set a name and repeat option for your maintenance window.",
|
||||
"option": {
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "e.g. Weekly Maintenance"
|
||||
},
|
||||
"repeat": {
|
||||
"label": "Repeat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"startTime": {
|
||||
"title": "Start time",
|
||||
"description": "Set the start time and duration for your maintenance window.",
|
||||
"option": {
|
||||
"duration": {
|
||||
"label": "Duration"
|
||||
},
|
||||
"startDate": {
|
||||
"label": "Start date"
|
||||
},
|
||||
"startTime": {
|
||||
"label": "Start time"
|
||||
}
|
||||
},
|
||||
"monitors": {
|
||||
"title": "Monitors",
|
||||
"description": "Monitors that the maintenance window should apply to",
|
||||
"option": {
|
||||
"addMonitors": {
|
||||
"label": "Add monitors"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -1282,16 +1321,5 @@
|
||||
"monitorDeleted": "Monitor deleted successfully",
|
||||
"monitorPaused": "Monitor paused successfully",
|
||||
"monitorResumed": "Monitor resumed successfully",
|
||||
"name": "Name",
|
||||
"maintenanceWindow": {
|
||||
"fallback": {
|
||||
"actionButton": "Let's create your first maintenance window!",
|
||||
"checks": [
|
||||
"Mark your maintenance periods",
|
||||
"Eliminate any misunderstandings",
|
||||
"Stop sending alerts in maintenance windows"
|
||||
],
|
||||
"title": "A maintenance window is used to:"
|
||||
}
|
||||
}
|
||||
"name": "Name"
|
||||
}
|
||||
Reference in new issue
Block a user