Merge pull request #717 from bluewave-labs/develop

Develop -> Master
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-08-23 15:40:21 -07:00
commit 72c4d760f2
29 files changed
+678 -200

No files matched your search

@@ -0,0 +1,62 @@
import PropTypes from "prop-types";
import { Box, Stack } from "@mui/material";
/**
* A component that renders a pulsating dot with a specified color.
*
* @component
* @example
* // Example usage:
* <PulseDot color="#f00" />
*
* @param {Object} props
* @param {string} props.color - The color of the dot.
* @returns {JSX.Element} The PulseDot component.
*/
const PulseDot = ({ color }) => {
return (
<Stack
width="26px"
height="24px"
alignItems="center"
justifyContent="center"
>
<Box
minWidth="18px"
minHeight="18px"
sx={{
position: "relative",
backgroundColor: color,
borderRadius: "50%",
"&::before": {
content: `""`,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "inherit",
borderRadius: "50%",
animation: "ripple 1.8s ease-out infinite",
},
"&::after": {
content: `""`,
position: "absolute",
width: "6px",
height: "6px",
borderRadius: "50%",
backgroundColor: "white",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
},
}}
/>
</Stack>
);
};
PulseDot.propTypes = {
color: PropTypes.string.isRequired,
};
export default PulseDot;
+4 -13
View File
@@ -1,22 +1,12 @@
.MuiTable-root .host a {
width: var(--env-var-img-width-2);
height: var(--env-var-img-width-2);
color: var(--env-var-color-5);
margin-right: 10px;
}
.MuiTable-root .host a svg {
width: var(--env-var-font-size-large);
height: var(--env-var-font-size-large);
}
.MuiTable-root .host div:nth-child(2) {
.MuiTable-root .host {
width: fit-content;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.MuiTable-root .host div:nth-child(2) span {
.MuiTable-root .host span {
font-size: var(--env-var-font-size-small);
margin-left: 10px;
margin-left: 8px;
}
.MuiTable-root .label {
@@ -128,6 +118,7 @@
.MuiTablePagination-root p {
color: var(--env-var-color-2);
font-weight: 500;
font-size: var(--env-var-font-size-small-plus);
}
.MuiTablePagination-root button,
.MuiTablePagination-root .MuiSelect-select {
+11 -5
View File
@@ -15,7 +15,6 @@ import {
Stack,
} from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useLocation, useParams } from "react-router-dom";
import { setRowsPerPage } from "../../Features/UI/uiSlice";
import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react";
import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react";
@@ -90,6 +89,13 @@ const TablePaginationActions = (props) => {
);
};
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
/**
* BasicTable Component
* Renders a table with optional pagination.
@@ -143,13 +149,12 @@ const TablePaginationActions = (props) => {
* <BasicTable data={data} rows={rows} paginated={true} />
*/
const BasicTable = ({ data, paginated, reversed }) => {
const BasicTable = ({ data, paginated, reversed, table }) => {
const DEFAULT_ROWS_PER_PAGE = 5;
const theme = useTheme();
const location = useLocation();
const dispatch = useDispatch();
const uiState = useSelector((state) => state.ui);
let table = location.pathname.split("/").pop();
let rowsPerPage = uiState[table].rowsPerPage;
let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE;
const [page, setPage] = useState(0);
useEffect(() => {
@@ -285,6 +290,7 @@ BasicTable.propTypes = {
paginated: PropTypes.bool,
reversed: PropTypes.bool,
rowPerPage: PropTypes.number,
table: PropTypes.string,
};
export default BasicTable;
@@ -0,0 +1,19 @@
.bar-tooltip .MuiTooltip-tooltip {
background-color: white;
border: solid 1px black;
box-shadow: var(--env-var-shadow-1);
border: solid 1px var(--env-var-color-6);
border-radius: var(--env-var-radius-1);
padding: 4px 8px;
}
.bar-tooltip .MuiTooltip-tooltip p {
font-size: var(--env-var-font-size-small-plus);
color: var(--env-var-color-2);
font-weight: 500;
}
.bar-tooltip .MuiTooltip-tooltip span {
font-size: var(--env-var-font-size-small);
color: var(--env-var-color-2);
font-weight: 600;
}
@@ -0,0 +1,142 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDate } from "../../../Utils/timeUtils";
import { useEffect, useState } from "react";
import "./index.css";
const BarChart = ({ checks = [] }) => {
const theme = useTheme();
const [animate, setAnimate] = useState(false);
useEffect(() => {
setAnimate(true);
});
// set responseTime to average if there's only one check
if (checks.length === 1) {
checks[0] = { ...checks[0], responseTime: 50 };
}
if (checks.length !== 25) {
const placeholders = Array(25 - checks.length).fill("placeholder");
checks = [...checks, ...placeholders];
}
return (
<Stack
direction="row"
flexWrap="nowrap"
gap="3px"
height="50px"
width="fit-content"
onClick={(event) => event.stopPropagation()}
sx={{
cursor: "default",
}}
>
{checks.map((check, index) =>
check === "placeholder" ? (
<Box
key={`${check}-${index}`}
position="relative"
width="9px"
height="100%"
backgroundColor={theme.palette.otherColors.fillGray}
sx={{
borderRadius: "3px",
}}
/>
) : (
<Tooltip
title={
<>
<Typography>
{formatDate(new Date(check.createdAt), { year: undefined })}
</Typography>
<Box mt={theme.gap.xs}>
<Box
display="inline-block"
width={theme.gap.small}
height={theme.gap.small}
backgroundColor={
check.status
? theme.label.up.dotColor
: theme.label.down.dotColor
}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.gap.xs}
gap={theme.gap.large}
>
<Typography component="span" sx={{ opacity: 0.8 }}>
Response Time
</Typography>
<Typography component="span">
{check.originalResponseTime}
<Typography component="span" sx={{ opacity: 0.8 }}>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
</>
}
placement="top"
key={`check-${check?._id}`}
slotProps={{
popper: {
className: "bar-tooltip",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
},
}}
>
<Box
position="relative"
width="9px"
height="100%"
backgroundColor={
check.status ? theme.label.up.bgColor : theme.label.down.bgColor
}
sx={{
borderRadius: "3px",
"&:hover > .MuiBox-root": {
filter: "brightness(0.8)",
},
}}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${animate ? check.responseTime : 0}%`}
backgroundColor={
check.status
? theme.label.up.dotColor
: theme.label.down.dotColor
}
sx={{
borderRadius: "3px",
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</Tooltip>
)
)}
</Stack>
);
};
export default BarChart;
@@ -48,6 +48,7 @@ const Field = forwardRef(
placeholder,
value,
onChange,
onInput,
error,
disabled,
},
@@ -87,6 +88,7 @@ const Field = forwardRef(
multiline={type === "description"}
rows={type === "description" ? 4 : 1}
value={value}
onInput={onInput}
onChange={onChange}
disabled={disabled}
inputRef={ref}
@@ -176,6 +178,8 @@ const Field = forwardRef(
}
);
Field.displayName = "Field";
Field.propTypes = {
type: PropTypes.oneOf(["text", "password", "url", "email", "description"]),
id: PropTypes.string.isRequired,
@@ -189,6 +193,7 @@ Field.propTypes = {
placeholder: PropTypes.string,
value: PropTypes.string.isRequired,
onChange: PropTypes.func,
onInput: PropTypes.func,
error: PropTypes.string,
disabled: PropTypes.bool,
};
+56 -9
View File
@@ -76,16 +76,19 @@ aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root::after {
content: "";
position: absolute;
left: -8px;
top: 50%;
transform: translateY(-50%);
top: 45%;
height: 3px;
width: 3px;
border-radius: 50%;
background-color: #d6d9dd;
}
aside {
transition: flex 300ms ease-out;
aside
.MuiCollapse-wrapperInner
.MuiList-root
> .selected-path.MuiListItemButton-root::after {
background-color: var(--env-var-color-2);
transform: scale(1.2);
}
.sidebar-popup .MuiPaper-root {
@@ -95,7 +98,7 @@ aside {
gap: 1px;
}
.sidebar-popup .MuiList-root {
padding: 6px 6px;
padding: 3px;
min-width: 100px;
}
.sidebar-popup li.MuiButtonBase-root {
@@ -104,21 +107,65 @@ aside {
padding: 5px 8px;
border-radius: var(--env-var-radius-1);
}
.sidebar-popup li.MuiButtonBase-root:hover{
.sidebar-popup li.MuiButtonBase-root:hover {
background-color: var(--env-var-color-13);
}
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root):hover{
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root):hover {
background-color: white;
}
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root){
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root) {
padding-bottom: 0;
}
.sidebar-popup .MuiModal-root p.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-2);
}
.sidebar-popup svg{
.sidebar-popup svg {
width: 16px;
height: 16px;
opacity: 0.9;
}
/* TRANSITIONS */
aside {
flex: 1;
transition: max-width 650ms cubic-bezier(0.36, -0.01, 0, 0.77);
}
.home-layout aside.collapsed {
max-width: 64px;
}
aside.expanded .MuiTypography-root,
aside.expanded p.MuiTypography-root,
aside.expanded .MuiListItemText-root + svg,
aside.expanded .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
visibility: visible;
animation: fadeIn 1s ease;
}
aside.collapsed .MuiTypography-root,
aside.collapsed p.MuiTypography-root,
aside.collapsed .MuiListItemText-root + svg,
aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
opacity: 0;
visibility: hidden;
}
aside .MuiListSubheader-root {
transition: all 200ms ease;
}
@keyframes fadeIn {
0% {
opacity: 0;
visibility: hidden;
}
30% {
opacity: 0;
visibility: hidden;
}
100% {
opacity: 0.9;
visibility: visible;
}
}
+56 -39
View File
@@ -26,8 +26,6 @@ import LockSvg from "../../assets/icons/lock.svg?react";
import UserSvg from "../../assets/icons/user.svg?react";
import TeamSvg from "../../assets/icons/user-two.svg?react";
import LogoutSvg from "../../assets/icons/logout.svg?react";
import BWULogo from "../../assets/Images/bwl-logo.svg?react";
import BWULogoAbbreviated from "../../assets/icons/bwu-abbreviated.svg?react";
import Support from "../../assets/icons/support.svg?react";
import Dashboard from "../../assets/icons/dashboard.svg?react";
import Account from "../../assets/icons/user-edit.svg?react";
@@ -55,9 +53,9 @@ const menu = [
],
},
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
{ name: "Status pages", path: "status", icon: <StatusPages /> },
// { name: "Status pages", path: "status", icon: <StatusPages /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
{ name: "Integrations", path: "integrations", icon: <Integrations /> },
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
{
name: "Account",
icon: <Account />,
@@ -125,24 +123,41 @@ function Sidebar() {
return (
<Stack
component="aside"
className={collapsed ? "collapsed" : "expanded"}
gap={theme.gap.medium}
sx={{ flex: collapsed ? 0 : 1 }}
>
<Stack
alignItems={collapsed ? "center" : "flex-start"}
py={theme.gap.large}
pl={collapsed ? 0 : theme.gap.lgplus}
>
{collapsed ? (
<BWULogoAbbreviated alt="BlueWave Uptime Logo" />
) : (
<BWULogo alt="BlueWave Uptime Logo" />
)}
<Stack pt={theme.gap.medium} pb={theme.gap.large} pl={theme.gap.ml}>
<Stack direction="row" alignItems="center" gap={theme.gap.small}>
<Stack
justifyContent="center"
alignItems="center"
minWidth={theme.gap.lgplus}
minHeight={theme.gap.lgplus}
fontSize="18px"
color="white"
sx={{
position: "relative",
backgroundColor: theme.palette.primary.main,
borderRadius: "4px",
userSelect: "none",
}}
>
BU
</Stack>
<Typography
component="span"
mt={theme.gap.xs}
sx={{ opacity: 0.8, fontWeight: 500 }}
>
BlueWave Uptime
</Typography>
</Stack>
<IconButton
sx={{
position: "absolute",
top: 60,
right: 0,
transform: `translate(50%, 50%)`,
transform: `translate(50%, 0)`,
backgroundColor: theme.palette.otherColors.fillGray,
border: `solid 1px ${theme.palette.otherColors.graishWhite}`,
p: "5px",
@@ -159,7 +174,10 @@ function Sidebar() {
borderColor: theme.palette.otherColors.graishWhite,
},
}}
onClick={() => dispatch(toggleSidebar())}
onClick={() => {
setOpen({ Dashboard: false, Account: false });
dispatch(toggleSidebar());
}}
>
{collapsed ? <ArrowRight /> : <ArrowLeft />}
</IconButton>
@@ -175,14 +193,13 @@ function Sidebar() {
id="nested-menu-subheader"
sx={{
pt: theme.gap.small,
px: collapsed ? theme.gap.xs : theme.gap.ml,
textAlign: collapsed ? "center" : "left",
px: collapsed ? theme.gap.xs : theme.gap.small,
}}
>
Menu
</ListSubheader>
}
sx={{ px: collapsed ? theme.gap.xs : theme.gap.ml }}
sx={{ px: theme.gap.medium }}
>
{menu.map((item) =>
item.path ? (
@@ -210,13 +227,14 @@ function Sidebar() {
}
onClick={() => navigate(`/${item.path}`)}
sx={{
gap: theme.gap.medium,
height: "37px",
gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
justifyContent: collapsed ? "center" : "flex-start",
px: theme.gap.small,
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
{!collapsed && <ListItemText>{item.name}</ListItemText>}
<ListItemText>{item.name}</ListItemText>
</ListItemButton>
</Tooltip>
) : collapsed ? (
@@ -247,13 +265,13 @@ function Sidebar() {
onClick={(event) => openPopup(event, item.name)}
sx={{
position: "relative",
gap: theme.gap.medium,
gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
justifyContent: collapsed ? "center" : "flex-start",
px: theme.gap.small,
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
{!collapsed && <ListItemText>{item.name}</ListItemText>}
<ListItemText>{item.name}</ListItemText>
</ListItemButton>
</Tooltip>
<Menu
@@ -265,7 +283,7 @@ function Sidebar() {
vertical: "top",
horizontal: "right",
}}
sx={{ ml: theme.gap.small }}
sx={{ ml: theme.gap.ml }}
>
{item.nested.map((child) => {
if (
@@ -292,6 +310,7 @@ function Sidebar() {
gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
pl: theme.gap.small,
mb: "1px",
}}
>
{child.icon}
@@ -311,9 +330,9 @@ function Sidebar() {
}))
}
sx={{
gap: theme.gap.medium,
gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
justifyContent: collapsed ? "center" : "flex-start",
px: theme.gap.small,
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
@@ -324,7 +343,7 @@ function Sidebar() {
<List
component="div"
disablePadding
sx={{ pl: theme.gap.lgplus }}
sx={{ pl: theme.gap.large }}
>
{item.nested.map((child) => {
if (
@@ -374,14 +393,13 @@ function Sidebar() {
id="nested-other-subheader"
sx={{
pt: theme.gap.small,
px: collapsed ? theme.gap.xs : theme.gap.ml,
textAlign: collapsed ? "center" : "left",
px: collapsed ? 0 : theme.gap.small,
}}
>
Other
</ListSubheader>
}
sx={{ px: collapsed ? theme.gap.xs : theme.gap.ml }}
sx={{ px: theme.gap.medium }}
>
{other.map((item) => (
<Tooltip
@@ -409,20 +427,20 @@ function Sidebar() {
onClick={() =>
item.path === "support"
? window.open(
"https://github.com/bluewave-labs/bluewave-uptime",
"https://github.com/bluewave-labs/bluewave-uptime/issues",
"_blank",
"noreferrer"
)
: navigate(`/${item.path}`)
}
sx={{
gap: theme.gap.medium,
gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
justifyContent: collapsed ? "center" : "flex-start",
px: theme.gap.small,
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
{!collapsed && <ListItemText>{item.name}</ListItemText>}
<ListItemText>{item.name}</ListItemText>
</ListItemButton>
</Tooltip>
))}
@@ -432,10 +450,9 @@ function Sidebar() {
<Stack
direction="row"
height="50px"
justifyContent={collapsed ? "center" : "flex-start"}
alignItems="center"
py={theme.gap.small}
px={theme.gap.medium}
px={theme.gap.ml}
gap={theme.gap.xs}
borderRadius={`${theme.shape.borderRadius}px`}
>
@@ -74,7 +74,8 @@ const TeamPanel = () => {
{ id: 1, name: "NAME" },
{ id: 2, name: "EMAIL" },
{ id: 3, name: "ROLE" },
{ id: 4, name: "ACTION" },
// FEATURE STILL TO BE IMPLEMENTED
// { id: 4, name: "ACTION" },
],
rows: team?.map((member, idx) => {
return {
@@ -101,22 +102,23 @@ const TeamPanel = () => {
id: idx + 2,
data: member.role.includes("admin") ? "Administrator" : "Member",
},
{
// TODO - Add delete onClick
id: idx + 3,
data: (
<IconButton
aria-label="remove member"
sx={{
"&:focus": {
outline: "none",
},
}}
>
<Remove />
</IconButton>
),
},
// FEATURE STILL TO BE IMPLEMENTED
// {
// // TODO - Add delete onClick
// id: idx + 3,
// data: (
// <IconButton
// aria-label="remove member"
// sx={{
// "&:focus": {
// outline: "none",
// },
// }}
// >
// <Remove />
// </IconButton>
// ),
// },
],
};
}),
@@ -197,7 +199,8 @@ const TeamPanel = () => {
return (
<TabPanel value="team">
<Stack component="form">
{/* FEATURE STILL TO BE IMPLEMENTED */}
{/* <Stack component="form">
<Box sx={{ alignSelf: "flex-start" }}>
<Typography component="h1">Organization name</Typography>
</Box>
@@ -242,7 +245,7 @@ const TeamPanel = () => {
/>
</Stack>
</Stack>
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(4) }} />
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(4) }} /> */}
<Stack
component="form"
noValidate
@@ -294,7 +297,12 @@ const TeamPanel = () => {
onClick={() => setIsOpen(true)}
/>
</Stack>
<BasicTable data={tableData} paginated={false} reversed={true} />
<BasicTable
data={tableData}
paginated={false}
reversed={true}
table={"team"}
/>
</Stack>
<Modal
aria-labelledby="modal-invite-member"
+26 -2
View File
@@ -55,8 +55,31 @@ const LandingPage = ({ onContinue }) => {
<Box maxWidth={400}>
<Typography className="tos-p">
By continuing, you agree to our{" "}
<Typography component="span">Terms of Service</Typography> and{" "}
<Typography component="span">Privacy Policy.</Typography>
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/terms-of-service-open-source",
"_blank",
"noreferrer"
);
}}
>
Terms of Service
</Typography>{" "}
and{" "}
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/privacy-policy-open-source",
"_blank",
"noreferrer"
);
}}
>
Privacy Policy.
</Typography>
</Typography>
</Box>
</Stack>
@@ -106,6 +129,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email}
ref={inputRef}
+26 -2
View File
@@ -60,8 +60,31 @@ const LandingPage = ({ isAdmin, onSignup }) => {
<Box maxWidth={400}>
<Typography className="tos-p">
By signing up, you agree to our{" "}
<Typography component="span">Terms of Service</Typography> and{" "}
<Typography component="span">Privacy Policy.</Typography>
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/terms-of-service-open-source",
"_blank",
"noreferrer"
);
}}
>
Terms of Service
</Typography>{" "}
and{" "}
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/privacy-policy-open-source",
"_blank",
"noreferrer"
);
}}
>
Privacy Policy.
</Typography>
</Typography>
</Box>
</Stack>
@@ -203,6 +226,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email}
ref={inputRef}
+14 -9
View File
@@ -2,24 +2,23 @@ import { useNavigate, useParams } from "react-router";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
import { useEffect, useState } from "react";
import Button from "../../../Components/Button";
import Field from "../../../Components/Inputs/Field";
import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import { monitorValidation } from "../../../Validation/validation";
import Select from "../../../Components/Inputs/Select";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import {
updateUptimeMonitor,
getUptimeMonitorsByUserId,
deleteUptimeMonitor,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Button from "../../../Components/Button";
import Field from "../../../Components/Inputs/Field";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import "./index.css";
import { logger } from "../../../Utils/Logger";
/**
* Parses a URL string and returns a URL object.
@@ -229,8 +228,14 @@ const Configure = () => {
gap={theme.gap.large}
flex={1}
>
<Stack direction="row" gap={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
<Stack direction="row" gap={theme.gap.xs}>
<PulseDot
color={
monitor?.status
? theme.label.up.dotColor
: theme.label.down.dotColor
}
/>
<Box>
{parsedUrl?.host ? (
<Typography component="h1" mb={theme.gap.xs} lineHeight={1}>
+16 -13
View File
@@ -4,20 +4,19 @@ import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { networkService } from "../../../main";
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "../../../Components/Button";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import PaginationTable from "./PaginationTable";
import { logger } from "../../../Utils/Logger";
import {
formatDuration,
formatDurationRounded,
} from "../../../Utils/timeUtils";
import "./index.css";
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "../../../Components/Button";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import PaginationTable from "./PaginationTable";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { logger } from "../../../Utils/Logger";
import PulseDot from "../../../Components/Animated/PulseDot";
import "./index.css";
const StatBox = ({ title, value }) => {
return (
@@ -171,8 +170,14 @@ const DetailsPage = () => {
]}
/>
<Stack gap={theme.gap.large} mt={theme.gap.large}>
<Stack direction="row" gap={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
<Stack direction="row" gap={theme.gap.xs}>
<PulseDot
color={
monitor?.status
? theme.label.up.dotColor
: theme.label.down.dotColor
}
/>
<Box>
<Typography component="h1" sx={{ lineHeight: 1 }}>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
@@ -325,9 +330,7 @@ const DetailsPage = () => {
</Box>
<Stack gap={theme.gap.ml}>
<Typography component="h2">History</Typography>
{/* TODO New Table */}
<PaginationTable monitorId={monitorId} dateRange={dateRange} />
{/* <BasicTable data={data} paginated={true} /> */}
</Stack>
</Stack>
</>
+9 -34
View File
@@ -10,11 +10,9 @@ import Button from "../../Components/Button";
import ServerStatus from "../../Components/Charts/Servers/ServerStatus";
import { useTheme } from "@emotion/react";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import OpenInNewPage from "../../assets/icons/open-in-new-page.svg?react";
import BasicTable from "../../Components/BasicTable";
import { StatusLabel } from "../../Components/Label";
import { createToast } from "../../Utils/toastUtils";
import ResponseTimeChart from "../../Components/Charts/ResponseTimeChart";
import {
Box,
IconButton,
@@ -28,6 +26,7 @@ import {
import Settings from "../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import BarChart from "../../Components/Charts/BarChart";
const ActionsMenu = ({ monitor }) => {
const [anchorEl, setAnchorEl] = useState(null);
@@ -206,7 +205,6 @@ ActionsMenu.propTypes = {
*
* @component
* @param {Object} params - An object containing the following properties:
* @param {string} params.url - The URL of the host.
* @param {string} params.title - The name of the host.
* @param {string} params.percentageColor - The color of the percentage text.
* @param {number} params.percentage - The percentage to display.
@@ -214,40 +212,17 @@ ActionsMenu.propTypes = {
*/
const Host = ({ params }) => {
return (
<Stack direction="row" alignItems="center" className="host">
<IconButton
aria-label="monitor link"
onClick={(event) => {
event.stopPropagation();
window.open(params.url, "_blank", "noreferrer");
}}
sx={{
"&:focus": {
outline: "none",
},
mr: "5px",
}}
>
<OpenInNewPage
style={{
marginTop: "-1px",
marginRight: "-1px",
}}
/>
</IconButton>
<Box>
{params.title}
<Typography component="span" sx={{ color: params.percentageColor }}>
{params.percentage}%
</Typography>
</Box>
<Stack direction="row" alignItems="baseline" className="host">
{params.title}
<Typography component="span" sx={{ color: params.percentageColor }}>
{params.percentage}%
</Typography>
</Stack>
);
};
Host.propTypes = {
params: PropTypes.shape({
url: PropTypes.string,
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.number,
@@ -326,7 +301,6 @@ const Monitors = () => {
data.rows = monitorState.monitors.map((monitor, idx) => {
const params = {
url: monitor.url,
title: monitor.name,
percentage: 100,
percentageColor:
@@ -357,7 +331,7 @@ const Monitors = () => {
/>
),
},
{ id: idx + 2, data: <ResponseTimeChart checks={reversedChecks} /> },
{ id: idx + 2, data: <BarChart checks={reversedChecks} /> },
{
id: idx + 3,
data: (
@@ -437,6 +411,7 @@ const Monitors = () => {
<ServerStatus title="Paused" value={0} state="pause" />
</Stack>
<Box
flex={1}
p={theme.gap.lgplus}
border={1}
borderColor={theme.palette.otherColors.graishWhite}
@@ -452,7 +427,7 @@ const Monitors = () => {
</Box>
{/* TODO - add search bar */}
</Stack>
<BasicTable data={data} paginated={true} />
<BasicTable data={data} paginated={true} table={"monitors"} />
</Box>
</>
)}
+10 -5
View File
@@ -10,15 +10,14 @@ import {
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import Button from "../../../Components/Button";
import Field from "../../../Components/Inputs/Field";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { logger } from "../../../Utils/Logger";
import PulseDot from "../../../Components/Animated/PulseDot";
import "./index.css";
@@ -168,8 +167,14 @@ const PageSpeedConfigure = () => {
flex={1}
gap={theme.gap.large}
>
<Stack direction="row" gap={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
<Stack direction="row" gap={theme.gap.xs}>
<PulseDot
color={
monitor?.status
? theme.label.up.dotColor
: theme.label.down.dotColor
}
/>
<Box>
<Typography
component="h1"
+11 -6
View File
@@ -1,3 +1,4 @@
import PropTypes from "prop-types";
import { Box, Skeleton, Stack, Typography } from "@mui/material";
import { PieChart } from "@mui/x-charts/PieChart";
import { useDrawingArea } from "@mui/x-charts";
@@ -9,19 +10,17 @@ import {
formatDuration,
formatDurationRounded,
} from "../../../Utils/timeUtils";
import { logger } from "../../../Utils/Logger";
import { networkService } from "../../../main";
import Button from "../../../Components/Button";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import LastCheckedIcon from "../../../assets/icons/calendar-check.svg?react";
import ClockIcon from "../../../assets/icons/maintenance.svg?react";
import IntervalCheckIcon from "../../../assets/icons/interval-check.svg?react";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import PageSpeedLineChart from "../../../Components/Charts/PagespeedLineChart";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import "./index.css";
import PropTypes from "prop-types";
import { logger } from "../../../Utils/Logger";
const StatBox = ({ icon, title, value }) => {
const theme = useTheme();
@@ -325,8 +324,14 @@ const PageSpeedDetails = () => {
{ name: "details", path: `/pagespeed/${monitorId}` },
]}
/>
<Stack direction="row" gap={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
<Stack direction="row" gap={theme.gap.xs}>
<PulseDot
color={
monitor?.status
? theme.label.up.dotColor
: theme.label.down.dotColor
}
/>
<Box>
<Typography
component="h1"
+6
View File
@@ -231,6 +231,12 @@ class NetworkService {
});
}
async deleteUser(authToken, userId) {
return this.axiosInstance.delete(`/auth/user/${userId}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
}
/**
* ************************************
* Forgot password request
+2 -1
View File
@@ -43,7 +43,7 @@ export const formatDurationRounded = (ms) => {
return time;
};
export const formatDate = (date) => {
export const formatDate = (date, customOptions) => {
const options = {
year: "numeric",
month: "long",
@@ -51,6 +51,7 @@ export const formatDate = (date) => {
hour: "numeric",
minute: "numeric",
hour12: true,
...customOptions
};
// Return the date using the specified options
+2 -1
View File
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
import { toast } from "react-toastify";
import { toast, Slide } from "react-toastify";
import Alert from "../Components/Alert";
/**
@@ -23,6 +23,7 @@ export const createToast = ({
autoClose: 3000,
hideProgressBar: true,
closeButton: false,
transition: Slide,
...config,
};
+11 -8
View File
@@ -49,6 +49,13 @@ const credentials = joi.object({
.string()
.trim()
.email({ tlds: { allow: false } })
.custom((value, helpers) => {
const lowercasedValue = value.toLowerCase();
if (value !== lowercasedValue) {
return helpers.message("Email must be in lowercase");
}
return lowercasedValue;
})
.messages({
"string.empty": "Email is required",
"string.email": "Must be a valid email address",
@@ -72,14 +79,10 @@ const credentials = joi.object({
});
const monitorValidation = joi.object({
url: joi
.string()
.uri({ allowRelative: true })
.trim()
.messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
}),
url: joi.string().uri({ allowRelative: true }).trim().messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
@@ -1,4 +0,0 @@
<svg width="26" height="16" viewBox="0 0 26 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.761364 15V0.454545H5.84659C6.85985 0.454545 7.69555 0.629735 8.35369 0.980114C9.01184 1.32576 9.50189 1.79214 9.82386 2.37926C10.1458 2.96165 10.3068 3.60795 10.3068 4.31818C10.3068 4.94318 10.1955 5.45928 9.97301 5.86648C9.75521 6.27367 9.46638 6.59564 9.10653 6.83239C8.75142 7.06913 8.36553 7.24432 7.94886 7.35795V7.5C8.39394 7.52841 8.84138 7.68466 9.29119 7.96875C9.741 8.25284 10.1174 8.66004 10.4205 9.19034C10.7235 9.72064 10.875 10.3693 10.875 11.1364C10.875 11.8655 10.7093 12.5213 10.3778 13.1037C10.0464 13.6861 9.5232 14.1477 8.80824 14.4886C8.09328 14.8295 7.16288 15 6.01705 15H0.761364ZM2.52273 13.4375H6.01705C7.16761 13.4375 7.98438 13.215 8.46733 12.7699C8.95502 12.3201 9.19886 11.7756 9.19886 11.1364C9.19886 10.6439 9.07339 10.1894 8.82244 9.77273C8.5715 9.35133 8.21402 9.01515 7.75 8.7642C7.28598 8.50852 6.73674 8.38068 6.10227 8.38068H2.52273V13.4375ZM2.52273 6.84659H5.78977C6.32008 6.84659 6.7983 6.74242 7.22443 6.53409C7.6553 6.32576 7.99621 6.0322 8.24716 5.65341C8.50284 5.27462 8.63068 4.82955 8.63068 4.31818C8.63068 3.67898 8.40814 3.13684 7.96307 2.69176C7.51799 2.24195 6.8125 2.01705 5.84659 2.01705H2.52273V6.84659Z" fill="#344054"/>
<path d="M23.3146 0.454545H25.076V10.0852C25.076 11.0795 24.8416 11.9673 24.3729 12.7486C23.9089 13.5251 23.2531 14.1383 22.4055 14.5881C21.558 15.0331 20.5637 15.2557 19.4226 15.2557C18.2815 15.2557 17.2872 15.0331 16.4396 14.5881C15.5921 14.1383 14.9339 13.5251 14.4652 12.7486C14.0012 11.9673 13.7692 11.0795 13.7692 10.0852V0.454545H15.5305V9.94318C15.5305 10.6534 15.6868 11.2855 15.9993 11.8395C16.3118 12.3887 16.7569 12.822 17.3345 13.1392C17.9169 13.4517 18.6129 13.608 19.4226 13.608C20.2322 13.608 20.9283 13.4517 21.5107 13.1392C22.093 12.822 22.5381 12.3887 22.8459 11.8395C23.1584 11.2855 23.3146 10.6534 23.3146 9.94318V0.454545Z" fill="#1570EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

+12 -1
View File
@@ -67,7 +67,7 @@
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
--env-var-height-2: 36px;
--env-var-height-2: 34px;
--env-var-nav-bar-height: 70px;
--env-var-side-bar-width: 250px;
@@ -168,6 +168,17 @@ body .MuiSkeleton-root {
background-color: var(--env-var-color-15);
}
@keyframes ripple {
from {
opacity: 1;
transform: scale(0);
}
to {
opacity: 0;
transform: scale(2);
}
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
+2 -1
View File
@@ -2,4 +2,5 @@
!build_images.sh
mongo/data/*
redis/data/*
*.env
*.env
certbot/*
+1
View File
@@ -16,6 +16,7 @@ RUN npm run build
FROM nginx:1.27.1-alpine
# COPY ./Docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
+11 -1
View File
@@ -1,11 +1,21 @@
version: "3"
services:
client:
image: uptime_client:latest
ports:
- "80:80"
- "443:443"
depends_on:
- server
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d/:ro
- ./certbot/www:/var/www/certbot/:ro
- ./certbot/conf/:/etc/nginx/ssl/:ro
certbot:
image: certbot/certbot:latest
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
server:
image: uptime_server:latest
ports:
+51
View File
@@ -0,0 +1,51 @@
server {
listen 80;
listen [::]:80;
server_name uptime-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://server:5000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 default_server ssl http2;
listen [::]:443 ssl http2;
server_name uptime-demo.bluewavelabs.ca;
ssl_certificate /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/privkey.pem;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://server:5000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+2 -2
View File
@@ -90,7 +90,7 @@ const registerController = async (req, res, next) => {
const token = issueToken(userForToken);
req.emailService.buildAndSendEmail(
await req.emailService.buildAndSendEmail(
"welcomeEmailTemplate",
{ name: newUser.firstName },
newUser.email,
@@ -240,7 +240,7 @@ const inviteController = async (req, res, next) => {
}
const inviteToken = await req.db.requestInviteToken(req, res);
req.emailService.buildAndSendEmail(
await req.emailService.buildAndSendEmail(
"employeeActivationTemplate",
{
name: firstname,
+56 -21
View File
@@ -3,6 +3,8 @@ const path = require("path");
const nodemailer = require("nodemailer");
const { compile } = require("handlebars");
const mjml2html = require("mjml");
const SERVICE_NAME = "EmailService";
const logger = require("../utils/logger");
/**
* Represents an email service that can load templates, build, and send emails.
@@ -19,12 +21,19 @@ class EmailService {
* @returns {Function} A compiled template function that can be used to generate HTML email content.
*/
this.loadTemplate = (templateName) => {
const templatePath = path.join(
__dirname,
`../templates/${templateName}.mjml`
);
const templateContent = fs.readFileSync(templatePath, "utf8");
return compile(templateContent);
try {
const templatePath = path.join(
__dirname,
`../templates/${templateName}.mjml`
);
const templateContent = fs.readFileSync(templatePath, "utf8");
return compile(templateContent);
} catch (error) {
logger.error("Error loading Email templates", {
error,
service: this.SERVICE_NAME,
});
}
};
/**
@@ -67,25 +76,51 @@ class EmailService {
*/
buildAndSendEmail = async (template, context, to, subject) => {
const buildHtml = async (template, context) => {
const mjml = this.templateLookup[template](context);
const html = await mjml2html(mjml);
return html.html;
try {
const mjml = this.templateLookup[template](context);
const html = await mjml2html(mjml);
return html.html;
} catch (error) {
logger.error("Error building Email HTML", {
error,
service: SERVICE_NAME,
});
}
};
const sendEmail = async (to, subject, html) => {
const info = await this.transporter.sendMail({
to: to,
subject: subject,
html: html,
});
return info;
try {
const info = await this.transporter.sendMail({
to: to,
subject: subject,
html: html,
});
return info;
} catch (error) {
logger.error("Error sending Email", {
error,
service: SERVICE_NAME,
});
}
};
const info = await sendEmail(
to,
subject,
await buildHtml(template, context)
);
return info.messageId;
try {
const info = await sendEmail(
to,
subject,
await buildHtml(template, context)
);
return info.messageId;
} catch (error) {
error.service = SERVICE_NAME;
if (error.method === undefined) {
error.method = "buildAndSendEmail";
}
logger.error("Error building and sending Email", {
error,
service: SERVICE_NAME,
});
}
};
}
+27 -3
View File
@@ -16,7 +16,17 @@ const roleValidatior = (role) => (value, helpers) => {
//****************************************
const loginValidation = joi.object({
email: joi.string().email().required(),
email: joi
.string()
.email()
.required()
.custom((value, helpers) => {
const lowercasedValue = value.toLowerCase();
if (value !== lowercasedValue) {
return helpers.message("Email must be in lowercase");
}
return lowercasedValue;
}),
password: joi
.string()
.min(8)
@@ -35,7 +45,17 @@ const registerValidation = joi.object({
.string()
.required()
.pattern(/^[A-Za-z]+$/),
email: joi.string().email().required(),
email: joi
.string()
.email()
.required()
.custom((value, helpers) => {
const lowercasedValue = value.toLowerCase();
if (value !== lowercasedValue) {
return helpers.message("Email must be in lowercase");
}
return lowercasedValue;
}),
password: joi
.string()
.min(8)
@@ -44,7 +64,11 @@ const registerValidation = joi.object({
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()])[A-Za-z0-9!@#$%^&*()]+$/
),
profileImage: joi.any(),
role: joi.array().required(),
role: joi
.array()
.items(joi.string().valid("admin", "user"))
.min(1)
.required(),
});
const editUserParamValidation = joi.object({