Files
Checkmate/client/src/Pages/Maintenance/CreateMaintenance/index.jsx
T

440 lines
12 KiB
React

import { Box, Button, Stack, Typography } from "@mui/material";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { maintenanceWindowValidation } from "@/Validation/validation.js";
import { createToast } from "@/Utils/toastUtils.jsx";
import MonitorList from "./Components/MonitorList/index.jsx";
import ConfigSelect from "./Components/ConfigSelect/index.jsx";
import useMaintenanceData from "./hooks/useMaintenanceData.jsx";
import useMaintenanceActions from "./hooks/useMaintenanceActions.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import Icon from "@/Components/v1/Icon";
import "./index.css";
import Search from "@/Components/v1/Inputs/Search/index.jsx";
import { logger } from "@/Utils/Logger.js";
import { useNavigate, useParams } from "react-router-dom";
import { buildErrors, hasValidationErrors } from "@/Validation/error.js";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
const repeatConfig = [
{ _id: 0, name: "Don't repeat", value: "none" },
{
_id: 1,
name: "Repeat daily",
value: "daily",
},
{ _id: 2, name: "Repeat weekly", value: "weekly" },
];
const durationConfig = [
{ _id: 0, name: "seconds" },
{ _id: 1, name: "minutes" },
{ _id: 2, name: "hours" },
{
_id: 3,
name: "days",
},
];
const CreateMaintenance = () => {
const { maintenanceWindowId } = useParams();
const { handleSubmitForm } = useMaintenanceActions();
const { fetchMonitorsMaintenance, initializeMaintenanceForEdit } = useMaintenanceData();
const navigate = useNavigate();
const theme = useTheme();
const { t } = useTranslation();
const { user } = useSelector((state) => state.auth);
const [monitors, setMonitors] = useState([]);
const [search, setSearch] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({});
const [form, setForm] = useState({
repeat: "none",
startDate: dayjs(),
startTime: dayjs(),
duration: "",
durationUnit: "seconds",
name: "",
monitors: [],
});
const handleFormChange = (key, value) => {
setForm((prev) => ({ ...prev, [key]: value }));
const { error } = maintenanceWindowValidation.validate(
{ [key]: value },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
useEffect(() => {
const fetchMonitors = async () => {
setIsLoading(true);
try {
const fetchedMonitors = await fetchMonitorsMaintenance();
setMonitors(fetchedMonitors);
if (maintenanceWindowId === undefined) {
return;
}
const maintenanceWindowInformation = await initializeMaintenanceForEdit(
maintenanceWindowId,
fetchedMonitors
);
setForm((prev) => ({
...prev,
...maintenanceWindowInformation,
}));
} catch (error) {
createToast({ body: "Failed to fetch data" });
logger.error("Failed to fetch monitors", error);
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [user]);
const handleSubmit = async () => {
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return;
const request = handleSubmitForm(maintenanceWindowId, form);
try {
setIsLoading(true);
await request;
createToast({
body: "Successfully created maintenance window",
});
navigate("/maintenance");
} catch (error) {
createToast({
body: `Failed to ${
maintenanceWindowId === undefined ? "create" : "edit"
} maintenance window`,
});
logger.error(error);
} finally {
setIsLoading(false);
}
};
return (
<Box className="create-maintenance">
<Breadcrumbs
list={[
{ name: "maintenance", path: "/maintenance" },
{
name: maintenanceWindowId === undefined ? "create" : "edit",
path: `/maintenance/create`,
},
]}
/>
<Stack
component="form"
noValidate
spellCheck="false"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Box>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
>
{`${maintenanceWindowId === undefined ? t("createA") : t("edit")}`}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
{t("maintenance")}{" "}
</Typography>
<Typography
component="span"
fontSize="inherit"
>
{t("window")}
</Typography>
</Typography>
<Typography
component="h2"
variant="body2"
fontSize={14}
>
{t("maintenanceWindowDescription")}
</Typography>
</Box>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
id="name"
label={t("friendlyNameInput")}
placeholder={t("friendlyNamePlaceholder")}
value={form.name}
onChange={(event) => {
handleFormChange("name", event.target.value);
}}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
<ConfigSelect
id="repeat"
name="maintenance-repeat"
label={t("maintenanceRepeat")}
valueSelect={form.repeat}
configSelection={repeatConfig}
onChange={(value) => handleFormChange("repeat", value)}
/>
<Stack>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
id="startDate"
disablePast
disableHighlightToday
value={form.startDate}
slots={{
openPickerIcon: () => (
<Icon
name="Calendar"
size={20}
/>
),
}}
slotProps={{
switchViewButton: { sx: { display: "none" } },
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
minHeight: 34,
p: 0,
pr: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.primary.lowContrast,
},
},
},
inputAdornment: { sx: { ml: 0, px: 3 } },
openPickerButton: {
sx: {
py: 0,
mr: 0,
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.1,
},
"&:hover": { backgroundColor: "transparent" },
},
},
// CAIO_REVIEW, entire popper
popper: {
sx: {
"& .MuiPickersDay-root": {
color: theme.palette.primary.contrastText,
"&.Mui-selected": {
backgroundColor: theme.palette.accent.main, // Selected day background
color: theme.palette.primary.contrastText, // Selected day text color
},
"&:hover": {
backgroundColor: theme.palette.accent.light, // Hover background
},
"&.Mui-disabled": {
color: theme.palette.secondary.main, // Disabled day color
},
},
"& .MuiDayCalendar-weekDayLabel": {
color: theme.palette.primary.contrastText,
},
"& .MuiPickersCalendarHeader-label": {
color: theme.palette.primary.contrastText,
},
},
},
}}
sx={{}}
onChange={(newDate) => {
handleFormChange("startDate", newDate);
}}
error={errors["startDate"]}
/>
</LocalizationProvider>
</Stack>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("startTime")}
</Typography>
<Typography> {t("timeZoneInfo")} </Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MobileTimePicker
id="startTime"
label={t("startTime")}
value={form.startTime}
onChange={(newTime) => {
handleFormChange("startTime", newTime);
}}
slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
minHeight: 34,
p: 0,
pl: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.primary.lowContrast,
},
},
},
}}
error={errors["startTime"]}
/>
</LocalizationProvider>
<Stack
direction="row"
alignItems="end"
spacing={theme.spacing(8)}
>
<TextInput
type="number"
id="duration"
label={t("duration")}
value={form.duration}
onChange={(event) => {
handleFormChange("duration", event.target.value);
}}
error={errors["duration"] ? true : false}
helperText={errors["duration"]}
/>
<ConfigSelect
id="durationUnit"
valueSelect={form.durationUnit}
configSelection={durationConfig}
onChange={(value) => handleFormChange("durationUnit", value)}
error={errors["durationUnit"]}
/>
</Stack>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("monitorsToApply")}
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<Search
id={"monitors"}
label={t("addMonitors")}
multiple={true}
isAdorned={true}
options={monitors}
filteredBy="name"
secondaryLabel={"type"}
inputValue={search}
value={form.monitors}
handleInputChange={setSearch}
handleChange={(selected) => {
handleFormChange("monitors", selected);
}}
error={errors["monitors"]}
/>
<MonitorList
selectedMonitors={form.monitors}
setSelectedMonitors={(monitors) =>
setForm((prev) => ({ ...prev, monitors }))
}
/>
</Stack>
</ConfigBox>
<Box
ml="auto"
display="inline-block"
>
<Button
variant="contained"
color="secondary"
onClick={() => navigate("/maintenance")}
sx={{ mr: theme.spacing(6) }}
>
{t("cancel")}
</Button>
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={handleSubmit}
disabled={false}
>
{`${
maintenanceWindowId === undefined
? t("createMaintenance")
: t("editMaintenance")
}`}
</Button>
</Box>
</Stack>
</Box>
);
};
export default CreateMaintenance;