Revert "FE - Advanced Settings Page Validation and Error Handling"

This commit is contained in:
Alexander Holliday
2024-10-22 18:21:10 +08:00
committed by GitHub
parent c9864d9075
commit 238db5bc8f
4 changed files with 279 additions and 339 deletions
+216 -239
View File
@@ -12,47 +12,45 @@ import { useNavigate } from "react-router";
import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
import { useState, useEffect } from "react";
import Select from "../../Components/Inputs/Select";
import { advancedSettingsValidation } from "../../Validation/validation";
import { buildErrors, hasValidationErrors } from "../../Validation/error";
const AdvancedSettings = ({ isAdmin }) => {
const navigate = useNavigate();
useEffect(() => {
if (!isAdmin) {
navigate("/");
}
}, [navigate, isAdmin]);
const navigate = useNavigate();
const theme = useTheme();
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const settings = useSelector((state) => state.settings);
const [localSettings, setLocalSettings] = useState({
apiBaseUrl: "",
logLevel: "debug",
systemEmailHost: "",
systemEmailPort: "",
systemEmailAddress: "",
systemEmailPassword: "",
jwtTTL: "",
dbType: "",
redisHost: "",
redisPort: "",
pagespeedApiKey: "",
});
const [errors, setErrors] = useState({});
useEffect(() => {
if (!isAdmin) {
navigate("/");
}
}, [navigate, isAdmin]);
useEffect(() => {
const getSettings = async () => {
const action = await dispatch(getAppSettings({ authToken }));
if (action.payload?.success) {
setLocalSettings(action.payload.data);
} else {
createToast({ body: "Failed to get settings" });
}
};
getSettings();
}, [authToken, dispatch]);
const theme = useTheme();
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const settings = useSelector((state) => state.settings);
const [localSettings, setLocalSettings] = useState({
apiBaseUrl: "",
logLevel: "debug",
systemEmailHost: "",
systemEmailPort: "",
systemEmailAddress: "",
systemEmailPassword: "",
jwtTTL: "",
dbType: "",
redisHost: "",
redisPort: "",
pagespeedApiKey: "",
});
useEffect(() => {
const getSettings = async () => {
const action = await dispatch(getAppSettings({ authToken }));
if (action.payload.success) {
setLocalSettings(action.payload.data);
} else {
createToast({ body: "Failed to get settings" });
}
};
getSettings();
}, [authToken, dispatch]);
const logItems = [
{ _id: 1, name: "none" },
@@ -74,210 +72,189 @@ const AdvancedSettings = ({ isAdmin }) => {
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
};
const handleChange = (event) => {
const { value, id } = event.target;
setLocalSettings({ ...localSettings, [id]: value });
const { error } = advancedSettingsValidation.validate(
{ [id]: value },
{
abortEarly: false,
}
);
setErrors((prev) => {
return buildErrors(prev, id, error);
});
};
const handleChange = (event) => {
const { value, id } = event.target;
setLocalSettings({ ...localSettings, [id]: value });
};
const handleSave = async () => {
if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors))
return;
const action = await dispatch(
updateAppSettings({ settings: localSettings, authToken })
);
let body = "";
if (action.payload.success) {
console.log(action.payload.data);
setLocalSettings(action.payload.data);
body = "Settings saved successfully";
} else {
body = "Failed to save settings";
}
createToast({ body });
};
const handleSave = async () => {
const action = await dispatch(
updateAppSettings({ settings: localSettings, authToken })
);
let body = "";
if (action.payload.success) {
console.log(action.payload.data);
setLocalSettings(action.payload.data);
body = "Settings saved successfully";
} else {
body = "Failed to save settings";
}
createToast({ body });
};
return (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
>
<ConfigBox>
<Box>
<Typography component="h1">Client settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify client settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
id="apiBaseUrl"
label="API URL Host"
value={localSettings.apiBaseUrl}
onChange={handleChange}
error={errors.apiBaseUrl}
/>
<Select
id="logLevel"
label="Log level"
name="logLevel"
items={logItems}
value={logItemLookup[localSettings.logLevel]}
onChange={handleLogLevel}
error={errors.logLevel}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Email settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Set your host email settings here. These settings are used for
sending system emails.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="systemEmailHost"
label="Email host"
name="systemEmailHost"
value={localSettings.systemEmailHost}
onChange={handleChange}
error={errors.systemEmailHost}
/>
<Field
type="number"
id="systemEmailPort"
label="System email address"
name="systemEmailPort"
value={localSettings.systemEmailPort.toString()}
onChange={handleChange}
error={errors.systemEmailPort}
/>
<Field
type="email"
id="systemEmailAddress"
label="System email address"
name="systemEmailAddress"
value={localSettings.systemEmailAddress}
onChange={handleChange}
error={errors.systemEmailAddress}
/>
<Field
type="text"
id="systemEmailPassword"
label="System email password"
name="systemEmailPassword"
value={localSettings.systemEmailPassword}
onChange={handleChange}
error={errors.systemEmailPassword}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Server settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify server settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="jwtTTL"
label="JWT time to live"
name="jwtTTL"
value={localSettings.jwtTTL}
onChange={handleChange}
error={errors.jwtTTL}
/>
<Field
type="text"
id="dbType"
label="Database type"
name="dbType"
value={localSettings.dbType}
onChange={handleChange}
error={errors.dbType}
/>
<Field
type="text"
id="redisHost"
label="Redis host"
name="redisHost"
value={localSettings.redisHost}
onChange={handleChange}
error={errors.redisHost}
/>
<Field
type="number"
id="redisPort"
label="Redis port"
name="redisPort"
value={localSettings.redisPort.toString()}
onChange={handleChange}
error={errors.redisPort}
/>
<Field
type="text"
id="pagespeedApiKey"
label="PageSpeed API key"
name="pagespeedApiKey"
value={localSettings.pagespeedApiKey}
onChange={handleChange}
error={errors.pagespeedApiKey}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
</Box>
<Box>
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
<Typography
sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}
>
Developed by Bluewave Labs.
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs"
label="https://github.com/bluewave-labs"
/>
</Box>
</ConfigBox>
<Stack direction="row" justifyContent="flex-end">
<LoadingButton
loading={settings.isLoading || settings.authIsLoading}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
Save
</LoadingButton>
</Stack>
</Stack>
</Box>
);
return (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
>
<ConfigBox>
<Box>
<Typography component="h1">Client settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify client settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
id="apiBaseUrl"
label="API URL Host"
value={localSettings.apiBaseUrl}
onChange={handleChange}
/>
<Select
id="logLevel"
label="Log level"
name="logLevel"
items={logItems}
value={logItemLookup[localSettings.logLevel]}
onChange={handleLogLevel}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Email settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Set your host email settings here. These settings are used for sending
system emails.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="systemEmailHost"
label="Email host"
name="systemEmailHost"
value={localSettings.systemEmailHost}
onChange={handleChange}
/>
<Field
type="number"
id="systemEmailPort"
label="System email address"
name="systemEmailPort"
value={localSettings.systemEmailPort.toString()}
onChange={handleChange}
/>
<Field
type="email"
id="systemEmailAddress"
label="System email address"
name="systemEmailAddress"
value={localSettings.systemEmailAddress}
onChange={handleChange}
/>
<Field
type="text"
id="systemEmailPassword"
label="System email password"
name="systemEmailPassword"
value={localSettings.systemEmailPassword}
onChange={handleChange}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Server settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify server settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="jwtTTL"
label="JWT time to live"
name="jwtTTL"
value={localSettings.jwtTTL}
onChange={handleChange}
/>
<Field
type="text"
id="dbType"
label="Database type"
name="dbType"
value={localSettings.dbType}
onChange={handleChange}
/>
<Field
type="text"
id="redisHost"
label="Redis host"
name="redisHost"
value={localSettings.redisHost}
onChange={handleChange}
/>
<Field
type="number"
id="redisPort"
label="Redis port"
name="redisPort"
value={localSettings.redisPort.toString()}
onChange={handleChange}
/>
<Field
type="text"
id="pagespeedApiKey"
label="PageSpeed API key"
name="pagespeedApiKey"
value={localSettings.pagespeedApiKey}
onChange={handleChange}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
</Box>
<Box>
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
Developed by Bluewave Labs.
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs"
label="https://github.com/bluewave-labs"
/>
</Box>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<LoadingButton
loading={settings.isLoading || settings.authIsLoading}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
Save
</LoadingButton>
</Stack>
</Stack>
</Box>
);
};
AdvancedSettings.propTypes = {
@@ -28,7 +28,6 @@ import {
MS_PER_WEEK,
} from "../../../Utils/timeUtils";
import { useNavigate, useParams } from "react-router-dom";
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
const getDurationAndUnit = (durationInMs) => {
if (durationInMs % MS_PER_DAY === 0) {
@@ -146,36 +145,46 @@ const CreateMaintenance = () => {
return;
}
const res = await networkService.getMaintenanceWindowById({
authToken: authToken,
maintenanceWindowId: maintenanceWindowId,
});
const maintenanceWindow = res.data.data;
const { name, start, end, repeat, monitorId } = maintenanceWindow;
const startTime = dayjs(start);
const endTime = dayjs(end);
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
const monitor = monitors.find((monitor) => monitor._id === monitorId);
setForm({
...form,
name,
repeat: REVERSE_REPEAT_LOOKUP[repeat],
startDate: startTime,
startTime,
duration,
durationUnit,
monitors: monitor ? [monitor] : [],
});
} catch (error) {
createToast({ body: "Failed to fetch data" });
logger.error("Failed to fetch monitors", error);
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, user]);
const res = await networkService.getMaintenanceWindowById({
authToken: authToken,
maintenanceWindowId: maintenanceWindowId,
});
const maintenanceWindow = res.data.data;
const { name, start, end, repeat, monitorId } = maintenanceWindow;
const startTime = dayjs(start);
const endTime = dayjs(end);
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
const monitor = monitors.find((monitor) => monitor._id === monitorId);
setForm({
...form,
name,
repeat: REVERSE_REPEAT_LOOKUP[repeat],
startDate: startTime,
startTime,
duration,
durationUnit,
monitors: monitor ? [monitor] : [],
});
} catch (error) {
createToast({ body: "Failed to fetch data" });
logger.error("Failed to fetch monitors", error);
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, user]);
const buildErrors = (prev, id, error) => {
const updatedErrors = { ...prev };
if (error) {
updatedErrors[id] = error.details[0].message;
} else {
delete updatedErrors[id];
}
return updatedErrors;
};
const handleSearch = (value) => {
setSearch(value);
@@ -214,17 +223,29 @@ const CreateMaintenance = () => {
});
};
const handleSubmit = async () => {
if(hasValidationErrors(form, maintenanceWindowValidation, setErrors))
return;
// Build timestamp for maintenance window from startDate and startTime
const start = dayjs(form.startDate)
.set("hour", form.startTime.hour())
.set("minute", form.startTime.minute());
// Build end timestamp for maintenance window
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
const durationInMs = form.duration * MS_MULTIPLIER;
const end = start.add(durationInMs);
const handleSubmit = async () => {
const { error } = maintenanceWindowValidation.validate(form, {
abortEarly: false,
});
// If errors, return early
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
logger.error(error);
return;
}
// Build timestamp for maintenance window from startDate and startTime
const start = dayjs(form.startDate)
.set("hour", form.startTime.hour())
.set("minute", form.startTime.minute());
// Build end timestamp for maintenance window
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
const durationInMs = form.duration * MS_MULTIPLIER;
const end = start.add(durationInMs);
// Get repeat value in milliseconds
const repeat = REPEAT_LOOKUP[form.repeat];
-25
View File
@@ -1,25 +0,0 @@
const buildErrors = (prev, id, error) => {
const updatedErrors = { ...prev };
if (error) {
updatedErrors[id] = error.details[0].message?? "Validation error";
} else {
delete updatedErrors[id];
}
return updatedErrors;
};
const hasValidationErrors = (form, validation, setErrors) => {
const { error } = validation.validate(form, {
abortEarly: false,
});
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message?? "Validation error";
});
setErrors(newErrors);
return true;
}
return false;
};
export { buildErrors, hasValidationErrors };
-33
View File
@@ -128,43 +128,10 @@ const maintenanceWindowValidation = joi.object({
monitors: joi.array().min(1),
});
const advancedSettingsValidation = joi.object({
apiBaseUrl: joi.string().uri({ allowRelative: true }).trim().messages({
"string.empty": "API base url is required.",
"string.uri": "The URL you provided is not valid.",
}),
logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
systemEmailHost: joi.string().allow(''),
systemEmailPort: joi.string().allow(''),
systemEmailAddress: joi
.string().allow(''),
systemEmailPassword: joi.string().allow(''),
jwtTTL: joi
.string()
.trim()
.messages({
"string.empty": "JWT TTL is required."
}),
dbType: joi.string().trim().messages({
"string.empty": "DB type is required",
}),
redisHost: joi.string().trim().messages({
"string.empty": "Redis host is required",
}),
redisPort: joi.string().allow('').custom((value, helpers) => {
if(value && isNaN(parseInt(value))){
return helpers.message("Redit port must be a number")
}
return value
}),
pagespeedApiKey: joi.string().allow('')
});
export {
credentials,
imageValidation,
monitorValidation,
settingsValidation,
maintenanceWindowValidation,
advancedSettingsValidation,
};