Merge branch 'master' into add-product-logo

This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-07-13 16:19:59 -07:00
commit c9485530fc
61 files changed
+1143 -1971

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
language: "en-CA"
tone_instructions: "Tale as old as time (ooh-ooh) Tune as old as song Bittersweet and strange Finding you can change Learning you were wrong Certain as the sun Rising in the east Tale as old as time Song as old as rhyme Beauty and the beast"
early_access: false
reviews:
profile: "assertive"
request_changes_workflow: false
high_level_summary: false
poem: false
review_status: true
auto_review:
enabled: true
drafts: false
chat:
auto_reply: false
+3
View File
@@ -20,6 +20,8 @@ import NewPasswordConfirmed from "./Pages/NewPasswordConfirmed";
import ProtectedRoute from "./Components/ProtectedRoute";
import Details from "./Pages/Details";
import Maintenance from "./Pages/Maintenance";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
function App() {
return (
@@ -92,6 +94,7 @@ function App() {
element={<NewPasswordConfirmed />}
/>
</Routes>
<ToastContainer />
</>
);
}
+3 -5
View File
@@ -1,9 +1,7 @@
.alert {
margin: 0;
width: fit-content;
padding: var(--env-var-spacing-1-plus);
}
.alert, .alert button{
font-size: var(--env-var-font-size-medium);
}
.alert .MuiStack-root {
max-width: 85%;
}
}
+66 -3
View File
@@ -1,10 +1,12 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import { Box, IconButton, Stack } from "@mui/material";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
import CloseIcon from "@mui/icons-material/Close";
import "./index.css";
import Button from "../Button";
/**
* Icons mapping for different alert variants.
@@ -22,10 +24,13 @@ const icons = {
* @param {'info' | 'error' | 'warning'} props.variant - The type of alert.
* @param {string} [props.title] - The title of the alert.
* @param {string} [props.body] - The body text of the alert.
* @param {boolean} [props.isToast] - Indicates if the alert is used as a toast notification.
* @param {boolean} [props.hasIcon] - Whether to display an icon in the alert.
* @param {function} props.onClick - Toast dismiss function.
* @returns {JSX.Element}
*/
const Alert = ({ variant, title, body }) => {
const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
const theme = useTheme();
const { bg, border, color } = theme.alert[variant];
const icon = icons[variant];
@@ -34,19 +39,67 @@ const Alert = ({ variant, title, body }) => {
<Stack
direction="row"
justifyContent="flex-start"
alignItems={hasIcon ? "" : "center"}
className="alert row-stack"
gap={theme.gap.ml}
sx={{
padding: hasIcon ? theme.gap.ml : `${theme.gap.small} ${theme.gap.ml}`,
backgroundColor: bg,
border: `solid 1px ${border}`,
borderRadius: `${theme.shape.borderRadius}px`,
}}
>
{icon && <Box sx={{ color: color }}>{icon}</Box>}
{hasIcon && <Box sx={{ color: color }}>{icon}</Box>}
<Stack direction="column" gap="2px" sx={{ flex: 1, color: color }}>
{title && <Box sx={{ fontWeight: "700" }}>{title}</Box>}
{body && <Box sx={{ fontWeight: "400" }}>{body}</Box>}
{hasIcon && isToast && (
<Button
level="tertiary"
label="Dismiss"
onClick={onClick}
sx={{
fontWeight: "600",
width: "fit-content",
mt: theme.gap.small,
padding: 0,
minWidth: 0,
"&:hover": {
backgroundColor: "transparent",
},
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
></Button>
)}
</Stack>
{isToast && (
<IconButton
onClick={onClick}
sx={{
alignSelf: "flex-start",
ml: "auto",
mr: "-5px",
mt: hasIcon ? "-5px" : 0,
padding: "5px",
"&:focus": {
outline: "none",
},
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
>
<CloseIcon
sx={{
fontSize: "20px",
}}
/>
</IconButton>
)}
</Stack>
);
};
@@ -55,6 +108,16 @@ Alert.propTypes = {
variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired,
title: PropTypes.string,
body: PropTypes.string,
isToast: PropTypes.bool,
hasIcon: PropTypes.bool,
onClick: function (props, propName, componentName) {
if (props.isToast && !props[propName]) {
return new Error(
`Prop '${propName}' is required when 'isToast' is true in '${componentName}'.`
);
}
return null;
},
};
export default Alert;
Whitespace-only changes.
+146
View File
@@ -0,0 +1,146 @@
import {
TableContainer,
Paper,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
TablePagination,
} from "@mui/material";
import React, { useState } from "react";
import PropTypes from "prop-types";
/**
* BasicTable Component
* Renders a table with optional pagination.
*
* @component
* @param {Object} props - Component props.
* @param {Array} props.headers - Array of header titles for the table.
* @param {Array} props.rowItems - Array of row data objects for the table.
* @param {boolean} props.paginated - Flag to enable pagination.
*
* @example
*
* Headers is an array of objets that have a unique ID and a name
* Rows is an array of objects that have a unique ID and some data to display
* Row data can be any valid React node
* Rows are split into groups according to the number of headers, so if you have 3 headers then
* Rows will go under the index % 3 column.
* ie: Row 0 -> col 0 % 3 = 0
* ie: Row 1 -> col 1 % 3 = 1
* ie: Row 2 -> col 2 % 3 = 2
* ie: Row 3 -> col 3 % 3 = 0
*
* Thus items are put in their correct columns
*
*
* const headers = [
* { id: 1, name: "ID" },
* { id: 2, name: "Name" },
* { id: 3, name: "Age" },
* ];
* const rows = [
* { id: 1, data: <span>999</span> },
* { id: 2, data: <div>Alex Holliday</div> },
* { id: 3, data: <div>5</div> },
* // More rows...
* ];
* <BasicTable headers={headers} rows={rows} paginated={true} />
*/
const BasicTable = ({ headers, rowItems, paginated }) => {
// Add headers to props validation
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0); // Reset to first page after changing rows per page
};
const numOfCols = headers.length;
const groupedRowItems = rowItems.reduce((acc, item, idx) => {
const rowIndex = Math.floor((item.id - 1) / numOfCols);
if (!acc[rowIndex]) acc[rowIndex] = { items: [], idx };
acc[rowIndex].items.push({ item });
return acc;
}, {});
const paginatedRowsKeys = Object.keys(groupedRowItems).slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
);
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
{headers.map((header) => {
return <TableCell key={header.id}>{header.name}</TableCell>;
})}
</TableRow>
</TableHead>
<TableBody>
{paginated === false &&
Object.keys(groupedRowItems).map((key) => {
return (
<TableRow key={groupedRowItems[key].idx}>
{groupedRowItems[key].items.map(({ item }) => {
return <TableCell key={item.id}>{item.data}</TableCell>;
})}
</TableRow>
);
})}
{paginated === true &&
paginatedRowsKeys.map((key) => (
<TableRow key={groupedRowItems[key].idx}>
{groupedRowItems[key].items.map(({ item }) => (
<TableCell key={item.id}>{item.data}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{paginated === true && (
<TablePagination
component="div"
count={Object.keys(groupedRowItems).length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[1, 5, 10]}
/>
)}
</>
);
};
BasicTable.propTypes = {
headers: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
name: PropTypes.string.isRequired,
})
).isRequired,
rowItems: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
data: PropTypes.node,
})
).isRequired,
paginated: PropTypes.bool,
};
export default BasicTable;
@@ -0,0 +1,89 @@
.field {
min-width: 250px;
}
.field h3.MuiTypography-root,
.field h5.MuiTypography-root,
.field input,
.field textarea,
.field .input-error {
font-size: var(--env-var-font-size-medium);
}
.field h3.MuiTypography-root,
.field h5.MuiTypography-root,
.field input,
.field textarea {
color: var(--env-var-color-5);
}
.field h3.MuiTypography-root {
font-weight: 500;
}
.field h3.MuiTypography-root span.field-required {
color: var(--env-var-color-19);
margin-left: 2px;
}
.field h3.MuiTypography-root span.field-optional {
opacity: 0.6;
margin-left: 4px;
}
.field h5.MuiTypography-root {
position: relative;
opacity: 0.8;
padding-right: var(--env-var-spacing-1-minus);
}
.field .MuiInputBase-root:has(input) {
height: 34px;
}
.field .MuiInputBase-root:has(.MuiInputAdornment-root) {
padding-right: var(--env-var-spacing-1-minus);
}
.field .MuiInputBase-root:has(.copy.MuiInputAdornment-root > .MuiButtonBase-root) {
padding-right: 0;
}
.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root {
padding: 0 var(--env-var-spacing-1-minus);
font-size: var(--env-var-font-size-medium);
border-radius: 0;
height: 34px;
}
.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root svg {
width: 20px;
height: 20px;
margin-right: 5px;
}
.field .MuiInputBase-root .MuiInputAdornment-root .MuiButtonBase-root:hover {
border-color: var(--env-var-color-29);
}
.field input {
height: 100%;
padding: 0 var(--env-var-spacing-1-minus);
}
.field .MuiInputBase-root:has(textarea) {
padding: var(--env-var-spacing-1-minus);
}
.field .MuiOutlinedInput-root fieldset {
border-color: var(--env-var-color-29);
border-radius: var(--env-var-radius-1);
}
.field:not(:has(.input-error))
.MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus))
fieldset {
border-color: var(--env-var-color-29);
}
.field .input-error {
color: var(--env-var-color-24);
opacity: 0.8;
}
.field:has(.input-error) .MuiOutlinedInput-root fieldset {
border-color: var(--env-var-color-19);
}
.register-page .field .MuiOutlinedInput-root fieldset,
.register-page .field input,
.login-page .field .MuiOutlinedInput-root fieldset,
.login-page .field input,
.forgot-password-page .field .MuiOutlinedInput-root fieldset,
.forgot-password-page .field input,
.set-new-password-page .field .MuiOutlinedInput-root fieldset,
.set-new-password-page .field input {
border-radius: var(--env-var-radius-2);
}
@@ -0,0 +1,158 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import {
IconButton,
InputAdornment,
Stack,
TextField,
Typography,
} from "@mui/material";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import Visibility from "@mui/icons-material/Visibility";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import Button from "../../Button";
import "./index.css";
import { useState } from "react";
/**
* @param {Object} props
* @param {string} [props.type] - Type of input field (e.g., 'text', 'password').
* @param {string} props.id - ID of the input field.
* @param {string} [props.label] - Label for the input field.
* @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk.
* @param {boolean} [props.isOptional] - Indicates if the field is optional, will display optional text.
* @param {boolean} [props.hasCopy] - Indicates if the field supports copying.
* @param {string} [props.autoComplete] - Autocomplete value for the input field.
* @param {string} [props.placeholder] - Placeholder text for the input field.
* @param {string} props.value - Value of the input field.
* @param {function} props.onChange - Function called on input change.
* @param {string} [props.error] - Error message to display for the input field.
* @param {boolean} [props.disabled] - Indicates if the input field is disabled.
*/
const Field = ({
type = "text",
id,
label,
isRequired,
isOptional,
hasCopy,
autoComplete,
placeholder,
value,
onChange,
error,
disabled,
}) => {
const theme = useTheme();
// TODO - are we using this feature anywhere ?
const [copy, setCopy] = useState(false);
const handleCopy = () => {
setCopy(true);
setTimeout(() => setCopy(false), 1000);
};
const [isVisible, setVisible] = useState(false);
return (
<Stack gap={theme.gap.xs} className={`field field-${type}`}>
{label && (
<Typography component="h3">
{label}
{isRequired ? <span className="field-required">*</span> : ""}
{isOptional ? <span className="field-optional">(optional)</span> : ""}
</Typography>
)}
<TextField
type={type === "password" ? (isVisible ? "text" : type) : type}
id={id}
autoComplete={autoComplete}
placeholder={placeholder}
multiline={type === "description"}
rows={type === "description" ? 4 : 1}
value={value}
onChange={onChange}
disabled={disabled}
InputProps={{
startAdornment: type === "url" && (
<Stack
direction="row"
alignItems="center"
height="100%"
sx={{
borderRight: `solid 1px ${theme.palette.section.borderColor}`,
}}
>
<Typography component="h5">https://</Typography>
</Stack>
),
endAdornment:
type === "password" ? (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setVisible((show) => !show)}
tabIndex={-1}
sx={{
color: theme.palette.section.borderColor,
padding: `calc(${theme.gap.xs} / 2)`,
"&:focus": {
outline: "none",
},
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
>
{!isVisible ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
) : (
hasCopy && (
<InputAdornment className="copy" position="end">
<Button
level="tertiary"
label={copy ? "Copied" : "Copy"}
img={<ContentCopyIcon />}
onClick={handleCopy}
sx={{
borderLeft: `solid 1px ${theme.palette.section.borderColor}`,
lineHeight: 0,
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
/>
</InputAdornment>
)
),
}}
/>
{error && (
<Typography component="span" className="input-error" mt={theme.gap.xs}>
{error}
</Typography>
)}
</Stack>
);
};
Field.propTypes = {
type: PropTypes.oneOf(["text", "password", "url", "email", "description"]),
id: PropTypes.string.isRequired,
label: PropTypes.string,
isRequired: PropTypes.bool,
isOptional: PropTypes.bool,
hasCopy: PropTypes.bool,
autoComplete: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
error: PropTypes.string,
disabled: PropTypes.bool,
};
export default Field;
+35 -108
View File
@@ -1,23 +1,8 @@
import "./index.css";
import { useState } from "react";
import PropTypes from "prop-types";
import ResponseTimeChart from "../Charts/ResponseTimeChart";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Pagination,
PaginationItem,
} from "@mui/material";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import BasicTable from "../BasicTable";
import OpenInNewPage from "../../assets/icons/open-in-new-page.svg?react";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
import { useNavigate } from "react-router-dom";
/**
* Host component.
@@ -95,100 +80,42 @@ const Status = ({ params }) => {
* @returns {React.Component} Returns a table with the monitor data.
*/
const MonitorTable = ({ monitors = [] }) => {
const navigate = useNavigate();
const [page, setPage] = useState(0);
const rowsPerPage = 5;
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const mappedRows = monitors
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((monitor) => {
const params = {
url: monitor.url,
title: monitor.name,
precentage: 100,
percentageColor:
monitor.status === true
? "var(--env-var-color-17)"
: "var(--env-var-color-19)",
status: monitor.status === true ? "up" : "down",
backgroundColor:
monitor.status === true
? "var(--env-var-color-20)"
: "var(--env-var-color-21)",
statusDotColor:
monitor.status === true
? "var(--env-var-color-17)"
: "var(--env-var-color-19)",
};
return (
<TableRow
sx={{ cursor: "pointer" }}
key={monitor._id}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
}}
>
<TableCell>
<Host params={params} />
</TableCell>
<TableCell>
<Status params={params} />
</TableCell>
<TableCell>
<ResponseTimeChart checks={monitor.checks} />
</TableCell>
<TableCell>TODO Add Actions</TableCell>
</TableRow>
);
const headers = [
{ id: 1, name: "Host" },
{ id: 2, name: "Status" },
{ id: 3, name: "Response Time" },
{ id: 4, name: "Actions" },
];
const data = [];
monitors.forEach((monitor) => {
const params = {
url: monitor.url,
title: monitor.name,
precentage: 100,
percentageColor:
monitor.status === true
? "var(--env-var-color-17)"
: "var(--env-var-color-19)",
status: monitor.status === true ? "up" : "down",
backgroundColor:
monitor.status === true
? "var(--env-var-color-20)"
: "var(--env-var-color-21)",
statusDotColor:
monitor.status === true
? "var(--env-var-color-17)"
: "var(--env-var-color-19)",
};
data.push({ id: data.length + 1, data: <Host params={params} /> });
data.push({ id: data.length + 1, data: <Status params={params} /> });
data.push({
id: data.length + 1,
data: <ResponseTimeChart checks={monitor.checks} />,
});
data.push({ id: data.length + 1, data: "TODO" });
});
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Host</TableCell>
<TableCell>
Status
<span>
<ArrowDownwardRoundedIcon />
</span>
</TableCell>
<TableCell>Response Time</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>{mappedRows}</TableBody>
</Table>
</TableContainer>
<Pagination
count={Math.ceil(monitors?.length / rowsPerPage)}
page={page + 1}
onChange={(event, value) => handleChangePage(event, value - 1)}
shape="rounded"
renderItem={(item) => (
<PaginationItem
slots={{
previous: ArrowBackRoundedIcon,
next: ArrowForwardRoundedIcon,
}}
{...item}
sx={{
"&:focus": {
outline: "none",
},
}}
/>
)}
/>
</>
);
return <BasicTable headers={headers} rowItems={data} paginated={true} />;
};
MonitorTable.propTypes = {
Whitespace-only changes.
+114
View File
@@ -0,0 +1,114 @@
import React from 'react';
import PropTypes from 'prop-types';
import { styled, useTheme } from '@mui/material/styles';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Pagination from '../../Components/Pagination';
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: theme.palette.action.hover,
color: theme.palette.common.black,
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
},
}));
const StyledTableRow = styled(TableRow)(({ theme }) => ({
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.action.hover,
},
'&:nth-of-type(even)': {
backgroundColor: theme.palette.common.white,
},
'&:last-child td, &:last-child th': {
border: 0,
},
}));
const itemsPerPage = 5; // Number of items per page
export default function StatusTable({ data, columns }) {
const theme = useTheme();
const [currentPage, setCurrentPage] = React.useState(1);
const onPageChange = (page) => {
setCurrentPage(page);
};
// Calculate total pages based on data length and items per page
const totalPages = Math.ceil(data.length / itemsPerPage);
// Slice data based on current page and items per page
const paginatedData = data.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
return (
<Box
sx={{
marginTop: theme.spacing(4),
marginX: 'auto',
width: '80%',
paddingX: theme.spacing(6),
[theme.breakpoints.down('md')]: {
width: '100%',
},
}}
>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 700 }} aria-label="customized table">
<TableHead>
<TableRow>
{columns.map((column, index) => (
<StyledTableCell key={index} align={column.align || 'left'}>
{column.header}
</StyledTableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{paginatedData.map((row, rowIndex) => (
<StyledTableRow key={rowIndex}>
{columns.map((column, colIndex) => (
<StyledTableCell key={colIndex} align={column.align || 'left'}>
{column.id === 'name' ? (
<div style={row.name.props.customStyles}>
{row.name}
</div>
) : (
row[column.id]
)}
</StyledTableCell>
))}
</StyledTableRow>
))}
</TableBody>
</Table>
</TableContainer>
{totalPages > 1 && (
<Pagination
page={currentPage}
count={totalPages}
onPageChange={onPageChange}
/>
)}
</Box>
);
}
StatusTable.propTypes = {
data: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
header: PropTypes.node.isRequired,
align: PropTypes.oneOf(['left', 'right', 'center']),
})
).isRequired,
};
@@ -3,11 +3,12 @@ import React, { useState } from "react";
import { useTheme } from "@emotion/react";
import { Box, Divider, Stack, Typography } from "@mui/material";
import ButtonSpinner from "../../ButtonSpinner";
import PasswordTextField from "../../TextFields/Password/PasswordTextField";
import Field from "../../Inputs/Field";
import { editPasswordValidation } from "../../../Validation/validation";
import Alert from "../../Alert";
import { update } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../../Utils/toastUtils";
/**
* PasswordPanel component manages the form for editing password.
@@ -34,7 +35,6 @@ const PasswordPanel = () => {
confirm: "",
});
const [errors, setErrors] = useState({});
const [showPassword, setShowPassword] = useState(false); // password visibility
const handleChange = (event) => {
const { value, id } = event.target;
@@ -78,15 +78,23 @@ const PasswordPanel = () => {
} else {
const action = await dispatch(update({ authToken, localData }));
if (action.payload.success) {
// TODO: Add toast/notification for password update success
createToast({
variant: "info",
body: "Your password was changed successfully.",
hasIcon: false,
});
setLocalData({
password: "",
newPassword: "",
confirm: "",
});
} else {
// TODO: Add toast/notification for password update rejected
// TODO: Check for other errors?
createToast({
variant: "info",
body: "Your password input was incorrect.",
hasIcon: false,
});
setErrors({ password: "*" + action.payload.msg + "." });
}
}
@@ -111,76 +119,43 @@ const PasswordPanel = () => {
<Stack>
<Typography component="h1">Current password</Typography>
</Stack>
<Stack>
<PasswordTextField
id="edit-current-password"
label={null}
placeholder="Enter your current password"
autoComplete="current-password"
visibility={showPassword}
setVisibility={setShowPassword}
value={localData.password}
onChange={handleChange}
error={errors[idToName["edit-current-password"]] ? true : false}
/>
{errors[idToName["edit-current-password"]] ? (
<Typography component="p" className="input-error">
{errors[idToName["edit-current-password"]]}
</Typography>
) : (
""
)}
</Stack>
<Field
type="password"
id="edit-current-password"
placeholder="Enter your current password"
autoComplete="current-password"
value={localData.password}
onChange={handleChange}
error={errors[idToName["edit-current-password"]]}
/>
</div>
<div className="edit-password-form__wrapper">
<Stack>
<Typography component="h1">New password</Typography>
</Stack>
<Stack>
<PasswordTextField
id="edit-new-password"
label={null}
placeholder="Enter your new password"
autoComplete="new-password"
visibility={showPassword}
setVisibility={setShowPassword}
value={localData.newPassword}
onChange={handleChange}
error={errors[idToName["edit-new-password"]] ? true : false}
/>
{errors[idToName["edit-new-password"]] ? (
<Typography component="p" className="input-error">
{errors[idToName["edit-new-password"]]}
</Typography>
) : (
""
)}
</Stack>
<Field
type="password"
id="edit-new-password"
placeholder="Enter your new password"
autoComplete="new-password"
value={localData.newPassword}
onChange={handleChange}
error={errors[idToName["edit-new-password"]]}
/>
</div>
<div className="edit-password-form__wrapper">
<Stack>
<Typography component="h1">Confirm new password</Typography>
</Stack>
<Stack>
<PasswordTextField
id="edit-confirm-password"
label={null}
placeholder="Reenter your new password"
autoComplete="new-password"
visibility={showPassword}
setVisibility={setShowPassword}
value={localData.confirm}
onChange={handleChange}
error={errors[idToName["edit-confirm-password"]] ? true : false}
/>
{errors[idToName["edit-confirm-password"]] ? (
<Typography component="p" className="input-error">
{errors[idToName["edit-confirm-password"]]}
</Typography>
) : (
""
)}
</Stack>
<Field
type="password"
id="edit-confirm-password"
placeholder="Reenter your new password"
autoComplete="new-password"
value={localData.confirm}
onChange={handleChange}
error={errors[idToName["edit-confirm-password"]]}
/>
</div>
<div className="edit-password-form__wrapper">
<Stack></Stack>
@@ -4,9 +4,9 @@ import TabPanel from "@mui/lab/TabPanel";
import { Box, Divider, Modal, Stack, Typography } from "@mui/material";
import ButtonSpinner from "../../ButtonSpinner";
import Button from "../../Button";
import EmailTextField from "../../TextFields/Email/EmailTextField";
import StringTextField from "../../TextFields/Text/TextField";
import Avatar from "../../Avatar";
import Field from "../../Inputs/Field";
import ImageField from "../../Inputs/Image";
import {
editProfileValidation,
imageValidation,
@@ -17,11 +17,11 @@ import {
deleteUser,
update,
} from "../../../Features/Auth/authSlice";
import ImageField from "../../TextFields/Image";
import ImageIcon from "@mui/icons-material/Image";
import ProgressUpload from "../../ProgressBars";
import { formatBytes } from "../../../Utils/fileUtils";
import { clearMonitorState } from "../../../Features/Monitors/monitorsSlice";
import { createToast } from "../../../Utils/toastUtils";
/**
* ProfilePanel component displays a form for editing user profile information
@@ -58,6 +58,7 @@ const ProfilePanel = () => {
// Handles input field changes and performs validation
const handleChange = (event) => {
errors["unchanged"] && clearError("unchanged");
const { value, id } = event.target;
const name = idToName[id];
setLocalData((prev) => ({
@@ -85,7 +86,7 @@ const ProfilePanel = () => {
delete: false,
});
//TODO - potentitally remove once image compression functions are implemented
//TODO - potentitally remove, will revisit in the future
intervalRef.current = setInterval(() => {
const buffer = 12;
setProgress((prev) => {
@@ -151,14 +152,11 @@ const ProfilePanel = () => {
deleteProfileImage: false,
}));
setIsOpen("");
errors["unchanged"] && clearError("unchanged");
};
console.log(
localData.deleteProfileImage === false && localData.file === undefined
);
// Handles form submission to update user profile
const handleSaveProfile = (event) => {
const handleSaveProfile = async (event) => {
event.preventDefault();
if (
localData.firstname === user.firstname &&
@@ -166,12 +164,29 @@ const ProfilePanel = () => {
localData.deleteProfileImage === undefined &&
localData.file === undefined
) {
// TODO: Add toast/notification for unchanged profile data
createToast({
variant: "warning",
body: "Unable to update profile: No changes detected.",
hasIcon: false,
});
setErrors({ unchanged: "unable to update profile" });
return;
}
dispatch(update({ authToken, localData }));
// TODO: Add toast/notification for profile update success
const action = await dispatch(update({ authToken, localData }));
if (action.payload.success) {
createToast({
variant: "info",
body: "Your profile data was changed successfully.",
hasIcon: false,
});
} else {
createToast({
variant: "error",
body: "There was an error updating your profile data.",
hasIcon: false,
});
}
};
// Removes current profile image from UI
@@ -180,6 +195,7 @@ const ProfilePanel = () => {
...prev,
deleteProfileImage: true,
}));
errors["unchanged"] && clearError("unchanged");
};
// Initiates the account deletion process
@@ -202,47 +218,27 @@ const ProfilePanel = () => {
<Stack>
<Typography component="h1">First name</Typography>
</Stack>
<Stack>
<StringTextField
id="edit-first-name"
label={null}
value={localData.firstname}
placeholder="Enter your first name"
autoComplete="given-name"
onChange={handleChange}
error={errors[idToName["edit-first-name"]] ? true : false}
/>
{errors[idToName["edit-first-name"]] ? (
<Typography component="p" className="input-error">
{errors[idToName["edit-first-name"]]}
</Typography>
) : (
""
)}
</Stack>
<Field
id="edit-first-name"
value={localData.firstname}
placeholder="Enter your first name"
autoComplete="given-name"
onChange={handleChange}
error={errors[idToName["edit-first-name"]]}
/>
</div>
<div className="edit-profile-form__wrapper">
<Stack>
<Typography component="h1">Last name</Typography>
</Stack>
<Stack>
<StringTextField
id="edit-last-name"
label={null}
value={localData.lastname}
placeholder="Enter your last name"
autoComplete="family-name"
onChange={handleChange}
error={errors[idToName["edit-last-name"]] ? true : false}
/>
{errors[idToName["edit-last-name"]] ? (
<Typography component="p" className="input-error">
{errors[idToName["edit-last-name"]]}
</Typography>
) : (
""
)}
</Stack>
<Field
id="edit-last-name"
placeholder="Enter your last name"
autoComplete="family-name"
value={localData.lastname}
onChange={handleChange}
error={errors[idToName["edit-last-name"]]}
/>
</div>
<div className="edit-profile-form__wrapper">
<Stack>
@@ -251,25 +247,15 @@ const ProfilePanel = () => {
This is your current email address — it cannot be changed.
</Typography>
</Stack>
<Stack>
<EmailTextField
id="edit-email"
label={null}
value={user.email}
placeholder="Enter your email"
autoComplete="email"
// onChange={handleChange}
// error={errors[idToName["edit-email"]] ? true : false}
disabled={true}
/>
{errors[idToName["edit-email"]] ? (
<Typography component="p" className="input-error">
{errors[idToName["edit-email"]]}
</Typography>
) : (
""
)}
</Stack>
<Field
id="edit-email"
value={user.email}
placeholder="Enter your email"
autoComplete="email"
// onChange={handleChange}
// error={errors[idToName["edit-email"]]}
disabled={true}
/>
</div>
<div className="edit-profile-form__wrapper">
<Stack>
@@ -1,49 +0,0 @@
import React from "react";
import "./descriptionTextField.css";
import { TextField } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
/**
* @component
* @param {Object} props
* @param {function} props.onChange - The function to call when the text field changes (optional)
* @param {string} props.hintText - The hint text displayed within the text field (required)
* @param {boolean} props.hasError - A flag indicating if the text field has an error (required)
* @returns {JSX.Element} - Renders the description text field component
*/
const DescriptionTextField = ({ onChange, hintText, hasError }) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
return (
<div
className="description-field-holder"
style={{ fontFamily: fontFamily }}
>
<div className="text-field-title">Description</div>
<TextField
onChange={onChange}
id="description-field-area"
error={hasError}
className="description-field-area"
multiline
rows={4}
placeholder="Enter a description..."
helperText={hintText}
/>
</div>
);
};
DescriptionTextField.propTypes = {
hintText: PropTypes.string.isRequired,
hasError: PropTypes.bool.isRequired,
};
export default DescriptionTextField;
@@ -1,77 +0,0 @@
:root {
--env-var-color-1: #101828;
--env-var-color-2: #475467;
--env-var-color-3: #1570ef;
--env-var-color-4: #d0d5dd;
--env-var-color-5: #344054;
--env-var-color-6: #eaecf0;
--env-var-color-7: #7f56d9;
--env-var-color-8: #fff;
--env-var-color-9: #f2f2f2;
--env-var-color-10: #175cd3;
--env-var-radius-1: 4px;
--env-var-radius-2: 8px;
--env-var-width-1: 100vw;
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
--env-var-spacing-1: 12px;
--env-var-spacing-2: 24px;
--env-var-spacing-3: 32px;
--env-var-spacing-4: 40px;
--env-var-font-size-small: 11px;
--env-var-font-size-medium: 13px;
--env-var-font-size-medium-plus: 14px;
--env-var-font-size-large: 16px;
--env-var-font-size-xlarge: 30px;
--env-var-img-width-1: 20px;
--env-var-default-1: 24px;
--env-var-default-2: 40px;
}
.description-field-holder {
margin: 10px 20px;
margin-top: 20px;
}
.text-field-title {
color: var(--env-var-color-5);
font-weight: bold;
margin-bottom: 10px;
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
font-weight: bold;
}
.description-field-area {
min-width: 320px;
font-size: var(--env-var-font-size-medium);
border-radius: 8px;
}
.description-field-label {
margin-top: 10px;
font-size: var(--env-var-font-size-small);
}
.with-error {
color: red;
}
#description-field-area-helper-text {
margin-left: 0;
margin-top: 10px;
font-size: var(--env-var-font-size-small);
}
#description-field-area {
min-width: 320px;
font-size: var(--env-var-font-size-medium);
}
@@ -1,106 +0,0 @@
import React, { useState } from "react";
import "./emailTextField.css";
import { InputAdornment, TextField } from "@mui/material";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import { useTheme } from "@mui/material";
/**
* `EmailTextField` is a functional React component that renders a text field for email input.
*
* @component
* @param {Object} props - The properties that define the `EmailTextField` component.
* @param {function} props.onChange - The function to call when the text field changes.
* @param {string} props.id - The id of the text field.
* @param {string} [props.label="Email"] - The label of the text field.
* @param {string} props.value - The value of the text field.
* @param {string} props.variant - The variant of the text field.
* @param {string} props.placeholder - The placeholder of the text field.
* @param {React.Element} props.icon - The icon to be displayed in the text field.
* @param {string} props.helperText - The helper text to be displayed below the text field.
* @param {boolean} props.error - If true, the text field will indicate an error state.
* @param {boolean} props.disabled - If true, the text field will be disabled and cannot be interacted with.
*
* @example
* // To use this component, import it and use it in your JSX like this:
* <EmailTextField id="email" variant="outlined" placeholder="Enter your email" />
*
* @returns {React.Element} The `EmailTextField` component with a text field for email input.
*/
const EmailTextField = ({
onChange,
id,
label = "Email",
value = undefined,
variant,
placeholder,
autoComplete,
icon,
helperText,
error,
disabled
}) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
const [showIcon, setShowIcon] = useState(false);
const handleMouseEnter = () => setShowIcon(true);
const handleMouseLeave = () => setShowIcon(false);
return (
<div style={{ fontFamily: fontFamily }}>
<div className="email-text-field-title">{label}</div>
<div className="email-text-field">
<TextField
onChange={onChange}
error={error}
className="email-text-field-input"
id={id}
value={value}
variant={variant}
placeholder={placeholder}
autoComplete={autoComplete}
disabled={disabled}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<div className="icon-holder">
{error && showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{error && !showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{!error && showIcon && !disabled && (
<HelpOutlineIcon
className="text-field-icon"
style={{ width: "16px", height: "16px" }}
/>
)}
</div>
</InputAdornment>
),
}}
helperText={helperText}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
</div>
</div>
);
};
export default EmailTextField;
@@ -1,77 +0,0 @@
:root {
--env-var-color-1: #101828;
--env-var-color-2: #475467;
--env-var-color-3: #1570ef;
--env-var-color-4: #d0d5dd;
--env-var-color-5: #344054;
--env-var-color-6: #eaecf0;
--env-var-color-7: #7f56d9;
--env-var-color-8: #fff;
--env-var-color-9: #f2f2f2;
--env-var-color-10: #175cd3;
--env-var-radius-1: 4px;
--env-var-radius-2: 8px;
--env-var-width-1: 100vw;
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
--env-var-spacing-1: 12px;
--env-var-spacing-2: 24px;
--env-var-spacing-3: 32px;
--env-var-spacing-4: 40px;
--env-var-font-size-small: 11px;
--env-var-font-size-medium: 13px;
--env-var-font-size-medium-plus: 14px;
--env-var-font-size-large: 16px;
--env-var-font-size-xlarge: 30px;
--env-var-img-width-1: 20px;
--env-var-default-1: 24px;
--env-var-default-2: 40px;
}
.email-text-field {
position: relative;
margin-top: 10px;
}
#outlined-basic {
width: 320px;
height: 14px;
font-size: var(--env-var-font-size-medium);
padding: 8px 12px;
}
.email-text-field-title {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
font-weight: bold;
}
.email-text-field-input {
font-size: var(--env-var-font-size-medium);
}
#outlined-basic-helper-text {
margin-left: 0;
margin-top: 10px;
font-size: var(--env-var-font-size-small);
}
.text-field-icon {
width: 16px;
height: 16px;
}
.icon-holder {
width: 16px;
height: 16px;
padding: 0;
margin: 0;
}
@@ -1,59 +0,0 @@
import PropTypes from "prop-types";
import { Stack, Typography } from "@mui/material";
import "./flexibileTextField.css";
import { useTheme } from "@emotion/react";
/**
* @param {Object} props - The component props.
* @param {string} props.id - The ID of the input field.
* @param {string} props.title - The title or label for the input field.
* @param {string} props.type - The type of the input field (e.g., 'text', 'password').
* @param {string} props.placeholder - The placeholder text for the input field.
* @param {string} props.value - The current value of the input field.
* @param {function} props.onChange - The function to call when the input field value changes.
* @param {string} [props.error] - Optional. Error message to display if there's an error with the input.
* @returns {JSX.Element} JSX Element representing the flexible text input field.
*/
const FlexibileTextField = ({
id,
title,
type,
placeholder,
value,
onChange,
error,
}) => {
const theme = useTheme();
return (
<Stack className="flexible-textfield" gap={theme.gap.small}>
<Typography component="h2">{title}</Typography>
<input
id={id}
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
/>
{error ? (
<Typography component="p" className="input-error">
{error}
</Typography>
) : (
""
)}
</Stack>
);
};
FlexibileTextField.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
placeholder: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
onChange: PropTypes.func.isRequired,
error: PropTypes.string,
};
export default FlexibileTextField;
@@ -1,30 +0,0 @@
.flexible-textfield{
position: relative;
}
.flexible-textfield h2.MuiTypography-root{
color: var(--env-var-color-5);
font-size: var(--env-var-font-size-medium);
font-weight: 400;
}
.flexible-textfield h2.MuiTypography-root span{
opacity: 0.6;
}
.flexible-textfield input {
min-width: 300px;
height: 34px;
padding: 0 var(--env-var-spacing-1);
border: 1px solid var(--env-var-color-4);
border-radius: var(--env-var-radius-1);
}
.flexible-textfield p.MuiTypography-root.input-error {
color: var(--env-var-color-24);
opacity: 0.8;
position: absolute;
top: calc(100% + 5px);
}
.flexible-textfield:has(p.MuiTypography-root.input-error) input{
border-color: var(--env-var-color-24);
}
.flexible-textfield input:focus {
outline: none;
}
@@ -1,119 +0,0 @@
import "./passwordTextField.css";
import React, { useState } from "react";
import { IconButton, InputAdornment, TextField } from "@mui/material";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import { useTheme } from "@mui/material";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import Visibility from "@mui/icons-material/Visibility";
/**
* @component
* @param {Object} props
* @param {string} props.value - The current value of the text field
* @param {function} props.onChange - The function to call when the text field changes (optional)
* @param {string} props.id - Unique ID for the text field (optional)
* @param {string} props.label - The label text displayed above the text field (optional)
* @param {"standard" | "outlined" | "filled"} props.variant - The variant of the text field (e.g., "outlined") (optional)
* @param {string} props.placeholder - The placeholder text displayed within the text field (optional)
* @param {"error" | "help"} [props.icon] - The type of icon to display (error or help) (optional)
* @param {string} props.helperText - The helper text displayed below the text field (optional)
* @param {boolean} props.error - A flag indicating if the text field has an error (required)
* @param {boolean} props.visibility - A flag indicating if the password visibility is toggled (optional)
* @param {function} props.setVisibility - Function to toggle password visibility (optional)
* @returns {JSX.Element} - Renders the password text field component with dynamic icon display
*/
const PasswordTextField = ({
value,
onChange,
id,
label = "Password",
autoComplete,
variant,
placeholder,
icon,
helperText,
error,
visibility,
setVisibility,
}) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
const [showIcon, setShowIcon] = useState(false);
const handleMouseEnter = () => setShowIcon(true);
const handleMouseLeave = () => setShowIcon(false);
return (
<div style={{ fontFamily: fontFamily }}>
<div className="password-text-field-title">{label}</div>
<div className="password-text-field">
<TextField
value={value}
onChange={onChange}
type={visibility ? "text" : "password"}
autoComplete={autoComplete}
error={error}
className="password-text-field-input"
id={id}
variant={variant}
placeholder={placeholder}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{visibility !== undefined ? (
<IconButton
aria-label="toggle password visibility"
onClick={() => setVisibility((show) => !show)}
tabIndex={-1}
sx={{
width: "30px",
height: "30px",
"&:focus": {
outline: "none",
},
}}
>
{!visibility ? <VisibilityOff /> : <Visibility />}
</IconButton>
) : (
<div className="icon-holder">
{error && showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{error && !showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{!error && showIcon && (
<HelpOutlineIcon
className="text-field-icon"
style={{ width: "16px", height: "16px" }}
/>
)}
</div>
)}
</InputAdornment>
),
}}
helperText={helperText}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
</div>
</div>
);
};
export default PasswordTextField;
@@ -1,73 +0,0 @@
:root {
--env-var-color-1: #101828;
--env-var-color-2: #475467;
--env-var-color-3: #1570ef;
--env-var-color-4: #d0d5dd;
--env-var-color-5: #344054;
--env-var-color-6: #eaecf0;
--env-var-color-7: #7f56d9;
--env-var-color-8: #fff;
--env-var-color-9: #f2f2f2;
--env-var-color-10: #175cd3;
--env-var-radius-1: 4px;
--env-var-radius-2: 8px;
--env-var-width-1: 100vw;
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
--env-var-spacing-1: 12px;
--env-var-spacing-2: 24px;
--env-var-spacing-3: 32px;
--env-var-spacing-4: 40px;
--env-var-font-size-small: 11px;
--env-var-font-size-medium: 13px;
--env-var-font-size-medium-plus: 14px;
--env-var-font-size-large: 16px;
--env-var-font-size-xlarge: 30px;
--env-var-img-width-1: 20px;
--env-var-default-1: 24px;
--env-var-default-2: 40px;
}
.password-text-field {
position: relative;
margin-top: 10px;
}
#outlined-basic {
width: 320px;
height: 14px;
font-size: var(--env-var-font-size-medium);
padding: 8px 12px;
}
.password-text-field-title {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
font-weight: bold;
}
#outlined-basic-helper-text {
margin-left: 0;
margin-top: 10px;
font-size: var(--env-var-font-size-small);
}
.text-field-icon {
width: 16px;
height: 16px;
}
.icon-holder {
width: 16px;
height: 16px;
padding: 0;
margin: 0;
}
@@ -1,94 +0,0 @@
import "./textField.css";
import React, { useState } from "react";
import { InputAdornment, TextField } from "@mui/material";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import { useTheme } from "@mui/material";
/**
* @param {Object} props - The component props.
* @param {function} props.onChange - The function to call when the text field changes.
* @param {string} props.id - The ID of the text field.
* @param {string} props.label - The label text for the text field.
* @param {string} props.value - The value of the text field.
* @param {string} props.variant - The variant of the text field.
* @param {string} props.placeholder - The placeholder text for the text field.
* @param {ReactNode} props.icon - The icon to display in the text field.
* @param {string} props.helperText - The helper text for the text field.
* @param {boolean} props.error - Whether the text field has an error.
* @returns {JSX.Element} The rendered component.
*/
const StringTextField = ({
onChange,
autoComplete,
id,
label,
value = undefined,
variant,
placeholder,
icon,
helperText,
error,
}) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
const [showIcon, setShowIcon] = useState(false);
const handleMouseEnter = () => setShowIcon(true);
const handleMouseLeave = () => setShowIcon(false);
return (
<div style={{ fontFamily: fontFamily }}>
<div className="email-text-field-title">{label}</div>
<div className="email-text-field">
<TextField
onChange={onChange}
error={error}
className="email-text-field-input"
id={id}
variant={variant}
value={value}
placeholder={placeholder}
autoComplete={autoComplete}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<div className="icon-holder">
{error && showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{error && !showIcon && (
<ErrorOutlineIcon
className="text-field-icon"
style={{ fill: "red", width: "16px", height: "16px" }}
/>
)}
{!error && showIcon && (
<HelpOutlineIcon
className="text-field-icon"
style={{ width: "16px", height: "16px" }}
/>
)}
</div>
</InputAdornment>
),
}}
helperText={helperText}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
</div>
</div>
);
};
export default StringTextField;
@@ -1,37 +0,0 @@
:root {
--env-var-color-1: #101828;
--env-var-color-2: #475467;
--env-var-color-3: #1570ef;
--env-var-color-4: #d0d5dd;
--env-var-color-5: #344054;
--env-var-color-6: #eaecf0;
--env-var-color-7: #7f56d9;
--env-var-color-8: #fff;
--env-var-color-9: #f2f2f2;
--env-var-color-10: #175cd3;
--env-var-radius-1: 4px;
--env-var-radius-2: 8px;
--env-var-width-1: 100vw;
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
--env-var-spacing-1: 12px;
--env-var-spacing-2: 24px;
--env-var-spacing-3: 32px;
--env-var-spacing-4: 40px;
--env-var-font-size-small: 11px;
--env-var-font-size-medium: 13px;
--env-var-font-size-medium-plus: 14px;
--env-var-font-size-large: 16px;
--env-var-font-size-xlarge: 30px;
--env-var-img-width-1: 20px;
--env-var-default-1: 24px;
--env-var-default-2: 40px;
}
@@ -1,72 +0,0 @@
import React, { useState } from "react";
import "./websiteTextField.css";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { useTheme } from "@mui/material";
/**
* @component
* @param {Object} props
* @param {boolean} [props.showHelp=true] - Controls the visibility of the help icon (defaults to true)
* @param {boolean} [props.hasCopyButton=false] - Controls the visibility of the copy button (defaults to false)
* @param {string} props.hintText - The hint text displayed below the text field (required)
* @returns {JSX.Element} - Renders the website text field component with optional help and copy features
*/
const WebsiteTextField = ({
showHelp = true,
hasCopyButton = false,
hintText,
}) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
const [copied, setCopied] = useState(false);
const handleCopy = () => {
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
return (
<div style={{ fontFamily: fontFamily }}>
<div className="website-textfield-title">Website</div>
<div className="website-textfield">
<label className="website-label">http://</label>
<input
type="url"
name="website-url"
id="website-url"
className="website-url"
/>
{showHelp && (
<div
className={
`website-icon-holder ` + !hasCopyButton
? "with-no-copy-button"
: ""
}
>
<HelpOutlineIcon style={{ fill: "var(--icon-color)" }} />
</div>
)}
{hasCopyButton && (
<div className="copy-to-clipboard-button" onClick={handleCopy}>
<ContentCopyIcon
className="copy-icon"
style={{ width: "20px", height: "20px" }}
/>
<span className="">{copied ? " Copied " : " Copy "}</span>
</div>
)}
</div>
<label className="website-textfield-hint">{hintText}</label>
</div>
);
};
export default WebsiteTextField;
@@ -1,111 +0,0 @@
:root {
--env-var-color-1: #101828;
--env-var-color-2: #475467;
--env-var-color-3: #1570ef;
--env-var-color-4: #d0d5dd;
--env-var-color-5: #344054;
--env-var-color-6: #eaecf0;
--env-var-color-7: #7f56d9;
--env-var-color-8: #fff;
--env-var-color-9: #f2f2f2;
--env-var-color-10: #175cd3;
--env-var-radius-1: 4px;
--env-var-radius-2: 8px;
--env-var-width-1: 100vw;
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
--env-var-spacing-1: 12px;
--env-var-spacing-2: 24px;
--env-var-spacing-3: 32px;
--env-var-spacing-4: 40px;
--env-var-font-size-small: 11px;
--env-var-font-size-medium: 13px;
--env-var-font-size-medium-plus: 14px;
--env-var-font-size-large: 16px;
--env-var-font-size-xlarge: 30px;
--env-var-img-width-1: 20px;
--env-var-default-1: 24px;
--env-var-default-2: 40px;
}
.website-textfield-title {
margin: 0 20px;
margin-top: 30px;
font-size: 13px;
color: var(--env-var-color-5);
font-weight: bold;
}
.website-textfield {
display: flex;
align-items: center;
position: relative;
margin: 10px 20px 5px;
}
.website-textfield-hint {
margin: 10px 20px;
font-size: 11px;
}
.website-label {
padding: 6px 20px 6px 10px;
border: solid 1px var(--env-var-color-4);
border-right: none;
border-radius: 4px 0 0 4px;
font-size: 13px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
color: var(--env-var-color-2);
}
.website-url {
width: 248px;
height: 31.5px;
padding: 0px 15px;
border: solid 1px var(--env-var-color-4);
}
.website-icon-holder {
width: 40px;
height: 33px;
padding: 5px;
border: solid 1px var(--env-var-color-4);
border-left: none;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.copy-to-clipboard-button {
padding: 5.5px 20px 6px 10px;
border: solid 1px var(--env-var-color-4);
border-left: none;
border-radius: 0 8px 8px 0;
font-size: 13px;
text-align: center;
display: flex;
justify-content: space-evenly;
align-items: center;
color: var(--env-var-color-2);
cursor: pointer;
}
.copy-icon {
margin-right: 5px;
}
.with-no-copy-button {
display: none;
}
@@ -1,71 +0,0 @@
import "./announcementUpdateSubscription.css";
import React from "react";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
/**
* @component
* @param {Object} props
* @param {string} props.title - Title text for the update subscription dialog (required)
* @param {string} props.text - Body text for the update subscription dialog (required)
* @param {string} props.cancel - Text for the negative (cancel) button (required)
* @param {string} props.positive - Text for the positive button (required)
* @param {string} props.header - Title text for the email subscription section (required)
* @param {string} props.button - Text for the subscribe button (required)
* @returns {JSX.Element} - Renders the announcement update subscription component
*/
const AnnouncementUpdateSubscription = ({
title,
text,
cancel,
positive,
header,
button,
}) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
return (
<div className="update-subscription" style={{ fontFamily: fontFamily }}>
<div className="update-subscription-dialog">
<div className="subscription-dialog-title">{title}</div>
<div className="v-spacing-small"></div>
<div className="subscription-dialog-text">{text}</div>
<div className="v-spacing-medium"></div>
<div className="subscription-dialog-controls">
<button className="controls-negative">{cancel}</button>
<button className="controls-positive">{positive}</button>
</div>
<div className="v-spacing-medium"></div>
</div>
<div className="v-spacing-medium"></div>
<div className="update-subscription-title">{header}</div>
<div className="subscription-email">
<input
className="subscription-email-field"
type="text"
placeholder="olivia@untitledui.com"
/>
<button className="subscribe-button">{button}</button>
</div>
<div className="v-spacing-medium"></div>
</div>
);
};
AnnouncementUpdateSubscription.propTypes = {
title: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
cancel: PropTypes.string.isRequired,
positive: PropTypes.string.isRequired,
header: PropTypes.string.isRequired,
button: PropTypes.string.isRequired,
};
export default AnnouncementUpdateSubscription;
@@ -1,86 +0,0 @@
.update-subscription {
margin: 20px;
}
.update-subscription,
.update-subscription-dialog {
max-width: 770px;
border-radius: 4px;
border: 1px solid #eaecf0;
}
.update-subscription-dialog {
padding: 15px 30px;
}
.subscription-dialog-title {
font-size: 1rem;
font-weight: 700;
color: var(--env-var-color-1);
}
.v-spacing-small {
height: 10px;
}
.v-spacing-medium {
height: 15px;
}
.subscription-dialog-text {
font-size: 13px;
color: var(--env-var-color-2);
}
.subscription-dialog-controls {
display: flex;
}
.controls-negative {
padding: 12px 16px;
margin-right: 12px;
border-radius: var(--env-var-radius-1);
border: 1px solid var(--env-var-color-6);
background-color: var(--env-var-color-8);
font-size: var(--env-var-font-size-medium);
}
.controls-positive,
.subscribe-button {
padding: 12px 16px;
margin-right: 12px;
border-radius: var(--env-var-radius-1);
border: 1px solid var(--env-var-color-7);
background-color: var(--env-var-color-3);
color: var(--env-var-color-8);
font-size: var(--env-var-font-size-medium);
}
.update-subscription-title {
color: var(--env-var-color-5);
padding: 0 20px;
font-weight: 700;
font-size: 13px;
margin-bottom: 10px;
}
.subscription-email {
display: flex;
padding: 0 20px;
}
.subscription-email-field {
width: 290px;
height: 24px;
border-radius: var(--env-var-radius-1);
border: 1px solid var(--env-var-color-4);
box-shadow: 0px 1px 2px 0px rgba(101, 182, 28, 0.05);
padding: 5px 15px;
margin-right: 20px;
}
.subscribe-button {
width: 100px;
height: 34px;
padding: 2px 15px;
}
@@ -1,55 +0,0 @@
import "./announcementsDualButton.css";
import React from "react";
import CloseIcon from "@mui/icons-material/Close";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
/**
* @component
* @param {Object} props
* @param {string} props.subject - The announcement subject text
* @param {string} props.body - The announcement body text content
* @param {string} props.esc - The text for the escape button
* @param {string} props.primary - The text for the primary button
* @returns {JSX.Element} - Renders the announcement dual button component
*/
const AnnouncementsDualButton = ({ subject, body, esc, primary }) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
return (
<div
className="announcement-without-tile"
style={{ fontFamily: fontFamily }}
>
<div className="announcement-without-content">
<div className="announcement-without-content-subject">{subject}</div>
<div className="v-spacing"></div>
<div className="announcement-without-content-body">{body}</div>
<div className="announcement-content-controllers">
<div className="controllers-button-esc">{esc}</div>
<div className="controllers-button-primary">{primary}</div>
</div>
</div>
<div className="h-spacing"></div>
<div className="announcement-close">
<CloseIcon style={{ fill: "#98A2B3" }} />
</div>
</div>
);
};
AnnouncementsDualButton.propTypes = {
subject: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
esc: PropTypes.string.isRequired,
primary: PropTypes.string.isRequired,
};
export default AnnouncementsDualButton;
@@ -1,25 +0,0 @@
.announcement-without-tile {
margin: 20px;
max-width: 343px;
width: fit-content;
display: flex;
border: 1px solid var(--env-var-color-4);
padding: 16px;
border-radius: var(--env-var-radius-1);
}
.h-spacing {
width: 20px;
}
.announcement-without-content-subject {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
font-weight: 700;
margin-bottom: 10px;
}
.announcement-without-content-body {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-2);
}
@@ -1,71 +0,0 @@
import "./announcementsDualButtonWithIcon.css";
import CloseIcon from "@mui/icons-material/Close";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
/**
* @component
* @param {Object} props
* @param {React.ReactNode} props.icon - Optional React node for an announcement icon
* @param {string} props.subject - The announcement subject text
* @param {string} props.body - The announcement body text content
* @param {string} props.esc - The text for the escape button (usually "Close")
* @param {string} props.primary - The text for the primary button
* @param {function} props.closeToast - Function to close the toast notification
* @returns {JSX.Element} - Renders the announcement dual button with icon component
*/
const AnnouncementsDualButtonWithIcon = ({
icon,
subject,
body,
esc,
primary,
closeToast,
}) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
return (
<div className="announcement-tile" style={{ fontFamily: fontFamily }}>
{icon && <div className="announcement-icon">{icon}</div>}
<div className="announcement-content">
{subject && (
<div className="announcement-content-subject">{subject}</div>
)}
{body && <div className="announcement-content-body">{body}</div>}
{(esc || primary) && (
<div className="announcement-content-controllers">
{esc && (
<div className="controllers-button-esc" onClick={closeToast}>
{esc}
</div>
)}
{primary && (
<div className="controllers-button-primary">{primary}</div>
)}
</div>
)}
</div>
<div className="announcement-close" onClick={closeToast}>
<CloseIcon style={{ fill: "#98A2B3" }} />
</div>
</div>
);
};
AnnouncementsDualButtonWithIcon.propTypes = {
icon: PropTypes.object,
subject: PropTypes.string,
body: PropTypes.string,
esc: PropTypes.string,
primary: PropTypes.string,
closeToast: PropTypes.func.isRequired,
};
export default AnnouncementsDualButtonWithIcon;
@@ -1,58 +0,0 @@
.announcement-tile {
margin: 20px;
width: fit-content;
display: flex;
border: 1px solid var(--env-var-color-4);
padding: 16px;
border-radius: var(--env-var-radius-1);
}
.info-icon-frame {
display: flex;
width: var(--env-var-default-2);
height: var(--env-var-default-2);
border: 1px solid var(--env-var-color-6);
border-radius: var(--env-var-radius-2);
justify-content: center;
align-items: center;
}
.announcement-content {
padding: 0 16px;
}
.announcement-content-subject {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
font-weight: 700;
margin-bottom: 10px;
}
.announcement-content-body {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-2);
font-weight: 300;
}
.announcement-content-controllers {
display: flex;
font-size: var(--env-var-font-size-medium);
margin-top: 15px;
}
.controllers-button-esc {
font-size: var(--env-var-font-size-medium);
font-weight: 600;
margin-right: 12px;
}
.controllers-button-primary {
font-size: var(--env-var-font-size-medium);
font-weight: 600;
color: var(--env-var-color-3);
}
.announcement-close {
width: var(--env-var-default-1);
height: var(--env-var-default-1);
}
@@ -1,36 +0,0 @@
import "./announcementsMessageBar.css";
import React from "react";
import CloseIcon from "@mui/icons-material/Close";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
/**
* @component
* @param {Object} props
* @param {string} props.message - The announcement message text content (required)
* @returns {JSX.Element} - Renders the announcement message bar component
*/
const AnnouncementsMessageBar = ({ message }) => {
const theme = useTheme();
const fontLookup = {
default: theme.font.default.font,
};
const fontFamily = fontLookup["default"];
return (
<div className="announcement-tile" style={{ fontFamily: fontFamily }}>
<div className="announcement-messagebar-body">{message}</div>
<div className="announcement-close">
<CloseIcon style={{ fill: "#98A2B3" }} />
</div>
</div>
);
};
AnnouncementsMessageBar.propTypes = {
message: PropTypes.string.isRequired,
};
export default AnnouncementsMessageBar;
@@ -1,8 +0,0 @@
.announcement-messagebar-body {
margin: 20px;
font-size: var(--env-var-font-size-medium);
padding: 0 16px 0 0;
justify-content: center;
align-items: center;
margin: auto;
}
-3
View File
@@ -1,3 +0,0 @@
.Toastify__toast-container {
width: auto;
}
-52
View File
@@ -1,52 +0,0 @@
import PropTypes from "prop-types";
import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert";
import AnnouncementsDualButtonWithIcon from "./AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon";
import "react-toastify/dist/ReactToastify.css";
import { toast, ToastContainer } from "react-toastify";
import "./index.css";
/**
* ToastComponent displays a notification that is triggered by error messages.
*
* @component
* @param {Object} props - The component props
* @param {string} props.subject - The subject or title of the toast message
* @param {string} props.body - The body content of the toast message
* @param {string} props.esc - The text for the dismiss action button
* @param {string} props.primary - The text for the primary action button
* @returns {JSX.Element} JSX element representing the ToastComponent
*/
const ToastComponent = ({ subject, body, esc, primary }) => {
const displayMsg = () => {
toast(
({ closeToast, toastProps }) => (
<AnnouncementsDualButtonWithIcon
icon={<ComplexAlert theme="red" />}
subject={subject}
body={body}
esc={esc}
primary={primary}
closeToast={closeToast}
/>
),
{ closeButton: false }
);
};
return (
<div>
<button onClick={displayMsg}>Click me</button>
<ToastContainer className="Toastify__toast-container" />
</div>
);
};
ToastComponent.propTypes = {
subject: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
esc: PropTypes.string.isRequired,
primary: PropTypes.string.isRequired,
};
export default ToastComponent;
@@ -50,11 +50,14 @@ export const getMonitorsByUserId = createAsyncThunk(
async (token, thunkApi) => {
const user = jwtDecode(token);
try {
const res = await axiosInstance.get("/monitors/user/" + user._id, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const res = await axiosInstance.get(
`/monitors/user/${user._id}?limit=25`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return res.data;
} catch (error) {
if (error.response && error.response.data) {
+3 -5
View File
@@ -139,7 +139,7 @@
}
}
.account .MuiStack-root:has(p.MuiTypography-root.input-error) {
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
}
.account .text-field-icon {
@@ -150,11 +150,9 @@
height: 18px !important;
opacity: 0.8;
}
.account p.MuiTypography-root.input-error {
color: var(--env-var-color-24);
opacity: 0.8;
.account span.MuiTypography-root.input-error {
position: absolute;
top: calc(100% + 5px);
top: 100%;
}
.account .email-text-field,
.account .password-text-field {
@@ -55,6 +55,13 @@
outline: none;
border: none;
}
.create-monitor-form .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
}
.create-monitor-form span.MuiTypography-root.input-error {
position: absolute;
top: 100%;
}
.monitors-gaps-medium {
height: var(--env-var-spacing-2);
+10 -12
View File
@@ -1,6 +1,5 @@
import "./index.css";
import ConfigBox from "../../Components/ConfigBox";
import FlexibileTextField from "../../Components/TextFields/Flexibile/FlexibileTextField";
import React, { useState } from "react";
import RadioButton from "../../Components/RadioButton";
import CustomizableCheckBox from "../../Components/Checkbox/CustomizableCheckbox";
@@ -12,6 +11,7 @@ import { createMonitor } from "../../Features/Monitors/monitorsSlice";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
import Field from "../../Components/Inputs/Field";
const CreateNewMonitor = () => {
const MS_PER_MINUTE = 60000;
@@ -86,6 +86,7 @@ const CreateNewMonitor = () => {
//obj to submit
let monitor = {
...generalSettings,
url: "https://" + generalSettings.url,
...checks,
};
@@ -162,25 +163,22 @@ const CreateNewMonitor = () => {
}
rightLayout={
<Stack gap={theme.gap.xl}>
<FlexibileTextField
id="monitor-url"
title="URL to monitor"
<Field
type="url"
placeholder="https://google.com"
id="monitor-url"
label="URL to monitor"
placeholder="google.com"
value={generalSettings.url}
onChange={(event) =>
handleChange(event, "url", setGeneralSettings)
}
error={errors["url"]}
/>
<FlexibileTextField
id="monitor-name"
title={
<>
Friendly name <span>(optional)</span>
</>
}
<Field
type="text"
id="monitor-name"
label="Friendly name"
isOptional={true}
placeholder="Google"
value={generalSettings.name}
onChange={(event) =>
+71 -6
View File
@@ -37,9 +37,11 @@ import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { getMonitors } from "../../Features/Monitors/monitorsSlice";
import { getMonitorsByUserId } from "../../Features/Monitors/monitorsSlice";
import ImageField from "../../Components/TextFields/Image";
import ImageField from "../../Components/Inputs/Image";
import ProgressUpload from "../../Components/ProgressBars";
import Alert from "../../Components/Alert";
import { createToast } from "../../Utils/toastUtils";
import Field from "../../Components/Inputs/Field";
const cols = [
{
@@ -201,6 +203,9 @@ const Demo = () => {
setIsLoading(false);
}, 4000);
};
//fields
const [visibility, setVisibility] = useState(false);
return (
<div>
<div style={{ padding: "4rem", border: "1px solid black" }}>
@@ -421,14 +426,23 @@ const Demo = () => {
</Stack>
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
<Stack justifyContent="center" alignItems="center">
<ImageField />
<ImageField
id="test-image-field"
onChange={() => console.log("changed")}
/>
</Stack>
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
<Stack justifyContent="center" alignItems="center">
<ProgressUpload icon={<ImageIcon />} label="image.jpg" size="2 MB" />
<ProgressUpload
icon={<ImageIcon />}
label="image.jpg"
size="2 MB"
progress={50}
onClick={() => console.log("click")}
/>
</Stack>
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
<Stack justifyContent="center" alignItems="center">
<Stack direction="row" gap="10px" flexWrap="wrap">
<Box width="500px">
<Alert
variant="info"
@@ -436,19 +450,70 @@ const Demo = () => {
body="Since you logged in via SSO, you cannot reset or modify your password."
/>
</Box>
<Box width="500px" mt="5px">
<Box width="500px">
<Alert
variant="warning"
body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol."
/>
</Box>
<Box width="500px" mt="5px">
<Box width="500px">
<Alert
variant="error"
title="Error"
body="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
/>
</Box>
<Box width="500px">
<Alert
variant="info"
title="We've just released a new feature"
body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid pariatur, ipsum dolor."
isToast={true}
/>
</Box>
<Box width="500px">
<Alert
variant="info"
body="Your password is incorrect."
isToast={true}
hasIcon={false}
/>
</Box>
<Box width="500px" mt="5px">
<Button
level="primary"
label="Toast"
onClick={() =>
createToast({
variant: "info",
body: "Your password is incorrect.",
hasIcon: false,
})
}
/>
</Box>
</Stack>
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
<Stack direction="row" justifyContent="center" gap="20px">
<Field type="text" label="Name" placeholder="Enter a name" />
<Field
type="email"
label="Email"
isRequired={true}
placeholder="name.surname@companyname.com"
error="This is an error message."
/>
<Field
type="password"
label="Password"
isRequired={true}
placeholder="Create a password"
/>
<Field
type="url"
label="Website"
placeholder="google.com"
/>
</Stack>
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
</div>
+8 -6
View File
@@ -2,13 +2,13 @@ import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPatt
import "./index.css";
import React from "react";
import Logomark from "../../assets/Images/key-password.png";
import EmailTextField from "../../Components/TextFields/Email/EmailTextField";
import Button from "../../Components/Button";
import LeftArrow from "../../assets/Images/arrow-left.png";
import { useState, useEffect } from "react";
import { recoveryValidation } from "../../Validation/validation";
import axiosInstance from "../../Utils/axiosConfig";
import { useNavigate } from "react-router-dom";
import Field from "../../Components/Inputs/Field";
const ForgotPassword = () => {
const navigate = useNavigate();
@@ -76,12 +76,14 @@ const ForgotPassword = () => {
</div>
<div className="forgot-password-v-gap-large"></div>
<div className="forgot-password-body">
<EmailTextField
onChange={handleInput}
error={errors.email !== undefined ? true : false}
helperText={errors.email !== undefined ? errors.email : ""}
placeholder="Enter your email"
<Field
type="email"
id="forgot-password-email-input"
label="Email"
isRequired={true}
placeholder="Enter your email"
onChange={handleInput}
error={errors.email}
/>
<div className="forgot-password-v-gap-medium"></div>
<Button
+47 -112
View File
@@ -1,133 +1,24 @@
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
import { styled, useTheme } from '@mui/material/styles';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { StatusLabel } from "../../Components/Label/";
import TuneIcon from '@mui/icons-material/Tune';
/**
* Creates a styled TableCell component.
* @param {object} theme - The theme object.
* @returns {JSX.Element} The styled TableCell component.
*/
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: theme.palette.action.hover,
color: theme.palette.common.black,
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
},
}));
/**
* Creates a styled TableRow component.
* @param {object} theme - The theme object.
* @returns {JSX.Element} The styled TableRow component.
*/
const StyledTableRow = styled(TableRow)(({ theme }) => ({
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.action.hover,
},
'&:nth-of-type(even)': {
backgroundColor: theme.palette.common.white,
},
'&:last-child td, &:last-child th': {
border: 0,
},
}));
import StatusTable from '../../Components/StatusTable';
import { StatusLabel } from "../../Components/Label/";
const filterOptions = createFilterOptions({
matchFrom: 'start',
stringify: (option) => option.title,
});
const titles = [
{ title: 'Down' },
{ title: 'Cannot resolve' },
{ title: 'Clear / show all' },
];
/**
* Creates a data object for the table row.
* @param {JSX.Element} name - The status label JSX element.
* @param {string} date - The date and time string.
* @param {string} message - The message string.
* @returns {object} The data object containing name, date, and message.
*/
function createData(name, date, message) {
return { name, date, message };
}
/**
* Customized table component displaying incident history.
* @returns {JSX.Element} The JSX element representing the customized table.
*/
export default function CustomizedTables() {
const theme = useTheme();
const rows = [
createData(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '2024-03-14 21:41:09', 'HTTP 350 - NOK'),
createData(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Cannot resolve" customStyles={{ backgroundColor: '#f2f4f7', borderColor: '#d2d6de', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Cannot resolve" customStyles={{ backgroundColor: '#f2f4f7', borderColor: '#d2d6de', color: '#344054' }} />, '2024-03-14 21:41:09', 'timeout of 48000ms exceeded'),
createData(<StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, '2024-03-14 21:41:09', 'HTTP 350 - NOK'),
];
return (
<Box
sx={{
marginTop: theme.spacing(4),
marginX: 'auto',
width: '80%',
paddingX: theme.spacing(6),
[theme.breakpoints.down('md')]: {
width: '100%',
},
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: theme.spacing(2) }}>
<Typography variant="h6" component="div" sx={{ fontWeight: 600, fontSize: 16 }}>
Incident History
</Typography>
<Filter />
</Box>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 700 }} aria-label="customized table">
<TableHead>
<TableRow>
<StyledTableCell>Status</StyledTableCell>
<StyledTableCell align="right">Date & Time</StyledTableCell>
<StyledTableCell align="right">Message</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<StyledTableRow key={index}>
<StyledTableCell component="th" scope="row">
{row.name}
</StyledTableCell>
<StyledTableCell align="right">{row.date}</StyledTableCell>
<StyledTableCell align="right">{row.message}</StyledTableCell>
</StyledTableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
}
/**
* Filter component with autocomplete for status filtering.
* @returns {JSX.Element} The JSX element representing the filter component.
@@ -165,3 +56,47 @@ function Filter() {
/>
);
}
const sampleData = [
{ name: <StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, date: '2024-03-14 21:41:09', message: 'HTTP 350 - NOK' },
{ name: <StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, date: '2024-03-14 21:41:09', message: 'timeout of 48000ms exceeded' },
{ name: <StatusLabel status="Cannot resolve" customStyles={{ backgroundColor: '#f2f4f7', borderColor: '#d2d6de', color: '#344054' }} />, date: '2024-03-14 21:41:09', message: 'timeout of 48000ms exceeded' },
{ name: <StatusLabel status="Cannot resolve" customStyles={{ backgroundColor: '#f2f4f7', borderColor: '#d2d6de', color: '#344054' }} />, date: '2024-03-14 21:41:09', message: 'timeout of 48000ms exceeded' },
{ name: <StatusLabel status="Down" customStyles={{ backgroundColor: '#fff9f9', borderColor: '#ffcac6', color: '#344054' }} />, date: '2024-03-14 21:41:09', message: 'HTTP 350 - NOK' },
];
const columns = [
{ id: 'name', header: 'Status' },
{ id: 'date', header: 'Date & Time' },
{ id: 'message', header: 'Message' },
];
/**
* Customized table component displaying incident history with a filter.
* @returns {JSX.Element} The JSX element representing the customized table with a filter.
*/
export default function CustomizedTables() {
const theme = useTheme();
return (
<Box
sx={{
marginTop: theme.spacing(4),
marginX: 'auto',
width: '80%',
paddingX: theme.spacing(6),
[theme.breakpoints.down('md')]: {
width: '100%',
},
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: theme.spacing(2) }}>
<Typography variant="h6" component="div" sx={{ fontWeight: 600, fontSize: 16 }}>
Incident History
</Typography>
<Filter />
</Box>
<StatusTable data={sampleData} columns={columns} />
</Box>
);
}
+3 -11
View File
@@ -40,17 +40,9 @@
color: var(--env-var-color-1);
}
#login-email-input {
width: 308px;
height: 1px;
font-size: var(--env-var-font-size-medium);
}
#login-password-input {
width: 308px;
height: 1px;
font-size: var(--env-var-font-size-medium);
}
/* .login-form-inputs .field h3.MuiTypography-root{
font-weight: 600;
} */
.login-form-password-options {
display: flex;
+37 -20
View File
@@ -8,11 +8,12 @@ import EmailTextField from "../../Components/TextFields/Email/EmailTextField";
import CheckBox from "../../Components/Checkbox/Checkbox";
import Button from "../../Components/Button";
import Google from "../../assets/Images/Google.png";
import PasswordTextField from "../../Components/TextFields/Password/PasswordTextField";
import axiosInstance from "../../Utils/axiosConfig";
import { loginValidation } from "../../Validation/validation";
import { login } from "../../Features/Auth/authSlice";
import { useDispatch } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
import Field from "../../Components/Inputs/Field";
const Login = () => {
const dispatch = useDispatch();
@@ -74,16 +75,29 @@ const Login = () => {
}
} catch (error) {
if (error.name === "ValidationError") {
// TODO Handle validation errors
console.log(error.details);
alert(error);
// validation errors
createToast({
variant: "info",
body:
error && error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
hasIcon: false,
});
} else if (error.response) {
// TODO handle dispatch errors
alert(error.response.msg);
// dispatch errors
createToast({
variant: "info",
body: error.response.msg,
hasIcon: false,
});
} else {
// TODO handle unknown errors
console.log(error);
alert("Unknown error");
// unknown errors
createToast({
variant: "info",
body: "Unknown error.",
hasIcon: false,
});
}
}
};
@@ -107,23 +121,26 @@ const Login = () => {
<div className="login-form-heading">Log in to your account</div>
</div>
<div className="login-form-v3-spacing" />
<div className="login-form-inputs">
<EmailTextField
onChange={handleInput}
error={errors.email ? true : false}
helperText={errors.email ? errors.email : ""}
<Field
type="email"
id="login-email-input"
label="Email"
placeholder="Enter your email"
autoComplete="email"
id="login-email-input"
onChange={handleInput}
error={errors.email}
/>
<div className="login-form-v2-spacing" />
<PasswordTextField
onChange={handleInput}
error={errors.password ? true : false}
helperText={errors.password ? errors.password : ""}
placeholder="Password"
autoComplete="current-password"
<Field
type="password"
id="login-password-input"
label="Password"
placeholder="Enter your password"
autoComplete="current-password"
onChange={handleInput}
error={errors.password}
/>
</div>
<div className="login-form-v3-spacing" />
+1 -1
View File
@@ -6,7 +6,7 @@ import { getMonitorsByUserId } from "../../Features/Monitors/monitorsSlice";
import { useNavigate } from "react-router-dom";
import Button from "../../Components/Button";
import ServerStatus from "../../Components/Charts/Servers/ServerStatus";
import SearchTextField from "../../Components/TextFields/Search/SearchTextField";
import SearchTextField from "../../Components/Inputs/Search/SearchTextField";
import MonitorTable from "../../Components/MonitorTable";
import { useTheme } from "@emotion/react";
+59 -98
View File
@@ -1,19 +1,8 @@
import React from "react";
import EmailTextField from "../../Components/TextFields/Email/EmailTextField";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import WebsiteTextField from "../../Components/TextFields/Website/WebsiteTextField";
import DescriptionTextField from "../../Components/TextFields/Description/DescriptionTextField";
import AnnouncementsDualButton from "../../Components/Toast/AnnouncementsDualButton/AnnouncementsDualButton";
import AnnouncementsDualButtonWithIcon from "../../Components/Toast/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert";
import AnnouncementsMessageBar from "../../Components/Toast/AnnouncementsMessageBar/AnnouncementsMessageBar";
import AnnouncementUpdateSubscription from "../../Components/Toast/AnnouncementUpdateSubscription/AnnouncementUpdateSubscription";
import PlayGroundCharts from "./PlayGround-Charts";
import PlayGroundPopupModals from "./PlayGround-Popup-Modals";
import PlayGroundTooltips from "./PlayGround-Tooltips";
import Toast from "../../Components/Toast";
import Field from "../../Components/Inputs/Field";
// This Component is just for the development and test
// purposes and just to see what my components look like while development
@@ -22,99 +11,71 @@ function PlayGround() {
return (
<div>
{/* Default state of Email Text Fields */}
<EmailTextField
error={false}
placeholder="olivia@untitledui.com"
id="outlined-basic"
helperText="This is a hint text to help user."
/>
{/* Email Text Field when validation error occures */}
<EmailTextField
error={true}
placeholder="olivia@untitledui.com"
id="outlined-basic"
icon={<ErrorOutlineIcon style={{ fill: "red" }} />}
helperText="This is an error message"
/>
{/* Email Text Field while mouse hover */}
<EmailTextField
error={false}
placeholder="olivia@untitledui.com"
id="outlined-basic"
icon={<HelpOutlineIcon />}
helperText="This is a hint text to help user."
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
maxWidth: "400px",
padding: "10px",
}}
>
<Field
type="email"
id="outlined-basic"
label="Email"
placeholder="olivia@untitledui.com"
/>
{/* Email Text Field when validation error occures */}
<Field
type="email"
id="outlined-basic"
label="Email"
placeholder="olivia@untitledui.com"
error="This is an error message"
/>
</div>
<hr />
{/* Now, illustration of the Website text fields */}
<WebsiteTextField hintText="This is a hint text to help user." />
<WebsiteTextField
hasCopyButton={true}
hintText="This is a hint text to help user."
/>
<Toast />
{/* Now, illustration of the Website text fields */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
maxWidth: "400px",
padding: "10px",
}}
>
<Field type="url" label="Website" />
<Field type="url" label="Website" hasCopy={true} />
</div>
<hr />
{/* Now, illustration of the Description text fields */}
<DescriptionTextField
hasError={false}
hintText="This is a hint text to help user."
/>
<DescriptionTextField
hasError={true}
hintText="This is a hint text to help user."
/>
<AnnouncementsDualButtonWithIcon
icon={
<div className="info-icon-frame">
<InfoOutlinedIcon style={{ fill: "#344054" }} />
</div>
}
subject="We’ve just released a new feature"
body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid
pariatur, ipsum dolor."
esc="Dismiss"
primary="View changes"
/>
<AnnouncementsDualButtonWithIcon
icon={<ComplexAlert theme="red" />}
subject="There was a problem with that action"
body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid
pariatur, ipsum dolor."
esc="Dismiss"
primary="Learn more"
/>
<AnnouncementsDualButtonWithIcon
icon={<ComplexAlert theme="green" />}
subject="Successfully updated profile"
body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid
pariatur, ipsum dolor."
esc="Dismiss"
primary="Learn more"
/>
<AnnouncementsDualButton
subject="We’ve just released a new feature"
body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid
pariatur, ipsum dolor."
esc="Dismiss"
primary="View changes"
/>
<AnnouncementsMessageBar message="New employee created successfully" />
<AnnouncementUpdateSubscription
title="We’ve just released a new update!"
text="Check out the all new dashboard view. Pages and now load faster."
cancel="Dismiss"
positive="Changelog"
header="Subscribe to updates"
button="Subscribe"
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
maxWidth: "400px",
padding: "10px",
}}
>
<Field
type="description"
label="Description"
placeholder="Enter a description... "
/>
<Field
type="description"
label="Description"
placeholder="Enter a description... "
error="This is an error message"
/>
</div>
<hr />
<PlayGroundCharts />
<hr />
-11
View File
@@ -1,5 +1,4 @@
.register-page {
width: var(--env-var-width-1);
display: flex;
justify-content: center;
min-height: var(--env-var-height-1);
@@ -40,16 +39,6 @@
font-weight: 600;
}
#register-firstname-input,
#register-lastname-input,
#register-email-input,
#register-password-input {
font-size: var(--env-var-font-size-medium);
width: 294px;
height: 11px;
padding: 12px 14px;
}
.register-bottom-spacing {
height: 50px;
}
+68 -37
View File
@@ -4,9 +4,6 @@ import { useNavigate } from "react-router-dom";
import "./index.css";
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
import Logomark from "../../assets/Images/Logomark.png";
import EmailTextField from "../../Components/TextFields/Email/EmailTextField";
import PasswordTextField from "../../Components/TextFields/Password/PasswordTextField";
import StringTextField from "../../Components/TextFields/Text/TextField";
import Check from "../../Components/Check/Check";
import Button from "../../Components/Button";
import Google from "../../assets/Images/Google.png";
@@ -14,6 +11,8 @@ import { registerValidation } from "../../Validation/validation";
import axiosInstance from "../../Utils/axiosConfig";
import { useDispatch } from "react-redux";
import { register } from "../../Features/Auth/authSlice";
import { createToast } from "../../Utils/toastUtils";
import Field from "../../Components/Inputs/Field";
const Register = () => {
const dispatch = useDispatch();
@@ -91,20 +90,34 @@ const Register = () => {
}
} catch (error) {
if (error.name === "ValidationError") {
// TODO Handle validation errors
console.log(error);
alert(error);
// validation errors
createToast({
variant: "info",
body:
error && error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
hasIcon: false,
});
} else if (error.response) {
// TODO handle dispatch errors
alert(error.response.msg);
// dispatch errors
createToast({
variant: "info",
body: error.response.msg,
hasIcon: false,
});
} else {
// TODO handle unknown errors
console.log(error);
alert("Unknown error");
// unknown errors
createToast({
variant: "info",
body: "Unknown error.",
hasIcon: false,
});
}
}
};
return (
<div className="register-page">
<BackgroundPattern></BackgroundPattern>
@@ -116,47 +129,65 @@ const Register = () => {
alt="Logomark"
/>
<div className="register-form-v-spacing-large" />
<div className="register-form-heading">Create an account</div>
<div className="register-form-heading">
Create Uptime Manager admin account
</div>
<div className="register-form-v-spacing-large"></div>
</div>
<div className="register-form-v-spacing-40px" />
<div className="register-form-inputs">
<StringTextField
onChange={handleInput}
error={errors.firstname ? true : false}
helperText={errors.firstname ? errors.firstname : ""}
label="First name*"
placeholder="Enter your first name"
<Field
id="register-firstname-input"
label="Name"
isRequired={true}
placeholder="Talha"
autoComplete="given-name"
onChange={handleInput}
error={errors.firstname}
/>
<div className="login-form-v2-spacing" />
<StringTextField
onChange={handleInput}
error={errors.lastname ? true : false}
helperText={errors.lastname ? errors.lastname : ""}
label="Last name*"
placeholder="Enter your last name"
<Field
id="register-lastname-input"
label="Surname"
isRequired={true}
placeholder="Bolat"
autoComplete="family-name"
onChange={handleInput}
error={errors.lastname}
/>
<div className="login-form-v2-spacing" />
<EmailTextField
onChange={handleInput}
label="Email*"
error={errors.email ? true : false}
helperText={errors.email ? errors.email : ""}
placeholder="Enter your email"
autoComplete="email"
<Field
type="email"
id="register-email-input"
label="Email"
isRequired={true}
placeholder="name.surname@companyname.com"
autoComplete="email"
onChange={handleInput}
error={errors.email}
/>
<div className="login-form-v2-spacing" />
<PasswordTextField
onChange={handleInput}
label="Password*"
error={errors.password ? true : false}
helperText={errors.password ? errors.password : ""}
<Field
type="password"
id="register-password-input"
label="Password"
isRequired={true}
placeholder="Create a password"
autoComplete="current-password"
id="register-password-input"
error={errors.password}
onChange={handleInput}
/>
<div className="login-form-v2-spacing" />
{/* TODO - hook up to form state and run checks */}
<Field
type="password"
id="register-confirm-input"
label="Confirm password"
isRequired={true}
placeholder="Confirm your password"
autoComplete="current-password"
error={errors.confirm}
onChange={handleInput}
/>
</div>
<div className="login-form-v2-spacing" />
+15 -13
View File
@@ -1,7 +1,6 @@
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
import "./index.css";
import LockIcon from "../../assets/Images/lock-icon.png";
import PasswordTextField from "../../Components/TextFields/Password/PasswordTextField";
import Check from "../../Components/Check/Check";
import Button from "../../Components/Button";
import LeftArrow from "../../assets/Images/arrow-left.png";
@@ -10,6 +9,7 @@ import { useState, useEffect } from "react";
import { newPasswordValidation } from "../../Validation/validation";
import axiosInstance from "../../Utils/axiosConfig";
import { useNavigate } from "react-router-dom";
import Field from "../../Components/Inputs/Field";
const SetNewPassword = () => {
const navigate = useNavigate();
@@ -83,22 +83,24 @@ const SetNewPassword = () => {
</div>
<div className="set-new-password-form-gap-large"></div>
<div className="set-new-password-form-content">
<PasswordTextField
label="Password"
onChange={handleInput}
error={errors.password !== undefined ? true : false}
helperText={errors.password !== undefined ? errors.password : ""}
placeholder="••••••••"
<Field
type="password"
id="register-password-input"
label="Password"
isRequired={true}
placeholder="••••••••"
onChange={handleInput}
error={errors.password}
/>
<div className="set-new-password-form-gap-medium"></div>
<PasswordTextField
label="Confirm password"
onChange={handleInput}
error={errors.confirm !== undefined ? true : false}
helperText={errors.confirm !== undefined ? errors.confirm : ""}
placeholder="••••••••"
<Field
type="password"
id="confirm-password-input"
label="Confirm password"
isRequired={true}
placeholder="••••••••"
onChange={handleInput}
error={errors.confirm}
/>
<div className="set-new-password-form-gap-medium"></div>
<div className="set-new-password-form-checks">
+5 -4
View File
@@ -91,6 +91,7 @@ const theme = createTheme({
pY: "40px",
},
gap: {
xs: "4px",
small: "8px",
medium: "12px",
ml: "16px",
@@ -101,7 +102,7 @@ const theme = createTheme({
info: {
color: secondaryMain,
bg: otherColorsWhite,
border: "#d0d5dd",
border: sectionBorder,
},
error: {
color: "#d92d20",
@@ -109,9 +110,9 @@ const theme = createTheme({
border: "#f04438",
},
warning: {
color: "#f79009",
bg: "#fffaef",
border: "#fecf60"
color: "#DC6803",
bg: "#fffcf5",
border: "#fec84b",
},
},
});
+44
View File
@@ -0,0 +1,44 @@
import PropTypes from "prop-types";
import { toast } from "react-toastify";
import Alert from "../Components/Alert";
/**
* @param {object} props
* @param {'info' | 'error' | 'warning'} - The variant of the alert (e.g., "info", "error").
* @param {string} props.title - The title of the alert.
* @param {string} props.body - The body/content of the alert.
* @param {boolean} props.hasIcon - Whether the alert should include an icon.
* @param {object} [props.config] - Additional configuration props for the toast.
*/
export const createToast = ({ variant, title, body, hasIcon, config = {} }) => {
const toastConfig = {
position: "bottom-right",
autoClose: 3000,
hideProgressBar: true,
closeButton: false,
...config,
};
toast(
({ closeToast }) => (
<Alert
variant={variant}
title={title}
body={body}
isToast={true}
hasIcon={hasIcon}
onClick={closeToast}
/>
),
toastConfig
);
};
createToast.propTypes = {
variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired,
title: PropTypes.string,
body: PropTypes.string,
hasIcon: PropTypes.bool,
config: PropTypes.object,
};
+13
View File
@@ -59,6 +59,7 @@
--env-var-spacing-1: 12px;
--env-var-spacing-1-plus: 16px;
--env-var-spacing-1-minus: 10px;
--env-var-spacing-2: 24px;
--env-var-spacing-3: 32px;
--env-var-spacing-4: 40px;
@@ -124,6 +125,18 @@ button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.Toastify__toast-container {
width: auto;
}
.Toastify [class^="Toastify__toast"] {
padding: 0;
margin: 0;
}
.Toastify__toast{
min-height: 0;
border-radius: 4px;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
+15
View File
@@ -60,6 +60,21 @@ const registerController = async (req, res, next) => {
return;
}
// Check if an admin user exists, if so, error
try {
const admin = await req.db.checkAdmin(req, res);
console.log(admin);
if (admin === true) {
throw new Error(errorMessages.AUTH_ADMIN_EXISTS);
}
} catch (error) {
console.log("WEEEEEEE", error.message);
error.service = SERVICE_NAME;
error.status = 403;
next(error);
return;
}
// Create a new user
try {
const newUser = await req.db.insertUser(req, res);
-6
View File
@@ -87,12 +87,6 @@ const getMonitorsByUserId = async (req, res, next) => {
const userId = req.params.userId;
const monitors = await req.db.getMonitorsByUserId(req, res);
if (monitors && monitors.length === 0) {
const err = new Error(errorMessages.MONITOR_GET_BY_USER_ID);
err.status = 404;
throw err;
}
return res.json({
success: true,
msg: successMessages.MONITOR_GET_BY_USER_ID(userId),
+7 -7
View File
@@ -293,19 +293,19 @@ const getMonitorById = async (req, res) => {
*/
const getMonitorsByUserId = async (req, res) => {
try {
const limit = req.body.limit;
const limit = req.query.limit;
const monitors = await Monitor.find({ userId: req.params.userId });
// Map each monitor to include its associated checks
const monitorsWithChecks = await Promise.all(
monitors.map(async (monitor) => {
if(limit) {
if (limit) {
// Checks are order oldest -> newest
const checks = await Check.find({ monitorId: monitor._id }).sort({
createdAt: 1,
}).limit(limit);;
const checks = await Check.find({ monitorId: monitor._id })
.sort({
createdAt: 1,
})
.limit(limit);
return { ...monitor.toObject(), checks };
} else {
// Checks are order oldest -> newest
const checks = await Check.find({ monitorId: monitor._id }).sort({
+1 -1
View File
@@ -5,7 +5,7 @@ const Monitor = require("../models/Monitor");
router.get("/", monitorController.getAllMonitors);
router.get("/:monitorId", monitorController.getMonitorById);
router.get("/user/:userId?limit", monitorController.getMonitorsByUserId);
router.get("/user/:userId", monitorController.getMonitorsByUserId);
router.post("/", monitorController.createMonitor);
router.post(
+1
View File
@@ -4,6 +4,7 @@ const errorMessages = {
UNKNOWN_ERROR: "An unknown error occurred",
// Auth Controller
UNAUTHORIZED: "Unauthorized access",
AUTH_ADMIN_EXISTS: "Admin already exists",
//Error handling middleware
UNKNOWN_SERVICE: "Unknown service",