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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -231,6 +231,12 @@ class NetworkService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUser(authToken, userId) {
|
||||
return this.axiosInstance.delete(`/auth/user/${userId}`, {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Forgot password request
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -2,4 +2,5 @@
|
||||
!build_images.sh
|
||||
mongo/data/*
|
||||
redis/data/*
|
||||
*.env
|
||||
*.env
|
||||
certbot/*
|
||||
@@ -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;"]
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user