Merge branch 'master' into add-product-logo
This commit is contained in:
61 files changed
+1143
-1971
No files matched your search
@@ -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
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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.
@@ -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;
|
||||
}
|
||||
-71
@@ -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;
|
||||
-86
@@ -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);
|
||||
}
|
||||
-71
@@ -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;
|
||||
-58
@@ -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;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.Toastify__toast-container {
|
||||
width: auto;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user