- Updated existing components for the status page usage

- Add libraries for drag and drop
This commit is contained in:
Shemy Gan
2024-12-30 14:07:05 -05:00
parent 3e841b2e16
commit 031508ef9b
9 changed files with 231 additions and 14 deletions

View File

@@ -20,9 +20,12 @@
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"immutability-helper": "^3.1.1",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-redux": "9.2.0",
"react-router": "^6.23.0",
@@ -1638,6 +1641,21 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-dnd/asap": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
"integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
},
"node_modules/@react-dnd/invariant": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
"integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
},
"node_modules/@react-dnd/shallowequal": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
},
"node_modules/@react-spring/animated": {
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
@@ -3235,6 +3253,24 @@
"node": ">=0.4.0"
}
},
"node_modules/dnd-core": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
"integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
"dependencies": {
"@react-dnd/asap": "^5.0.1",
"@react-dnd/invariant": "^4.0.1",
"redux": "^4.2.0"
}
},
"node_modules/dnd-core/node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -4262,6 +4298,11 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutability-helper": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz",
"integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -5414,6 +5455,43 @@
"node": ">=0.10.0"
}
},
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
"dependencies": {
"@react-dnd/invariant": "^4.0.1",
"@react-dnd/shallowequal": "^4.0.1",
"dnd-core": "^16.0.1",
"fast-deep-equal": "^3.1.3",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"@types/hoist-non-react-statics": ">= 3.3.1",
"@types/node": ">= 12",
"@types/react": ">= 16",
"react": ">= 16.14"
},
"peerDependenciesMeta": {
"@types/hoist-non-react-statics": {
"optional": true
},
"@types/node": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dnd-html5-backend": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
"dependencies": {
"dnd-core": "^16.0.1"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",

View File

@@ -23,9 +23,12 @@
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"immutability-helper": "^3.1.1",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-redux": "9.2.0",
"react-router": "^6.23.0",

View File

@@ -21,6 +21,7 @@ import "./index.css";
* @param {string} [props.value] - Optional value associated with the checkbox
* @param {Function} [props.onChange] - Callback function triggered when checkbox state changes
* @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled
* @param {boolean} [props.alignSelf] - Whether the checkbox label should be positioned on flex-start.
*
* @returns {React.ReactElement} Rendered Checkbox component
*
@@ -42,6 +43,7 @@ import "./index.css";
* isChecked={isAdvanced}
* isDisabled={!canModify}
* onChange={handleAdvancedToggle}
* alignSelf = {alignSelf}
* />
*/
const Checkbox = ({
@@ -53,10 +55,12 @@ const Checkbox = ({
value,
onChange,
isDisabled,
alignSelf
}) => {
/* TODO move sizes to theme */
const sizes = { small: "14px", medium: "16px", large: "18px" };
const theme = useTheme();
const override = alignSelf? { alignSelf: "flex-start" } : {}
return (
<FormControlLabel
className="checkbox-wrapper"
@@ -75,7 +79,7 @@ const Checkbox = ({
sx={{
"&:hover": { backgroundColor: "transparent" },
"& svg": { width: sizes[size], height: sizes[size] },
alignSelf: "flex-start",
...override
}}
/>
}
@@ -114,6 +118,7 @@ Checkbox.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
isDisabled: PropTypes.bool,
alignSelf: PropTypes.bool,
};
export default Checkbox;

View File

@@ -11,11 +11,15 @@ import { checkImage } from "../../../Utils/fileUtils";
* @param {string} props.id - The unique identifier for the input field.
* @param {string} props.src - The URL of the image to display.
* @param {function} props.onChange - The function to handle file input change.
* @param {string} props.isRound - The shape of the image to display.
* @returns {JSX.Element} The rendered component.
*/
const ImageField = ({ id, src, loading, onChange }) => {
const ImageField = ({ id, src, loading, onChange, error, isRound = true }) => {
const theme = useTheme();
const error_border_style = error ? { borderColor: theme.palette.error.main } : {};
const roundShape = isRound ? { borderRadius: "50%" } : {};
const [isDragging, setIsDragging] = useState(false);
const handleDragEnter = () => {
@@ -28,7 +32,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
return (
<>
{!checkImage(src) || loading ? (
<>
<>
<Box
className="image-field-wrapper"
mt={theme.spacing(8)}
@@ -46,6 +50,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
...error_border_style
}}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
@@ -62,6 +67,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
cursor: "pointer",
maxWidth: "500px",
minHeight: "175px",
zIndex: 1,
},
"& fieldset": {
padding: 0,
@@ -78,7 +84,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: "-1",
zIndex: 0,
width: "100%",
}}
>
@@ -113,7 +119,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
>
(maximum size: 3MB)
</Typography>
</Stack>
</Stack>
</Box>
<Typography
component="p"
@@ -122,6 +128,19 @@ const ImageField = ({ id, src, loading, onChange }) => {
>
Supported formats: JPG, PNG
</Typography>
{error && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</>
) : (
<Stack
@@ -132,10 +151,10 @@ const ImageField = ({ id, src, loading, onChange }) => {
sx={{
width: "250px",
height: "250px",
borderRadius: "50%",
overflow: "hidden",
backgroundImage: `url(${src})`,
backgroundSize: "cover",
...roundShape,
}}
></Box>
</Stack>
@@ -148,6 +167,7 @@ ImageField.propTypes = {
id: PropTypes.string.isRequired,
src: PropTypes.string,
onChange: PropTypes.func.isRequired,
isRound: PropTypes.bool,
};
export default ImageField;

View File

@@ -1,10 +1,12 @@
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useState } from "react";
import PropTypes from "prop-types";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import Visibility from "@mui/icons-material/Visibility";
export const HttpAdornment = ({ https }) => {
import ReorderRoundedIcon from '@mui/icons-material/ReorderRounded';
import DeleteIcon from "../../../../assets/icons/trash-bin.svg?react";
export const HttpAdornment = ({ https, prefix }) => {
const theme = useTheme();
return (
<Stack
@@ -23,7 +25,7 @@ export const HttpAdornment = ({ https }) => {
color={theme.palette.text.secondary}
sx={{ lineHeight: 1, opacity: 0.8 }}
>
{https ? "https" : "http"}://
{prefix !== undefined ? prefix : https ? "https://" : "http://"}
</Typography>
</Stack>
);
@@ -31,6 +33,7 @@ export const HttpAdornment = ({ https }) => {
HttpAdornment.propTypes = {
https: PropTypes.bool.isRequired,
prefix: PropTypes.string,
};
export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
@@ -63,3 +66,37 @@ PasswordEndAdornment.propTypes = {
fieldType: PropTypes.string,
setFieldType: PropTypes.func,
};
export const ServerStartAdornment = () => {
return (
<InputAdornment position="start">
<ReorderRoundedIcon />
</InputAdornment>
);
};
export const ServerEndAdornment = ({ id, removeItem }) => {
const theme = useTheme();
return (
<InputAdornment position="end">
<IconButton
aria-label="remove server"
onClick={() => removeItem(id)}
sx={{
color: theme.palette.border.dark,
padding: theme.spacing(1),
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
>
<DeleteIcon />
</IconButton>
</InputAdornment>
);
};

View File

@@ -169,7 +169,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
ProgressUpload.propTypes = {
icon: PropTypes.element, // JSX element for the icon (optional)
label: PropTypes.string.isRequired, // Label text for the progress item
label: PropTypes.string, // Label text for the progress item
size: PropTypes.string.isRequired, // Size information for the progress item
progress: PropTypes.number.isRequired, // Current progress value (0-100)
onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button

View File

@@ -846,6 +846,26 @@ class NetworkService {
"https://api.github.com/repos/bluewave-labs/bluewave-uptime/releases/latest"
);
}
async getStatusPageByUrl(config) {
const { url, authToken } = config;
return this.axiosInstance.get(`/status-page/${url}`, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
}
async createStatusPage(config) {
const { url, authToken, data } = config;
return this.axiosInstance.post(`/status-page/${url}`, data, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
}
}
export default NetworkService;

View File

@@ -1,6 +1,16 @@
/**
* Update errors if passed id matches the error.details[0].path, otherwise remove
* the error for the id
* @param {*} prev Previous errors *
* @param {*} id ID of the field whose error is to be either updated or removed
* @param {*} error the error object
* @returns the Update Errors with the specific field with id being either removed or updated
*/
const buildErrors = (prev, id, error) => {
const updatedErrors = { ...prev };
if (error) {
if (error && id == error.details[0].path) {
updatedErrors[id] = error.details[0].message ?? "Validation error";
} else {
delete updatedErrors[id];
@@ -32,7 +42,15 @@ const getTouchedFieldErrors = (validation, touchedErrors) => {
return newErrors;
};
/**
*
* @param {*} form The form object of the submitted form data
* @param {*} validation The Joi validation rules
* @param {*} setErrors The function used to set the local errors
* @returns true if there is no error or false if there is error after validating the form
* the error will be reset to {} if returns false; otherwise the errors object will be set with
* the new value
*/
const hasValidationErrors = (form, validation, setErrors) => {
const { error } = validation.validate(form, {
abortEarly: false,
@@ -48,6 +66,10 @@ const hasValidationErrors = (form, validation, setErrors) => {
"refreshTokenTTL",
"jwtTTL",
"notify-email-list",
"_id",
"__v",
"createdAt",
"updatedAt"
].includes(err.path[0])
) {
newErrors[err.path[0]] = err.message ?? "Validation error";
@@ -66,8 +88,6 @@ const hasValidationErrors = (form, validation, setErrors) => {
newErrors["usage_temperature"] = null;
}
});
console.log("newErrors", newErrors);
if (Object.values(newErrors).some((v) => v)) {
setErrors(newErrors);
return true;
@@ -76,6 +96,7 @@ const hasValidationErrors = (form, validation, setErrors) => {
return false;
}
}
setErrors({});
return false;
};
export { buildErrors, hasValidationErrors, getTouchedFieldErrors };

View File

@@ -135,6 +135,39 @@ const imageValidation = joi.object({
}),
});
const logoImageValidation = joi.object({
type: joi.string().valid("image/jpeg", "image/png").messages({
"any.only": "Invalid file format.",
"string.empty": "File type required.",
}),
size: joi
.number()
.max(800*800)
.messages({
"number.base": "File size must be a number.",
"number.max": "File size must be less than 640000 pixels.",
"number.empty": "File size required.",
}),
});
const publicPageGeneralSettingsValidation = joi.object({
publish: joi.bool(),
companyName: joi
.string()
.trim()
.messages({ "string.empty": "Company name is required." }),
url: joi.string().trim().messages({ "string.empty": "URL is required." }),
timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }),
color: joi.string().trim().messages({ "string.empty": "Color is required." }),
theme: joi.string(),
monitors: joi.array().min(1).items(joi.string().required()).messages({
"string.pattern.base": "Must be a valid monitor ID",
"array.base": "Monitors must be an array",
"array.empty": "At least one monitor is required",
"any.required": "Monitors are required",
}),
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
"string.empty": "TTL is required",