duration
This commit is contained in:
1 parent
dc0e198a8e
commit
f0b152fc8f
10 files changed
+142
-72
No files matched your search
@@ -3,6 +3,7 @@ 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 Typography from "@mui/material/Typography";
|
||||
import { FieldLabel } from "./FieldLabel";
|
||||
import { Calendar } from "lucide-react";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
@@ -11,11 +12,15 @@ import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
interface DatePickerComponentProps extends Omit<DatePickerProps<Dayjs>, "label"> {
|
||||
fieldLabel?: string;
|
||||
required?: boolean;
|
||||
error?: boolean;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
export const DatePickerComponent = ({
|
||||
fieldLabel,
|
||||
required,
|
||||
error,
|
||||
helperText,
|
||||
...props
|
||||
}: DatePickerComponentProps) => {
|
||||
const theme = useTheme();
|
||||
@@ -56,12 +61,12 @@ export const DatePickerComponent = ({
|
||||
pr: theme.spacing(5),
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.divider,
|
||||
borderColor: error ? theme.palette.error.main : 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,
|
||||
borderColor: error ? theme.palette.error.main : theme.palette.divider,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -78,14 +83,18 @@ export const DatePickerComponent = ({
|
||||
</LocalizationProvider>
|
||||
);
|
||||
|
||||
if (fieldLabel) {
|
||||
return (
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||
{picker}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return picker;
|
||||
return (
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
{fieldLabel && <FieldLabel required={required}>{fieldLabel}</FieldLabel>}
|
||||
{picker}
|
||||
{helperText && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color={error ? "error" : "text.secondary"}
|
||||
>
|
||||
{helperText}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import dayjs from "dayjs";
|
||||
import {
|
||||
maintenanceWindowSchema,
|
||||
repeatOptions,
|
||||
durationUnitOptions,
|
||||
type MaintenanceWindowFormData,
|
||||
} from "@/Validation/maintenanceWindow";
|
||||
import type { MaintenanceWindow } from "@/Types/MaintenanceWindow";
|
||||
@@ -17,27 +16,6 @@ const getRepeatId = (repeatMs: number): string => {
|
||||
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 = {}) => {
|
||||
@@ -46,19 +24,14 @@ export const useMaintenanceWindowForm = ({
|
||||
|
||||
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,
|
||||
duration: data.duration ?? 0,
|
||||
durationUnit: data.durationUnit ?? "minutes",
|
||||
monitors: [data.monitorId],
|
||||
};
|
||||
} else {
|
||||
|
||||
@@ -19,25 +19,32 @@ import type { MaintenanceWindow } from "@/Types/MaintenanceWindow";
|
||||
import type { MaintenanceWindowFormData } from "@/Validation/maintenanceWindow";
|
||||
import { repeatOptions, durationUnitOptions } from "@/Validation/maintenanceWindow";
|
||||
import { useMaintenanceWindowForm } from "@/Hooks/useMaintenanceWindowForm";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGet, usePost, usePatch } from "@/Hooks/UseApi";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { useEffect } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
const CreateMaintenanceWindowPage = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { maintenanceWindowId } = useParams<{ maintenanceWindowId: string }>();
|
||||
const isEditMode = Boolean(maintenanceWindowId);
|
||||
|
||||
const { data: existingMaintenanceWindow } = useGet<MaintenanceWindow>(
|
||||
isEditMode ? `/maintenance-window/${maintenanceWindowId}` : null
|
||||
isEditMode ? `/maintenance-window/${maintenanceWindowId}` : null,
|
||||
{},
|
||||
{ keepPreviousData: false }
|
||||
);
|
||||
|
||||
const { data: monitors } = useGet<Monitor[]>("/monitors/team");
|
||||
|
||||
const { post, loading: isPosting } = usePost();
|
||||
const { patch, loading: isPatching } = usePatch();
|
||||
|
||||
const { schema, defaults } = useMaintenanceWindowForm({
|
||||
data: existingMaintenanceWindow,
|
||||
});
|
||||
@@ -47,14 +54,61 @@ const CreateMaintenanceWindowPage = () => {
|
||||
defaultValues: defaults,
|
||||
});
|
||||
|
||||
const { control, handleSubmit } = form;
|
||||
const { control, handleSubmit, trigger } = form;
|
||||
useEffect(() => {
|
||||
if (existingMaintenanceWindow) {
|
||||
form.reset(defaults);
|
||||
}
|
||||
}, [existingMaintenanceWindow, defaults, form]);
|
||||
|
||||
const onSubmit = (data: MaintenanceWindowFormData) => {
|
||||
console.log("Form submitted with data:", data);
|
||||
const buildPayload = (data: MaintenanceWindowFormData) => {
|
||||
const startDateTime = dayjs(data.startDate)
|
||||
.set("hour", parseInt(data.startTime.split(":")[0], 10))
|
||||
.set("minute", parseInt(data.startTime.split(":")[1], 10));
|
||||
|
||||
const durationUnit = durationUnitOptions.find((o) => o.id === data.durationUnit);
|
||||
const durationInMs = data.duration * (durationUnit?.multiplier ?? 1000);
|
||||
const endDateTime = startDateTime.add(durationInMs, "milliseconds");
|
||||
|
||||
const repeatOption = repeatOptions.find((o) => o.id === data.repeat);
|
||||
const repeat = repeatOption?.value ?? 0;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
name: data.name,
|
||||
duration: data.duration,
|
||||
durationUnit: data.durationUnit,
|
||||
monitors: data.monitors,
|
||||
start: startDateTime.toISOString(),
|
||||
end: endDateTime.toISOString(),
|
||||
repeat,
|
||||
};
|
||||
|
||||
if (repeat === 0) {
|
||||
payload.expiry = endDateTime.toISOString();
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
const onSubmit = async (data: MaintenanceWindowFormData) => {
|
||||
const payload = buildPayload(data);
|
||||
|
||||
let result;
|
||||
if (isEditMode && maintenanceWindowId) {
|
||||
result = await patch(`/maintenance-window/${maintenanceWindowId}`, payload);
|
||||
} else {
|
||||
result = await post("/maintenance-window", payload);
|
||||
}
|
||||
|
||||
if (result?.success) {
|
||||
navigate("/maintenance");
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = isPosting || isPatching;
|
||||
|
||||
const onError = (errors: any) => {
|
||||
console.log("Form errors:", errors);
|
||||
console.error("Form submission errors:", errors);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -121,7 +175,7 @@ const CreateMaintenanceWindowPage = () => {
|
||||
name="startDate"
|
||||
control={control}
|
||||
defaultValue={defaults.startDate}
|
||||
render={({ field }) => (
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
fieldLabel={t(
|
||||
"pages.maintenanceWindow.form.startDate.option.startDate.label"
|
||||
@@ -130,6 +184,8 @@ const CreateMaintenanceWindowPage = () => {
|
||||
onChange={(date) => {
|
||||
field.onChange(date ? date.format("YYYY-MM-DD") : "");
|
||||
}}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -152,7 +208,7 @@ const CreateMaintenanceWindowPage = () => {
|
||||
)}
|
||||
value={field.value ? dayjs(field.value, "HH:mm") : null}
|
||||
onChange={(time) => {
|
||||
field.onChange(time ? time.format("HH:mm") : "");
|
||||
field.onChange(time ? time.format("HH:mm") : ""); trigger("startDate");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -241,7 +297,9 @@ const CreateMaintenanceWindowPage = () => {
|
||||
{...params}
|
||||
placeholder={
|
||||
selectedMonitors.length === 0
|
||||
? t("pages.maintenanceWindow.form.startTime.monitors.option.addMonitors.label")
|
||||
? t(
|
||||
"pages.maintenanceWindow.form.startTime.monitors.option.addMonitors.label"
|
||||
)
|
||||
: ""
|
||||
}
|
||||
error={!!fieldState.error}
|
||||
@@ -269,9 +327,7 @@ const CreateMaintenanceWindowPage = () => {
|
||||
size="small"
|
||||
onClick={() => {
|
||||
field.onChange(
|
||||
field.value.filter(
|
||||
(id: string) => id !== monitor.id
|
||||
)
|
||||
field.value.filter((id: string) => id !== monitor.id)
|
||||
);
|
||||
}}
|
||||
aria-label="Remove monitor"
|
||||
@@ -294,7 +350,7 @@ const CreateMaintenanceWindowPage = () => {
|
||||
spacing={theme.spacing(2)}
|
||||
>
|
||||
<Button
|
||||
// loading={isSubmitting || isPatching}
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
export type DurationUnit = "seconds" | "minutes" | "hours" | "days";
|
||||
|
||||
export interface MaintenanceWindow {
|
||||
id: string;
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
active: boolean;
|
||||
name: string;
|
||||
duration: number;
|
||||
durationUnit: DurationUnit;
|
||||
repeat: number;
|
||||
start: string;
|
||||
end: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const repeatOptions = [
|
||||
{ id: "none", name: "Don't repeat", value: 0 },
|
||||
@@ -13,20 +14,30 @@ export const durationUnitOptions = [
|
||||
{ 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 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"),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const startDateTime = dayjs(data.startDate)
|
||||
.set("hour", parseInt(data.startTime.split(":")[0], 10))
|
||||
.set("minute", parseInt(data.startTime.split(":")[1], 10));
|
||||
return startDateTime.isAfter(dayjs());
|
||||
},
|
||||
{
|
||||
message: "Start date and time must be in the future",
|
||||
path: ["startDate"],
|
||||
}
|
||||
);
|
||||
|
||||
export type MaintenanceWindowFormData = z.infer<typeof maintenanceWindowSchema>;
|
||||
@@ -818,7 +818,7 @@
|
||||
},
|
||||
"startTime": {
|
||||
"title": "Start time",
|
||||
"description": "Set the start time and duration for your maintenance window.",
|
||||
"description": "Set the start time and duration for your maintenance window. All values are in UTC",
|
||||
"option": {
|
||||
"duration": {
|
||||
"label": "Duration"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema, model, type Types } from "mongoose";
|
||||
import type { MaintenanceWindow } from "@/types/maintenanceWindow.js";
|
||||
import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js";
|
||||
|
||||
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
|
||||
monitorId: Types.ObjectId;
|
||||
@@ -34,6 +34,13 @@ const MaintenanceWindowSchema = new Schema<MaintenanceWindowDocument>(
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
},
|
||||
durationUnit: {
|
||||
type: String,
|
||||
enum: ["seconds", "minutes", "hours", "days"],
|
||||
},
|
||||
repeat: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
@@ -33,6 +33,8 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
||||
teamId: this.toStringId(doc.teamId),
|
||||
active: doc.active,
|
||||
name: doc.name,
|
||||
duration: doc.duration,
|
||||
durationUnit: doc.durationUnit,
|
||||
repeat: doc.repeat,
|
||||
start: this.toDateString(doc.start),
|
||||
end: this.toDateString(doc.end),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
export type DurationUnit = "seconds" | "minutes" | "hours" | "days";
|
||||
|
||||
export interface MaintenanceWindow {
|
||||
id: string;
|
||||
monitorId: String;
|
||||
teamId: string;
|
||||
active: boolean;
|
||||
name: string;
|
||||
duration: number;
|
||||
durationUnit: DurationUnit;
|
||||
repeat: number;
|
||||
start: string;
|
||||
end: string;
|
||||
|
||||
@@ -364,6 +364,8 @@ const createMaintenanceWindowBodyValidation = joi.object({
|
||||
monitors: joi.array().items(joi.string()).required(),
|
||||
name: joi.string().required(),
|
||||
active: joi.boolean(),
|
||||
duration: joi.number().required(),
|
||||
durationUnit: joi.string().valid("seconds", "minutes", "hours", "days").required(),
|
||||
start: joi.date().required(),
|
||||
end: joi.date().required(),
|
||||
repeat: joi.number().required(),
|
||||
@@ -402,6 +404,8 @@ const editMaintenanceByIdWindowBodyValidation = joi.object({
|
||||
end: joi.date(),
|
||||
expiry: joi.date(),
|
||||
monitors: joi.array(),
|
||||
duration: joi.number(),
|
||||
durationUnit: joi.string().valid("seconds", "minutes", "hours", "days"),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
|
||||
Reference in new issue
Block a user