Merge branch 'develop' into feat/fe/statuspage-3

This commit is contained in:
Shemy Gan committed 2024-12-24 10:11:11 -05:00
commit feaf1e3272
66 files changed
+3168 -2702

No files matched your search

+21 -22
View File
@@ -15,8 +15,8 @@
"@mui/lab": "6.0.0-beta.20",
"@mui/material": "6.2.1",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.23.2",
"@mui/x-date-pickers": "7.23.2",
"@mui/x-data-grid": "7.23.3",
"@mui/x-date-pickers": "7.23.3",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
@@ -1451,9 +1451,9 @@
}
},
"node_modules/@mui/x-data-grid": {
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.2.tgz",
"integrity": "sha512-Xhm4Bh+WiU5MSLFIZ8mE1egHoS0xPEVlwvjYvairPIkxNPubB7f+gtLkuAJ2+m+BYbgO7yDte+Pp7dfjkU+vOA==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.3.tgz",
"integrity": "sha512-EiM5kut6N/0o0iEYx8A7M3fJqknAa1kcPvGhlX3hH50ERLDeuJaqoKzvRYLBbYKWydHIc+0hHIFcK5oQTXLenw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1488,9 +1488,9 @@
}
},
"node_modules/@mui/x-date-pickers": {
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.2.tgz",
"integrity": "sha512-Kt9VsEnShaBKiaastTYku66UIWptgc9UMA16d0G/0TkfIsvZrAD3iacQR6HHAXWspaFshdfsRmW2JAoFhzKZsg==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.3.tgz",
"integrity": "sha512-bjTYX/QzD5ZhVZNNnastMUS3j2Hy4p4IXmJgPJ0vKvQBvUdfEO+ZF42r3PJNNde0FVT1MmTzkmdTlz0JZ6ukdw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -2390,9 +2390,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.17.tgz",
"integrity": "sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==",
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -4939,16 +4939,15 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -5455,9 +5454,9 @@
}
},
"node_modules/react-router": {
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz",
"integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==",
"version": "6.28.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz",
"integrity": "sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.21.0"
@@ -5470,13 +5469,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz",
"integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==",
"version": "6.28.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.1.tgz",
"integrity": "sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.21.0",
"react-router": "6.28.0"
"react-router": "6.28.1"
},
"engines": {
"node": ">=14.0.0"
+2 -2
View File
@@ -18,8 +18,8 @@
"@mui/lab": "6.0.0-beta.20",
"@mui/material": "6.2.1",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.23.2",
"@mui/x-date-pickers": "7.23.2",
"@mui/x-data-grid": "7.23.3",
"@mui/x-date-pickers": "7.23.3",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
+16 -3
View File
@@ -115,7 +115,7 @@ TablePaginationActions.propTypes = {
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
* @param {number} props.rowsPerPage- Number of rows per page (table).
*
* @param {string} props.emptyMessage - Message to display when there is no data.
* @example
* const data = {
* cols: [
@@ -149,7 +149,7 @@ TablePaginationActions.propTypes = {
* <BasicTable data={data} rows={rows} paginated={true} />
*/
const BasicTable = ({ data, paginated, reversed, table }) => {
const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => {
const DEFAULT_ROWS_PER_PAGE = 5;
const theme = useTheme();
const dispatch = useDispatch();
@@ -216,7 +216,9 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
sx={{
cursor: row.handleClick ? "pointer" : "default",
"&:hover": {
backgroundColor: theme.palette.background.accent,
filter: "brightness(.75)",
opacity: 0.75,
transition: "filter 0.3s ease, opacity 0.3s ease",
},
}}
key={row.id}
@@ -228,6 +230,16 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
</TableRow>
);
})}
{displayData.length === 0 && (
<TableRow>
<TableCell
sx={{ textAlign: "center" }}
colSpan={data.cols.length}
>
{emptyMessage}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
@@ -317,6 +329,7 @@ BasicTable.propTypes = {
reversed: PropTypes.bool,
rowPerPage: PropTypes.number,
table: PropTypes.string,
emptyMessage: PropTypes.string,
};
export default BasicTable;
@@ -1,12 +1,15 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
backgroundColor: theme.palette.background.main,
"& > *": {
paddingTop: theme.spacing(12),
paddingBottom: theme.spacing(18),
@@ -24,15 +27,12 @@ export const ConfigBox = styled(Stack)(({ theme }) => ({
paddingRight: theme.spacing(20),
paddingLeft: theme.spacing(18),
},
"& h2": {
"& h1, & h2": {
color: theme.palette.text.secondary,
fontSize: 15,
fontWeight: 600,
},
"& h3, & p": {
color: theme.palette.text.tertiary,
},
"& p": {
fontSize: 13,
color: theme.palette.text.tertiary,
},
}));
export default ConfigBox;
+77
View File
@@ -0,0 +1,77 @@
import { Box, styled } from "@mui/material";
import PropTypes from "prop-types";
/**
* IconBox - A styled box component for rendering icons with consistent sizing and styling
*
* @component
* @param {Object} [props] - Configuration options for the IconBox
* @param {number} [props.height=34] - Height of the icon box
* @param {number} [props.width=34] - Width of the icon box
* @param {number} [props.minWidth=34] - Minimum width of the icon box
* @param {number} [props.borderRadius=4] - Border radius of the icon box
* @param {number} [props.svgWidth=20] - Width of the SVG icon
* @param {number} [props.svgHeight=20] - Height of the SVG icon
*
* @example
* // Basic usage
* <IconBox>
* <SomeIcon />
* </IconBox>
*
* @example
* // Customized usage
* <IconBox
* height={40}
* width={40}
* svgWidth={24}
* svgHeight={24}
* >
* <CustomIcon />
* </IconBox>
*
* @returns {React.ReactElement} A styled box containing an icon
*/
const IconBox = styled(Box)(
({
theme,
height = 34,
width = 34,
minWidth = 34,
borderRadius = 4,
svgWidth = 20,
svgHeight = 20,
}) => ({
height: height,
minWidth: minWidth,
width: width,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: borderRadius,
backgroundColor: theme.palette.background.accent,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: svgWidth,
height: svgHeight,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
})
);
IconBox.propTypes = {
height: PropTypes.number,
width: PropTypes.number,
minWidth: PropTypes.number,
borderRadius: PropTypes.number,
svgWidth: PropTypes.number,
svgHeight: PropTypes.number,
};
export default IconBox;
@@ -42,6 +42,9 @@ const Radio = (props) => {
width: 16,
height: 16,
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
"&:not(.Mui-checked)": {
boxShadow: `inset 0 0 0 1px ${theme.palette.text.primary}70`, // Use theme text color for the outline
},
mt: theme.spacing(0.5),
}}
/>
@@ -40,6 +40,7 @@ const SearchAdornment = () => {
//TODO keep search state inside of component
const Search = ({
label,
id,
options,
filteredBy,
@@ -76,6 +77,14 @@ const Search = ({
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
<Stack>
<Typography
component="h3"
fontSize={"var(--env-var-font-size-medium)"}
color={theme.palette.text.secondary}
fontWeight={500}
>
{label}
</Typography>
<TextField
{...params}
error={Boolean(error)}
@@ -26,7 +26,7 @@ const ProtectedRoute = ({ children }) => {
};
ProtectedRoute.propTypes = {
children: PropTypes.elementType.isRequired,
children: PropTypes.element.isRequired,
};
export default ProtectedRoute;
+42 -22
View File
@@ -21,6 +21,7 @@ import { useDispatch, useSelector } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import ThemeSwitch from "../ThemeSwitch";
import Avatar from "../Avatar";
import LockSvg from "../../assets/icons/lock.svg?react";
import UserSvg from "../../assets/icons/user.svg?react";
@@ -43,6 +44,7 @@ import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
import Folder from "../../assets/icons/folder.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
import ChatBubbleOutlineRoundedIcon from "@mui/icons-material/ChatBubbleOutlineRounded";
import "./index.css";
@@ -64,20 +66,31 @@ const menu = [
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
],
},
{
name: "Settings",
icon: <Settings />,
path: "settings",
},
{
name: "Other",
icon: <Folder />,
nested: [
{ name: "Settings", path: "settings", icon: <Settings /> },
{ name: "Support", path: "support", icon: <Support /> },
{
name: "Discussions",
path: "discussions",
icon: <ChatBubbleOutlineRoundedIcon />,
},
{ name: "Docs", path: "docs", icon: <Docs /> },
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
],
},
];
/* TODO this could be a key in nested Path would be the link */
const URL_MAP = {
support: "https://github.com/bluewave-labs/bluewave-uptime/issues",
support: "https://discord.com/invite/NAb6H3UTjK",
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
docs: "https://bluewavelabs.gitbook.io/checkmate",
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
};
@@ -546,28 +559,35 @@ function Sidebar() {
{authState.user?.role}
</Typography>
</Box>
<Tooltip
title="Controls"
disableInteractive
<Stack
flexDirection={"row"}
marginLeft={"auto"}
columnGap={theme.spacing(2)}
>
<IconButton
sx={{
ml: "auto",
mr: "-8px",
"&:focus": { outline: "none" },
"& svg": {
width: "20px",
height: "20px",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
onClick={(event) => openPopup(event, "logout")}
<ThemeSwitch />
<Tooltip
title="Controls"
disableInteractive
>
<DotsVertical />
</IconButton>
</Tooltip>
<IconButton
sx={{
ml: "auto",
mr: "-8px",
"&:focus": { outline: "none" },
"& svg": {
width: "20px",
height: "20px",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
onClick={(event) => openPopup(event, "logout")}
>
<DotsVertical />
</IconButton>
</Tooltip>
</Stack>
</>
)}
<Menu
+74
View File
@@ -0,0 +1,74 @@
import { Box, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
/**
* StatBox Component
*
* A reusable component that displays a statistic with a heading and subheading
* in a styled box with a gradient background.
*
* @component
* @param {Object} props - The component props
* @param {string} props.heading - The primary heading/title of the statistic
* @param {string|React.ReactNode} props.subHeading - The value or description of the statistic
* @param {Object} [props.sx] - Additional custom styling to be applied to the box
*
* @example
* return (
* <StatBox
* heading="Total Users"
* subHeading="1,234"
* sx={{ width: 300 }}
* />
* )
*
* @returns {React.ReactElement} A styled box containing the statistic
*/
const StatBox = ({ heading, subHeading, sx }) => {
const theme = useTheme();
return (
<Box
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
minWidth: 200,
width: 225,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,
color: theme.palette.text.secondary,
textTransform: "uppercase",
},
"& p": {
fontSize: 18,
color: theme.palette.text.primary,
marginTop: theme.spacing(2),
"& span": {
color: theme.palette.text.tertiary,
marginLeft: theme.spacing(2),
fontSize: 15,
},
},
...sx,
}}
>
<Typography component="h2">{heading}</Typography>
<Typography>{subHeading}</Typography>
</Box>
);
};
StatBox.propTypes = {
heading: PropTypes.string.isRequired,
subHeading: PropTypes.node.isRequired,
sx: PropTypes.object,
};
export default StatBox;
@@ -64,7 +64,7 @@ const TeamPanel = () => {
};
fetchTeam();
}, [user]);
}, [authToken]);
useEffect(() => {
let team = members;
@@ -81,8 +81,6 @@ const TeamPanel = () => {
{ id: 1, name: "NAME" },
{ id: 2, name: "EMAIL" },
{ id: 3, name: "ROLE" },
// FEATURE STILL TO BE IMPLEMENTED
// { id: 4, name: "ACTION" },
],
rows: team?.map((member, idx) => {
const roles = member.role.map((role) => roleMap[role]).join(",");
@@ -104,45 +102,19 @@ const TeamPanel = () => {
},
{ id: idx + 1, data: member.email },
{
// TODO - Add select dropdown
id: idx + 2,
data: roles,
},
// FEATURE STILL TO BE IMPLEMENTED
// {
// // TODO - Add delete onClick
// id: idx + 3,
// data: (
// <IconButton
// aria-label="remove member"
// sx={{
// "&:focus": {
// outline: "none",
// },
// }}
// >
// <Remove />
// </IconButton>
// ),
// },
],
};
}),
};
setTableData(data);
}, [members, filter]);
}, [filter, members, roleMap, theme]);
useEffect(() => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
}, [errors, toInvite.email]);
// RENAME ORGANIZATION
// const toggleEdit = () => {
// setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
// };
// const handleRename = () => {};
// INVITE MEMBER
const [isOpen, setIsOpen] = useState(false);
const handleChange = (event) => {
@@ -226,58 +198,11 @@ const TeamPanel = () => {
},
}}
>
{/* FEATURE STILL TO BE IMPLEMENTED */}
{/* <Stack component="form">
<Box sx={{ alignSelf: "flex-start" }}>
<Typography component="h1">Organization name</Typography>
</Box>
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
sx={{ height: "34px" }}
>
<TextField
value={orgStates.name}
onChange={(event) =>
setOrgStates((prev) => ({
...prev,
name: event.target.value,
}))
}
disabled={!orgStates.isEdit}
sx={{
color: theme.palette.otherColors.bluishGray,
"& .Mui-disabled": {
WebkitTextFillColor: "initial !important",
},
"& .Mui-disabled fieldset": {
borderColor: "transparent !important",
},
}}
inputProps={{
sx: { textAlign: "end", padding: theme.spacing(4) },
}}
/>
<Button
level={orgStates.isEdit ? "secondary" : "tertiary"}
label={orgStates.isEdit ? "Save" : ""}
img={!orgStates.isEdit ? <EditSvg /> : ""}
onClick={() => toggleEdit()}
sx={{
minWidth: 0,
paddingX: theme.spacing(4),
ml: orgStates.isEdit ? theme.spacing(4) : 0,
}}
/>
</Stack>
</Stack>
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(4) }} /> */}
<Stack
component="form"
noValidate
spellCheck="false"
gap={SPACING_GAP}
gap={SPACING_GAP}
>
<Typography component="h1">Team members</Typography>
<Stack
@@ -328,6 +253,7 @@ const TeamPanel = () => {
paginated={false}
reversed={true}
table={"team"}
emptyMessage={"There are no team members with this role"}
/>
</Stack>
@@ -341,7 +267,7 @@ const TeamPanel = () => {
theme={theme}
>
<TextInput
marginBottom={SPACING_GAP}
marginBottom={SPACING_GAP}
type="email"
id="input-team-member"
placeholder="Email"
@@ -0,0 +1,98 @@
import { useTheme } from "@mui/material";
import "./index.css";
const SunAndMoonIcon = () => {
const theme = useTheme();
return (
<svg
className="sun-and-moon"
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
>
<mask
className="moon"
id="moon-mask"
>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="#fff"
/>
<circle
cx="24"
cy="10"
r="6"
fill="#000"
/>
</mask>
<circle
className="sun"
cx="12"
cy="12"
r="6"
fill={theme.palette.text.secondary}
mask="url(#moon-mask)"
/>
<g
className="sun-beams"
stroke={theme.palette.text.secondary}
>
<line
x1="12"
y1="1"
x2="12"
y2="3"
/>
<line
x1="12"
y1="21"
x2="12"
y2="23"
/>
<line
x1="4.22"
y1="4.22"
x2="5.64"
y2="5.64"
/>
<line
x1="18.36"
y1="18.36"
x2="19.78"
y2="19.78"
/>
<line
x1="1"
y1="12"
x2="3"
y2="12"
/>
<line
x1="21"
y1="12"
x2="23"
y2="12"
/>
<line
x1="4.22"
y1="19.78"
x2="5.64"
y2="18.36"
/>
<line
x1="18.36"
y1="5.64"
x2="19.78"
y2="4.22"
/>
</g>
</svg>
);
};
export default SunAndMoonIcon;
@@ -0,0 +1,64 @@
.sun-and-moon > :is(.moon, .sun, .sun-beams) {
transform-origin: center;
}
.theme-toggle .sun-and-moon > .sun-beams {
stroke-width: 2px;
}
.theme-dark .sun-and-moon > .sun {
transform: scale(1.75);
}
.theme-dark .sun-and-moon > .sun-beams {
opacity: 0;
}
.theme-dark .sun-and-moon > .moon > circle {
transform: translateX(-7px);
}
@supports (cx: 1) {
.theme-dark .sun-and-moon > .moon > circle {
cx: 17;
transform: translateX(0);
}
}
@media (prefers-reduced-motion: no-preference) {
.sun-and-moon > .sun {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.sun-and-moon > .sun-beams {
transition:
transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55),
opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.sun-and-moon .moon > circle {
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
@supports (cx: 1) {
.sun-and-moon .moon > circle {
transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.theme-dark .sun-and-moon > .sun {
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
transition-duration: 0.25s;
transform: scale(1.75);
}
.theme-dark .sun-and-moon > .sun-beams {
transition-duration: 0.15s;
transform: rotateZ(-25deg);
}
.theme-dark .sun-and-moon > .moon > circle {
transition-duration: 0.5s;
transition-delay: 0.25s;
}
}
@@ -0,0 +1,47 @@
/**
* ThemeSwitch Component
* Dark and Light Theme Switch
* Original Code: https://web.dev/patterns/theming/theme-switch
* License: Apache License 2.0
* Copyright © Google LLC
*
* This code has been adapted for use in this project.
* Apache License: https://www.apache.org/licenses/LICENSE-2.0
*/
import { IconButton } from "@mui/material";
import SunAndMoonIcon from "./SunAndMoonIcon";
import { useDispatch, useSelector } from "react-redux";
import { setMode } from "../../Features/UI/uiSlice";
import "./index.css";
const ThemeSwitch = ({ width = 48, height = 48 }) => {
const mode = useSelector((state) => state.ui.mode);
const dispatch = useDispatch();
const toggleTheme = () => {
dispatch(setMode(mode === "light" ? "dark" : "light"));
};
return (
<IconButton
id="theme-toggle"
title="Toggles light & dark"
className={`theme-${mode}`}
aria-label="auto"
aria-live="polite"
onClick={toggleTheme}
sx={{
width,
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<SunAndMoonIcon />
</IconButton>
);
};
export default ThemeSwitch;
+1
View File
@@ -92,6 +92,7 @@ const Account = ({ open = "profile" }) => {
paddingY: theme.spacing(4),
fontWeight: 400,
marginRight: theme.spacing(8),
borderBottom: "2px solid transparent",
"&:focus": {
borderBottom: `2px solid ${theme.palette.border.light}`,
},
Whitespace-only changes.
-361
View File
@@ -1,361 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import TextInput from "../../Components/Inputs/TextInput";
import Link from "../../Components/Link";
import "./index.css";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
import PropTypes from "prop-types";
import LoadingButton from "@mui/lab/LoadingButton";
import { ConfigBox } from "../Settings/styled";
import { useNavigate } from "react-router";
import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
import { useState, useEffect } from "react";
import Select from "../../Components/Inputs/Select";
import { advancedSettingsValidation } from "../../Validation/validation";
import { buildErrors, hasValidationErrors } from "../../Validation/error";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
const AdvancedSettings = () => {
const navigate = useNavigate();
const isAdmin = useIsAdmin();
useEffect(() => {
if (!isAdmin) {
navigate("/");
}
}, [navigate, isAdmin]);
const [errors, setErrors] = useState({});
const theme = useTheme();
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const settings = useSelector((state) => state.settings);
const [localSettings, setLocalSettings] = useState({
apiBaseUrl: "",
logLevel: "debug",
systemEmailHost: "",
systemEmailPort: "",
systemEmailAddress: "",
systemEmailPassword: "",
jwtTTLNum: 99,
jwtTTLUnits: "days",
jwtTTL: "99d",
dbType: "",
redisHost: "",
redisPort: "",
pagespeedApiKey: "",
});
const parseJWTTTL = (data) => {
if (data.jwtTTL) {
const len = data.jwtTTL.length;
data.jwtTTLNum = data.jwtTTL.substring(0, len - 1);
data.jwtTTLUnits = unitItems.filter(
(itm) => itm._id == data.jwtTTL.substring(len - 1)
)[0].name;
}
};
useEffect(() => {
const getSettings = async () => {
const action = await dispatch(getAppSettings({ authToken }));
if (action.payload.success) {
parseJWTTTL(action.payload.data);
setLocalSettings(action.payload.data);
} else {
createToast({ body: "Failed to get settings" });
}
};
getSettings();
}, [authToken, dispatch]);
const logItems = [
{ _id: 1, name: "none" },
{ _id: 2, name: "debug" },
{ _id: 3, name: "error" },
{ _id: 4, name: "warn" },
];
const logItemLookup = {
none: 1,
debug: 2,
error: 3,
warn: 4,
};
const unitItemLookup = {
days: "d",
hours: "h",
};
const unitItems = Object.keys(unitItemLookup).map((key) => ({
_id: unitItemLookup[key],
name: key,
}));
const handleLogLevel = (e) => {
const id = e.target.value;
const newLogLevel = logItems.find((item) => item._id === id).name;
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
};
const handleJWTTTLUnits = (e) => {
const id = e.target.value;
const newUnits = unitItems.find((item) => item._id === id).name;
setLocalSettings({ ...localSettings, jwtTTLUnits: newUnits });
};
const handleBlur = (event) => {
const { value, id } = event.target;
const { error } = advancedSettingsValidation.validate(
{ [id]: value },
{
abortEarly: false,
}
);
setErrors((prev) => {
return buildErrors(prev, id, error);
});
};
const handleChange = (event) => {
const { value, id } = event.target;
setLocalSettings({ ...localSettings, [id]: value });
};
const handleSave = async () => {
localSettings.jwtTTL =
localSettings.jwtTTLNum + unitItemLookup[localSettings.jwtTTLUnits];
if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) {
return;
}
const action = await dispatch(
updateAppSettings({ settings: localSettings, authToken })
);
let body = "";
if (action.payload.success) {
parseJWTTTL(action.payload.data);
setLocalSettings(action.payload.data);
body = "Settings saved successfully";
} else {
body = "Failed to save settings";
}
createToast({ body });
};
return (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
>
<ConfigBox>
<Box>
<Typography component="h1">Client settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify client settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="apiBaseUrl"
label="API URL Host"
value={localSettings.apiBaseUrl}
onChange={handleChange}
onBlur={handleBlur}
error={errors.apiBaseUrl ? true : false}
helperText={errors.apiBaseUrl}
/>
<Select
id="logLevel"
label="Log level"
name="logLevel"
items={logItems}
value={logItemLookup[localSettings.logLevel]}
onChange={handleLogLevel}
onBlur={handleBlur}
error={errors.logLevel}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Email settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Set your host email settings here. These settings are used for sending
system emails.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
type="text"
id="systemEmailHost"
label="System email host"
name="systemEmailHost"
value={localSettings.systemEmailHost}
onChange={handleChange}
onBlur={handleBlur}
error={errors.systemEmailHost ? true : false}
helperText={errors.systemEmailHost}
/>
<TextInput
type="number"
id="systemEmailPort"
label="System email port"
name="systemEmailPort"
value={localSettings.systemEmailPort?.toString()}
onChange={handleChange}
onBlur={handleBlur}
error={errors.systemEmailPort ? true : false}
helperText={errors.systemEmailPort}
/>
<TextInput
type="email"
id="systemEmailAddress"
label="System email address"
name="systemEmailAddress"
value={localSettings.systemEmailAddress}
onChange={handleChange}
onBlur={handleBlur}
error={errors.systemEmailAddress ? true : false}
helperText={errors.systemEmailAddress}
/>
<TextInput
type="text"
id="systemEmailPassword"
label="System email password"
name="systemEmailPassword"
value={localSettings.systemEmailPassword}
onChange={handleChange}
onBlur={handleBlur}
error={errors.systemEmailPassword ? true : false}
helperText={errors.systemEmailPassword}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Server settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify server settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Stack
direction="row"
gap={theme.spacing(10)}
>
<TextInput
type="number"
id="jwtTTLNum"
label="JWT time to live"
name="jwtTTLNum"
value={localSettings.jwtTTLNum.toString()}
onChange={handleChange}
onBlur={handleBlur}
error={errors.jwtTTLNum ? true : false}
helperText={errors.jwtTTLNum}
/>
<Select
id="jwtTTLUnits"
label="JWT TTL Units"
name="jwtTTLUnits"
placeholder="Select time"
isHidden={true}
items={unitItems}
value={unitItemLookup[localSettings.jwtTTLUnits]}
onChange={handleJWTTTLUnits}
onBlur={handleBlur}
error={errors.jwtTTLUnits}
labelControlSpacing={0}
/>
</Stack>
<TextInput
type="text"
id="dbType"
label="Database type"
name="dbType"
value={localSettings.dbType}
onChange={handleChange}
onBlur={handleBlur}
error={errors.dbType ? true : false}
helperText={errors.dbType}
/>
<TextInput
type="text"
id="redisHost"
label="Redis host"
name="redisHost"
value={localSettings.redisHost}
onChange={handleChange}
onBlur={handleBlur}
error={errors.redisHost ? true : false}
helperText={errors.redisHost}
/>
<TextInput
type="number"
id="redisPort"
label="Redis port"
name="redisPort"
value={localSettings.redisPort?.toString()}
onChange={handleChange}
onBlur={handleBlur}
error={errors.redisPort ? true : false}
helperText={errors.redisPort}
/>
<TextInput
type="text"
id="pagespeedApiKey"
label="PageSpeed API key"
name="pagespeedApiKey"
value={localSettings.pagespeedApiKey}
onChange={handleChange}
onBlur={handleBlur}
error={errors.pagespeedApiKey ? true : false}
helperText={errors.pagespeedApiKey}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
</Box>
<Box>
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
Developed by Bluewave Labs.
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs"
label="https://github.com/bluewave-labs"
/>
</Box>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<LoadingButton
loading={settings.isLoading || settings.authIsLoading}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
Save
</LoadingButton>
</Stack>
</Stack>
</Box>
);
};
AdvancedSettings.propTypes = {
isAdmin: PropTypes.bool,
};
export default AdvancedSettings;
+17 -4
View File
@@ -5,10 +5,10 @@ import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { createToast } from "../../Utils/toastUtils";
import { forgotPassword } from "../../Features/Auth/authSlice";
import { IconBox } from "./styled";
import Background from "../../assets/Images/background-grid.svg?react";
import EmailIcon from "../../assets/icons/email.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import IconBox from "../../Components/IconBox";
import "./index.css";
const CheckEmail = () => {
@@ -144,9 +144,22 @@ const CheckEmail = () => {
textAlign="center"
>
<Box>
<IconBox>
<EmailIcon alt="email icon" />
</IconBox>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<EmailIcon alt="email icon" />
</IconBox>
</Stack>
<Typography component="h1">Check your email</Typography>
<Typography>
We sent a password reset link to{" "}
+17 -4
View File
@@ -6,12 +6,12 @@ import { forgotPassword } from "../../Features/Auth/authSlice";
import { useEffect, useState } from "react";
import { credentials } from "../../Validation/validation";
import { useNavigate } from "react-router-dom";
import { IconBox } from "./styled";
import TextInput from "../../Components/Inputs/TextInput";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Key from "../../assets/icons/key.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import LoadingButton from "@mui/lab/LoadingButton";
import IconBox from "../../Components/IconBox";
import "./index.css";
const ForgotPassword = () => {
@@ -146,9 +146,22 @@ const ForgotPassword = () => {
textAlign="center"
>
<Box>
<IconBox>
<Key alt="password key icon" />
</IconBox>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<Key alt="password key icon" />
</IconBox>
</Stack>
<Typography component="h1">Forgot password?</Typography>
<Typography>No worries, we&apos;ll send you reset instructions.</Typography>
</Box>
-562
View File
@@ -1,562 +0,0 @@
import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { credentials } from "../../Validation/validation";
import { login } from "../../Features/Auth/authSlice";
import LoadingButton from "@mui/lab/LoadingButton";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
import { networkService } from "../../main";
import TextInput from "../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import Background from "../../assets/Images/background-grid.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Mail from "../../assets/icons/mail.svg?react";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import PropTypes from "prop-types";
import { logger } from "../../Utils/Logger";
import "./index.css";
const DEMO = import.meta.env.VITE_APP_DEMO;
/**
* Displays the initial landing page.
*
* @param {Object} props
* @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
* @returns {JSX.Element}
*/
const LandingPage = ({ onContinue }) => {
const theme = useTheme();
return (
<>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>We are pleased to see you again!</Typography>
</Box>
<Box width="100%">
<Button
variant="outlined"
color="info"
onClick={onContinue}
sx={{
width: "100%",
"& svg": {
mr: theme.spacing(4),
"& path": {
stroke: theme.palette.other.icon,
},
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<Mail />
Continue with Email
</Button>
</Box>
<Box maxWidth={400}>
<Typography className="tos-p">
By continuing, you agree to our{" "}
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/terms-of-service-open-source",
"_blank",
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.text.tertiary,
},
}}
>
Terms of Service
</Typography>{" "}
and{" "}
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/privacy-policy-open-source",
"_blank",
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.text.tertiary,
},
}}
>
Privacy Policy.
</Typography>
</Typography>
</Box>
</Stack>
</>
);
};
LandingPage.propTypes = {
onContinue: PropTypes.func.isRequired,
};
/**
* Renders the first step of the login process.
*
* @param {Object} props
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onChange - Callback function to handle form input changes.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
const theme = useTheme();
const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<>
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your email address</Typography>
</Box>
<Box
textAlign="left"
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
>
<TextInput
type="email"
id="login-email-input"
label="Email"
isRequired={true}
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email}
ref={inputRef}
/>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
Back
</Button>
<Button
variant="contained"
color="primary"
type="submit"
disabled={errors.email && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
Continue
</Button>
</Stack>
</Box>
</Stack>
</>
);
};
StepOne.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
/**
* Renders the second step of the login process, including a password input field.
*
* @param {Object} props
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onChange - Callback function to handle form input changes.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
const theme = useTheme();
const navigate = useNavigate();
const inputRef = useRef(null);
const authState = useSelector((state) => state.auth);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const handleNavigate = () => {
if (form.email !== "" && !errors.email) {
sessionStorage.setItem("email", form.email);
}
navigate("/forgot-password");
};
return (
<>
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
position="relative"
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your password</Typography>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
textAlign="left"
mb={theme.spacing(5)}
sx={{
display: "grid",
gap: { xs: theme.spacing(12), sm: theme.spacing(16) },
}}
>
<TextInput
type="password"
id="login-password-input"
label="Password"
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onChange}
error={errors.password ? true : false}
helperText={errors.password}
ref={inputRef}
endAdornment={<PasswordEndAdornment />}
/>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
Back
</Button>
<LoadingButton
variant="contained"
color="primary"
type="submit"
loading={authState.isLoading}
disabled={errors.password && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
Continue
</LoadingButton>
</Stack>
</Box>
<Box
textAlign="center"
sx={{
position: "absolute",
bottom: 0,
left: "50%",
transform: `translate(-50%, 150%)`,
}}
>
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
</Stack>
</>
);
};
StepTwo.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const { authToken } = authState;
const idMap = {
"login-email-input": "email",
"login-password-input": "password",
};
const [form, setForm] = useState({
email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
password: DEMO !== undefined ? "Demouser1!" : "",
});
const [errors, setErrors] = useState({});
const [step, setStep] = useState(0);
useEffect(() => {
if (authToken) {
navigate("/uptime");
return;
}
networkService
.doesSuperAdminExist()
.then((response) => {
if (response.data.data === false) {
navigate("/register");
}
})
.catch((error) => {
logger.error(error);
});
}, [authToken, navigate]);
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
setForm((prev) => ({
...prev,
[name]: value,
}));
const { error } = credentials.validate({ [name]: value }, { abortEarly: false });
setErrors((prev) => {
const prevErrors = { ...prev };
if (error) prevErrors[name] = error.details[0].message;
else delete prevErrors[name];
return prevErrors;
});
};
const handleSubmit = async (event) => {
event.preventDefault();
if (step === 1) {
const { error } = credentials.validate(
{ email: form.email },
{ abortEarly: false }
);
if (error) {
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
createToast({ body: error.details[0].message });
} else {
setStep(2);
}
} else if (step === 2) {
const { error } = credentials.validate(form, { abortEarly: false });
if (error) {
// validation errors
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
});
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/uptime");
createToast({
body: "Welcome back! You're successfully logged in.",
});
} else {
if (action.payload) {
if (action.payload.msg === "Incorrect password")
setErrors({
password: "The password you provided does not match our records",
});
// dispatch errors
createToast({
body: action.payload.msg,
});
} else {
// unknown errors
createToast({
body: "Unknown error.",
});
}
}
}
}
};
return (
<Stack
className="login-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 28,
},
"& p": { fontSize: 14, color: theme.palette.text.accent },
"& span": { fontSize: "inherit" },
}}
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.border.light,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
direction="row"
alignItems="center"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
</Stack>
<Stack
width="100%"
maxWidth={600}
flex={1}
justifyContent="center"
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
sx={{
"& > .MuiStack-root": {
border: 1,
borderRadius: theme.spacing(5),
borderColor: theme.palette.border.light,
backgroundColor: theme.palette.background.main,
padding: {
xs: theme.spacing(12),
sm: theme.spacing(20),
},
},
}}
>
{step === 0 ? (
<LandingPage onContinue={() => setStep(1)} />
) : step === 1 ? (
<StepOne
form={form}
errors={errors}
onSubmit={handleSubmit}
onChange={handleChange}
onBack={() => setStep(0)}
/>
) : (
step === 2 && (
<StepTwo
form={form}
errors={errors}
onSubmit={handleSubmit}
onChange={handleChange}
onBack={() => setStep(1)}
/>
)
)}
</Stack>
</Stack>
);
};
export default Login;
@@ -1,4 +1,4 @@
import { useRef, useEffect } from "react";
import { useRef, useEffect } from "react";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import TextInput from "../../../../Components/Inputs/TextInput";
@@ -30,6 +30,7 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
textAlign="center"
position="relative"
>
<Box>
<Typography component="h1">Log In</Typography>
@@ -92,4 +93,4 @@ EmailStep.propTypes = {
onChange: PropTypes.func.isRequired,
};
export default EmailStep;
export default EmailStep;
@@ -0,0 +1,43 @@
import { Box, Typography, useTheme } from "@mui/material";
import PropTypes from "prop-types";
import { useNavigate } from "react-router";
const ForgotPasswordLabel = ({ email, errorEmail }) => {
const theme = useTheme();
const navigate = useNavigate();
const handleNavigate = () => {
if (email !== "" && !errorEmail) {
sessionStorage.setItem("email", email);
}
navigate("/forgot-password");
};
return (
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
);
};
ForgotPasswordLabel.proptype = {
email: PropTypes.string.isRequired,
emailError: PropTypes.string.isRequired,
};
export default ForgotPasswordLabel;
@@ -1,5 +1,4 @@
import { useRef, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import LoadingButton from "@mui/lab/LoadingButton";
@@ -22,7 +21,6 @@ import PropTypes from "prop-types";
*/
const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
const theme = useTheme();
const navigate = useNavigate();
const inputRef = useRef(null);
const authState = useSelector((state) => state.auth);
@@ -32,13 +30,6 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
}
}, []);
const handleNavigate = () => {
if (form.email !== "" && !errors.email) {
sessionStorage.setItem("email", form.email);
}
navigate("/forgot-password");
};
return (
<>
<Stack
@@ -117,32 +108,6 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
</LoadingButton>
</Stack>
</Box>
<Box
textAlign="center"
sx={{
position: "absolute",
bottom: 0,
left: "50%",
transform: `translate(-50%, 150%)`,
}}
>
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
</Stack>
</>
);
@@ -156,4 +121,4 @@ PasswordStep.propTypes = {
onBack: PropTypes.func.isRequired,
};
export default PasswordStep
export default PasswordStep;
+14 -4
View File
@@ -11,8 +11,10 @@ import Background from "../../../assets/Images/background-grid.svg?react";
import Logo from "../../../assets/icons/bwu-icon.svg?react";
import { logger } from "../../../Utils/Logger";
import "../index.css";
import EmailStep from "./Components/EmailStep";
import PasswordStep from "./Components/PasswordStep";
import EmailStep from "./Components/EmailStep";
import PasswordStep from "./Components/PasswordStep";
import ThemeSwitch from "../../../Components/ThemeSwitch";
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
const DEMO = import.meta.env.VITE_APP_DEMO;
@@ -42,7 +44,7 @@ const Login = () => {
useEffect(() => {
if (authToken) {
navigate("/monitors");
navigate("/uptime");
return;
}
networkService
@@ -108,7 +110,7 @@ const Login = () => {
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/monitors");
navigate("/uptime");
createToast({
body: "Welcome back! You're successfully logged in.",
});
@@ -174,6 +176,7 @@ const Login = () => {
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
rowGap={theme.spacing(8)}
sx={{
"& > .MuiStack-root": {
border: 1,
@@ -205,6 +208,13 @@ const Login = () => {
/>
)
)}
<ForgotPasswordLabel
email={form.email}
errorEmail={errors.email}
/>
<Box marginX={"auto"}>
<ThemeSwitch />
</Box>
</Stack>
</Stack>
);
+18 -5
View File
@@ -4,10 +4,10 @@ import { useNavigate } from "react-router";
import { useDispatch } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { IconBox } from "./styled";
import Background from "../../assets/Images/background-grid.svg?react";
import ConfirmIcon from "../../assets/icons/check-outlined.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import IconBox from "../../Components/IconBox";
import "./index.css";
const NewPasswordConfirmed = () => {
@@ -80,9 +80,22 @@ const NewPasswordConfirmed = () => {
textAlign="center"
>
<Box>
<IconBox>
<ConfirmIcon alt="password confirm icon" />
</IconBox>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<ConfirmIcon alt="password confirm icon" />
</IconBox>
</Stack>
<Typography component="h1">Password reset</Typography>
<Typography mb={theme.spacing(2)}>
Your password has been successfully reset. Click below to log in magically.
@@ -91,7 +104,7 @@ const NewPasswordConfirmed = () => {
<Button
variant="contained"
color="primary"
onClick={() => navigate("/monitors")}
onClick={() => navigate("/uptime")}
sx={{
width: "100%",
maxWidth: 400,
+17 -5
View File
@@ -11,8 +11,7 @@ import { credentials } from "../../Validation/validation";
import Check from "../../Components/Check/Check";
import TextInput from "../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import { IconBox } from "./styled";
import IconBox from "../../Components/IconBox";
import LockIcon from "../../assets/icons/lock.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
@@ -126,9 +125,22 @@ const SetNewPassword = () => {
textAlign="center"
>
<Box>
<IconBox>
<LockIcon alt="lock icon" />
</IconBox>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<LockIcon alt="lock icon" />
</IconBox>
</Stack>
<Typography component="h1">Set new password</Typography>
<Typography>
Your new password must be different to previously used passwords.
-26
View File
@@ -1,26 +0,0 @@
import { Box, styled } from "@mui/material";
export const IconBox = styled(Box)(({ theme }) => ({
height: 48,
minWidth: 48,
width: 48,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: 12,
backgroundColor: theme.palette.background.accent,
margin: "auto",
marginBottom: 8,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 24,
height: 24,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
}));
@@ -16,7 +16,7 @@ import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Link from "../../../Components/Link";
import { ConfigBox } from "../../Uptime/styled";
import ConfigBox from "../../../Components/ConfigBox";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
@@ -20,6 +20,7 @@ import {
TemperatureTooltip,
} from "../../../Components/Charts/Utils/chartUtils";
import PropTypes from "prop-types";
import StatBox from "../../../Components/StatBox";
const BASE_BOX_PADDING_VERTICAL = 4;
const BASE_BOX_PADDING_HORIZONTAL = 8;
@@ -29,18 +30,50 @@ const TYPOGRAPHY_PADDING = 8;
* @param {number} bytes - Number of bytes to convert
* @returns {number} Converted value in gigabytes
*/
const formatBytes = (bytes) => {
if (bytes === undefined || bytes === null) return "0 GB";
if (typeof bytes !== "number") return "0 GB";
if (bytes === 0) return "0 GB";
const formatBytes = (bytes, space = false) => {
if (bytes === undefined || bytes === null)
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
if (typeof bytes !== "number")
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
if (bytes === 0)
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
const GB = bytes / (1024 * 1024 * 1024);
const MB = bytes / (1024 * 1024);
if (GB >= 1) {
return `${Number(GB.toFixed(0))} GB`;
return (
<>
{Number(GB.toFixed(0))}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
} else {
return `${Number(MB.toFixed(0))} MB`;
return (
<>
{Number(MB.toFixed(0))}
<Typography component="span">MB</Typography>
</>
);
}
};
@@ -93,27 +126,6 @@ BaseBox.propTypes = {
sx: PropTypes.object,
};
/**
* Renders a statistic box with a heading and subheading
* @param {Object} props - Component properties
* @param {string} props.heading - Primary heading text
* @param {string} props.subHeading - Secondary heading text
* @returns {React.ReactElement} Stat box component
*/
const StatBox = ({ heading, subHeading }) => {
return (
<BaseBox>
<Typography component="h2">{heading}</Typography>
<Typography>{subHeading}</Typography>
</BaseBox>
);
};
StatBox.propTypes = {
heading: PropTypes.string.isRequired,
subHeading: PropTypes.string.isRequired,
};
/**
* Renders a gauge box with usage visualization
* @param {Object} props - Component properties
@@ -193,7 +205,7 @@ const InfrastructureDetails = () => {
const [monitor, setMonitor] = useState(null);
const { authToken } = useSelector((state) => state.auth);
const [dateRange, setDateRange] = useState("all");
const { statusColor, determineState } = useUtils();
const { statusColor, statusStyles, determineState } = useUtils();
// These calculations are needed because ResponsiveContainer
// doesn't take padding of parent/siblings into account
// when calculating height.
@@ -205,7 +217,7 @@ const InfrastructureDetails = () => {
(chartContainerHeight - totalChartContainerPadding - totalTypographyPadding) * 0.95;
// end height calculations
const buildStatBoxes = (checks) => {
const buildStatBoxes = (checks, uptime) => {
let latestCheck = checks[0] ?? null;
if (latestCheck === null) return [];
@@ -224,25 +236,51 @@ const InfrastructureDetails = () => {
const platform = latestCheck?.host?.platform ?? null;
const osPlatform = os === null && platform === null ? null : `${os} ${platform}`;
return [
{
id: 7,
sx: statusStyles[determineState(monitor)],
heading: "Status",
subHeading: monitor?.status === true ? "Active" : "Inactive",
},
{
id: 0,
heading: "CPU (Physical)",
subHeading: `${physicalCores} cores`,
subHeading: (
<>
{physicalCores}
<Typography component="span">cores</Typography>
</>
),
},
{
id: 1,
heading: "CPU (Logical)",
subHeading: `${logicalCores} cores`,
subHeading: (
<>
{logicalCores}
<Typography component="span">cores</Typography>
</>
),
},
{
id: 2,
heading: "CPU Frequency",
subHeading: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
subHeading: (
<>
{(cpuFrequency / 1000).toFixed(2)}
<Typography component="span">Ghz</Typography>
</>
),
},
{
id: 3,
heading: "Average CPU Temperature",
subHeading: `${cpuTemperature.toFixed(2)} C`,
subHeading: (
<>
{cpuTemperature.toFixed(2)}
<Typography component="span">C</Typography>
</>
),
},
{
id: 4,
@@ -254,12 +292,17 @@ const InfrastructureDetails = () => {
heading: "Disk",
subHeading: formatBytes(diskTotalBytes),
},
{ id: 6, heading: "Uptime", subHeading: "100%" },
{
id: 7,
heading: "Status",
subHeading: monitor?.status === true ? "Active" : "Inactive",
id: 6,
heading: "Uptime",
subHeading: (
<>
{(uptime * 100).toFixed(2)}
<Typography component="span">%</Typography>
</>
),
},
{
id: 8,
heading: "OS",
@@ -285,9 +328,9 @@ const InfrastructureDetails = () => {
value: decimalToPercentage(memoryUsagePercent),
heading: "Memory usage",
metricOne: "Used",
valueOne: formatBytes(memoryUsedBytes),
valueOne: formatBytes(memoryUsedBytes, true),
metricTwo: "Total",
valueTwo: formatBytes(memoryTotalBytes),
valueTwo: formatBytes(memoryTotalBytes, true),
},
{
type: "cpu",
@@ -304,9 +347,9 @@ const InfrastructureDetails = () => {
value: decimalToPercentage(disk.usage_percent),
heading: `Disk${idx} usage`,
metricOne: "Used",
valueOne: formatBytes(disk.total_bytes - disk.free_bytes),
valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true),
metricTwo: "Total",
valueTwo: formatBytes(disk.total_bytes),
valueTwo: formatBytes(disk.total_bytes, true),
})),
];
};
@@ -466,7 +509,10 @@ const InfrastructureDetails = () => {
fetchData();
}, [authToken, monitorId, dateRange, navigate]);
const statBoxConfigs = buildStatBoxes(monitor?.checks ?? []);
const statBoxConfigs = buildStatBoxes(
monitor?.checks ?? [],
monitor?.uptimePercentage ?? "Unknown"
);
const gaugeBoxConfigs = buildGaugeBoxConfigs(monitor?.checks ?? []);
const areaChartConfigs = buildAreaChartConfigs(monitor?.checks ?? []);
@@ -2,7 +2,7 @@ import { Box, Button, Stack, Typography } from "@mui/material";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
import { ConfigBox } from "./styled";
import ConfigBox from "../../../Components/ConfigBox";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
@@ -321,18 +321,27 @@ const CreateMaintenance = () => {
Your pings won&apos;t be sent during this time frame
</Typography>
</Box>
<ConfigBox direction="row">
<Typography
component="h2"
variant="h2"
>
General Settings
</Typography>
<Box
px={theme.spacing(14)}
borderLeft={1}
borderLeftColor={theme.palette.border.light}
>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
General Settings
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
id="name"
label="Friendly name"
placeholder="Maintenance at __ : __ for ___ minutes"
value={form.name}
onChange={(event) => {
handleFormChange("name", event.target.value);
}}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
<Select
id="repeat"
name="maintenance-repeat"
@@ -346,15 +355,12 @@ const CreateMaintenance = () => {
}}
items={repeatConfig}
/>
<Stack
gap={theme.spacing(2)}
mt={theme.spacing(16)}
>
<Typography component="h3">Date</Typography>
<Stack>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
id="startDate"
disablePast
label="Date"
disableHighlightToday
value={form.startDate}
slots={{ openPickerIcon: CalendarIcon }}
@@ -403,72 +409,64 @@ const CreateMaintenance = () => {
/>
</LocalizationProvider>
</Stack>
</Box>
</Stack>
</ConfigBox>
<ConfigBox>
<Stack direction="row">
<Box>
<Typography
component="h2"
variant="h2"
>
Start time
</Typography>
<Typography>All dates and times are in GMT+0 time zone.</Typography>
</Box>
<Stack direction="row">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MobileTimePicker
id="startTime"
value={form.startTime}
onChange={(newTime) => {
handleTimeChange("startTime", newTime);
}}
slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
height: 34,
p: 0,
pl: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.border.dark,
},
<Box>
<Typography
component="h2"
variant="h2"
>
Start time
</Typography>
<Typography>All dates and times are in GMT+0 time zone.</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MobileTimePicker
id="startTime"
label="Start time"
value={form.startTime}
onChange={(newTime) => {
handleTimeChange("startTime", newTime);
}}
slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
height: 34,
p: 0,
pl: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.border.dark,
},
},
}}
error={errors["startTime"]}
/>
</LocalizationProvider>
</Stack>
</Stack>
<Stack direction="row">
<Box>
<Typography
component="h2"
variant="h2"
>
Duration
</Typography>
</Box>
},
}}
error={errors["startTime"]}
/>
</LocalizationProvider>
<Stack
direction="row"
alignItems="end"
spacing={theme.spacing(8)}
>
<TextInput
type="number"
id="duration"
label="Duration"
value={form.duration}
onChange={(event) => {
handleFormChange("duration", event.target.value);
@@ -491,66 +489,34 @@ const CreateMaintenance = () => {
</Stack>
</Stack>
</ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
fontSize={16}
my={theme.spacing(6)}
>
Monitor related settings
</Typography>
<ConfigBox>
<Stack direction="row">
<Box>
<Typography
component="h2"
variant="h2"
>
Friendly name
</Typography>
</Box>
<Box>
<TextInput
id="name"
placeholder="Maintenance at __ : __ for ___ minutes"
value={form.name}
onChange={(event) => {
handleFormChange("name", event.target.value);
}}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Box>
</Stack>
<Stack direction="row">
<Box>
<Typography
component="h2"
variant="h2"
>
Add monitors
</Typography>
</Box>
<Box>
<Search
id={"monitors"}
multiple={true}
isAdorned={false}
options={monitors ? monitors : []}
filteredBy="name"
secondaryLabel={"type"}
inputValue={search}
value={form.monitors}
handleInputChange={handleSearch}
handleChange={handleSelectMonitors}
error={errors["monitors"]}
disabled={maintenanceWindowId !== undefined}
/>
</Box>
</Stack>
</ConfigBox>
</Box>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
Monitors to apply maintenance window to
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<Search
id={"monitors"}
label="Add monitors"
multiple={true}
isAdorned={false}
options={monitors ? monitors : []}
filteredBy="name"
secondaryLabel={"type"}
inputValue={search}
value={form.monitors}
handleInputChange={handleSearch}
handleChange={handleSelectMonitors}
error={errors["monitors"]}
disabled={maintenanceWindowId !== undefined}
/>
</Stack>
</ConfigBox>
<Box
ml="auto"
display="inline-block"
@@ -1,25 +0,0 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
backgroundColor: theme.palette.background.main,
"& > *": { padding: theme.spacing(14) },
"& > :first-of-type, & > .MuiStack-root > div:first-of-type": {
flex: 0.6,
},
"& > div:last-of-type, & > .MuiStack-root > div:last-of-type": {
flex: 1,
},
"& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) },
"& > .MuiStack-root > div:last-of-type": {
paddingLeft: theme.spacing(14),
},
"& h2": { fontSize: 13.5, fontWeight: 500 },
"& h3, & p": {
color: theme.palette.text.tertiary,
},
"& h3": { fontWeight: 500 },
}));
@@ -13,7 +13,7 @@ import {
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../../Uptime/styled";
import ConfigBox from "../../../Components/ConfigBox";
import TextInput from "../../../Components/Inputs/TextInput";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
@@ -20,8 +20,7 @@ import LoadingButton from "@mui/lab/LoadingButton";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { ConfigBox } from "../../Uptime/styled";
import ConfigBox from "../../../Components/ConfigBox";
import { createToast } from "../../../Utils/toastUtils";
import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
@@ -101,21 +101,21 @@ PieValueLabel.propTypes = {
highlighted: PropTypes.bool.isRequired,
};
/**
* Weight constants for different performance metrics.
* @type {Object}
*/
const weights = {
fcp: 10,
si: 10,
lcp: 25,
tbt: 30,
cls: 25,
};
const PieChart = ({ audits }) => {
const theme = useTheme();
/**
* Weight constants for different performance metrics.
* @type {Object}
*/
const weights = {
fcp: 10,
si: 10,
lcp: 25,
tbt: 30,
cls: 25,
};
/**
* Retrieves color properties based on the performance value.
*
@@ -128,7 +128,7 @@ const PieChart = ({ audits }) => {
stroke: theme.palette.success.main,
strokeBg: theme.palette.success.light,
text: theme.palette.success.contrastText,
bg: theme.palette.success.dark,
bg: theme.palette.success.light,
};
else if (value >= 50 && value < 90)
return {
+22 -15
View File
@@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
import { ChartBox, IconBox, StatBox } from "./styled";
import { ChartBox } from "./styled";
import { logger } from "../../../Utils/Logger";
import { networkService } from "../../../main";
import SkeletonLayout from "./skeleton";
@@ -22,6 +22,8 @@ import PieChart from "./Charts/PieChart";
import useUtils from "../../Uptime/utils";
import "./index.css";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import StatBox from "../../../Components/StatBox";
import IconBox from "../../../Components/IconBox";
const PageSpeedDetails = () => {
const theme = useTheme();
@@ -187,20 +189,24 @@ const PageSpeedDetails = () => {
direction="row"
gap={theme.spacing(8)}
>
<StatBox>
<Typography component="h2">checks since</Typography>
<Typography>
{splitDuration(monitor?.uptimeDuration)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last check</Typography>
<Typography>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
<StatBox
heading="checks since"
subHeading={
<>
{splitDuration(monitor?.uptimeDuration)}
<Typography component="span">ago</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</>
}
/>
</Stack>
<Box>
<Typography
@@ -288,6 +294,7 @@ const PageSpeedDetails = () => {
</Box>
<ChartBox
flex={1}
/* TODO apply 1fr 1fr for columns, and auto 1fr for Rows */
sx={{ gridTemplateColumns: "50% 50%", gridTemplateRows: "15% 85%" }}
>
<Stack
+2 -53
View File
@@ -1,8 +1,8 @@
import { Box, Stack, styled } from "@mui/material";
import { Stack, styled } from "@mui/material";
export const ChartBox = styled(Stack)(({ theme }) => ({
display: "grid",
height: 300,
minHeight: 300,
minWidth: 250,
border: 1,
borderStyle: "solid",
@@ -40,54 +40,3 @@ export const ChartBox = styled(Stack)(({ theme }) => ({
transition: "stroke-width 400ms ease",
},
}));
export const IconBox = styled(Box)(({ theme }) => ({
height: 34,
minWidth: 34,
width: 34,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: 4,
backgroundColor: theme.palette.background.accent,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 20,
height: 20,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
}));
export const StatBox = styled(Box)(({ theme }) => ({
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
minWidth: 200,
width: 225,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,
color: theme.palette.text.secondary,
textTransform: "uppercase",
},
"& p": {
fontSize: 18,
color: theme.palette.text.primary,
marginTop: theme.spacing(2),
"& span": {
color: theme.palette.text.tertiary,
marginLeft: theme.spacing(2),
fontSize: 15,
},
},
}));
+1 -2
View File
@@ -4,13 +4,12 @@ import { StatusLabel } from "../../Components/Label";
import { Box, Grid, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router";
import { useTheme } from "@emotion/react";
import { IconBox } from "./Details/styled";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils";
import useUtils from "../Uptime/utils";
import { useState } from "react";
import IconBox from "../../Components/IconBox";
/**
* CustomToolTip displays a tooltip with formatted date and score information.
* @param {Object} props
+2 -26
View File
@@ -18,13 +18,11 @@ import LoadingButton from "@mui/lab/LoadingButton";
import { setTimezone, setMode } from "../../Features/UI/uiSlice";
import timezones from "../../Utils/timezones.json";
import { useState, useEffect } from "react";
import { ConfigBox } from "./styled";
import { networkService } from "../../main";
import { settingsValidation } from "../../Validation/validation";
import { useNavigate } from "react-router";
import Dialog from "../../Components/Dialog";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import ConfigBox from "../../Components/ConfigBox";
const SECONDS_PER_DAY = 86400;
const Settings = () => {
@@ -45,7 +43,6 @@ const Settings = () => {
const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false };
const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState);
const dispatch = useDispatch();
const navigate = useNavigate();
//Fetching latest release version from github
useEffect(() => {
@@ -324,28 +321,7 @@ const Settings = () => {
/>
</ConfigBox>
)}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">Advanced settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Click here to modify advanced settings
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Box>
<Button
variant="contained"
onClick={() => {
navigate("/advanced-settings");
}}
>
Advanced Settings
</Button>
</Box>
</Stack>
</ConfigBox>
)}
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
-28
View File
@@ -1,28 +0,0 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
paddingTop: theme.spacing(12),
paddingInline: theme.spacing(15),
paddingBottom: theme.spacing(25),
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
"& > div:first-of-type": {
flex: 0.7,
},
"& > div:last-of-type": {
flex: 1,
},
"& h1, & h2": {
color: theme.palette.text.secondary,
},
"& p": {
color: theme.palette.text.tertiary,
},
}));
+1 -1
View File
@@ -6,7 +6,7 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../styled";
import ConfigBox from "../../../Components/ConfigBox";
import {
updateUptimeMonitor,
pauseUptimeMonitor,
@@ -17,14 +17,13 @@ import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { ConfigBox } from "../styled";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import ConfigBox from "../../../Components/ConfigBox";
const CreateMonitor = () => {
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
+21 -20
View File
@@ -26,14 +26,15 @@ import HistoryIcon from "../../../assets/icons/history-icon.svg?react";
import PaginationTable from "./PaginationTable";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import { StatBox, ChartBox, IconBox } from "./styled";
import { ChartBox } from "./styled";
import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts";
import SkeletonLayout from "./skeleton";
import "./index.css";
import useUtils from "../utils";
import { formatDateWithTz } from "../../../Utils/timeUtils";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import IconBox from "../../../Components/IconBox";
import StatBox from "../../../Components/StatBox";
/**
* Details page component displaying monitor details and related information.
* @component
@@ -283,24 +284,24 @@ const DetailsPage = () => {
direction="row"
gap={theme.spacing(8)}
>
<StatBox sx={statusStyles[determineState(monitor)]}>
<Typography component="h2">active for</Typography>
<Typography>{splitDuration(monitor?.uptimeDuration)}</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last check</Typography>
<Typography>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last response time</Typography>
<Typography>
{monitor?.latestResponseTime}
<Typography component="span">ms</Typography>
</Typography>
</StatBox>
<StatBox
sx={statusStyles[determineState(monitor)]}
heading={"active for"}
subHeading={splitDuration(monitor?.uptimeDuration)}
/>
<StatBox
heading="last check"
subHeading={splitDuration(monitor?.lastChecked)}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitor?.latestResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
</Stack>
<Box>
<Stack
+1 -52
View File
@@ -1,4 +1,4 @@
import { Box, Stack, styled } from "@mui/material";
import { Stack, styled } from "@mui/material";
export const ChartBox = styled(Stack)(({ theme }) => ({
flex: "1 30%",
@@ -43,54 +43,3 @@ export const ChartBox = styled(Stack)(({ theme }) => ({
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}));
export const IconBox = styled(Box)(({ theme }) => ({
height: 34,
minWidth: 34,
width: 34,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: 4,
backgroundColor: theme.palette.background.accent,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 20,
height: 20,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
}));
export const StatBox = styled(Box)(({ theme }) => ({
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
minWidth: 200,
width: 225,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,
color: theme.palette.text.secondary,
textTransform: "uppercase",
},
"& p": {
fontSize: 18,
color: theme.palette.text.primary,
marginTop: theme.spacing(2),
"& span": {
color: theme.palette.text.tertiary,
marginLeft: theme.spacing(2),
fontSize: 15,
},
},
}));
@@ -144,7 +144,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePaus
setMonitors(res?.data?.data?.monitors ?? []);
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
};
/* TODO Apply component basic table? */
return (
<Box position="relative">
{isSearching && (
@@ -264,7 +264,9 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePaus
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
filter: "brightness(.75)",
opacity: 0.75,
transition: "filter 0.3s ease, opacity 0.3s ease",
},
}}
onClick={() => {
@@ -327,6 +329,7 @@ MonitorTable.propTypes = {
setIsSearching: PropTypes.func,
isSearching: PropTypes.bool,
setMonitorUpdateTrigger: PropTypes.func,
handlePause: PropTypes.func,
};
const MemoizedMonitorTable = memo(MonitorTable);
+1 -1
View File
@@ -72,11 +72,11 @@ const UptimeMonitors = () => {
</Stack>
<Greeting type="uptime" />
</Box>
{noMonitors && <Fallback isAdmin={isAdmin} />}
{loading ? (
<SkeletonLayout />
) : (
<>
{noMonitors && <Fallback isAdmin={isAdmin} />}
{hasMonitors && (
<>
<Stack
+1 -6
View File
@@ -3,7 +3,7 @@ import HomeLayout from "../Components/Layouts/HomeLayout";
import { Infrastructure } from "../Pages/Infrastructure";
import InfrastructureDetails from "../Pages/Infrastructure/Details";
import NotFound from "../Pages/NotFound";
import Login from "../Pages/Auth/Login";
import Login from "../Pages/Auth/Login/Login";
import Register from "../Pages/Auth/Register/Register";
import Account from "../Pages/Account";
import Monitors from "../Pages/Uptime/Home";
@@ -19,7 +19,6 @@ import SetNewPassword from "../Pages/Auth/SetNewPassword";
import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed";
import ProtectedRoute from "../Components/ProtectedRoute";
import Details from "../Pages/Uptime/Details";
import AdvancedSettings from "../Pages/AdvancedSettings";
import Maintenance from "../Pages/Maintenance";
import Configure from "../Pages/Uptime/Configure";
import PageSpeed from "../Pages/PageSpeed";
@@ -120,10 +119,6 @@ const Routes = () => {
path="settings"
element={<Settings />}
/>
<Route
path="advanced-settings"
element={<AdvancedSettings />}
/>
<Route
path="account/profile"
element={<Account open={"profile"} />}
+9 -2
View File
@@ -46,6 +46,7 @@ const paletteColors = {
green50: "#D4F4E1",
green150: "#45BB7A",
green400: "#079455",
green500: "#07B467",
green800: "#1C4428",
green900: "#12261E",
red50: "#F9ECED",
@@ -96,10 +97,10 @@ const semanticColors = {
},
contrastText: {
light: paletteColors.green50,
dark: paletteColors.green900,
dark: paletteColors.green50,
},
light: {
light: paletteColors.green50,
light: paletteColors.green500,
dark: paletteColors.green800,
},
dark: {
@@ -107,6 +108,12 @@ const semanticColors = {
dark: paletteColors.green900,
},
},
neutral: {
contrastText: {
light: paletteColors.blueGray900,
dark: paletteColors.gray100,
},
},
error: {
main: {
light: paletteColors.red300,
+4
View File
@@ -8,6 +8,7 @@ const {
success,
error,
warning,
neutral,
gradient: {
color1: { dark: color1 },
color2: { dark: color2 },
@@ -51,6 +52,9 @@ const palette = {
contrastText: warning.contrastText.dark,
dark: warning.dark.dark,
},
neutral: {
contrastText: neutral.contrastText.dark,
},
/* From this part on, try to create semantic structure, not feature based structure */
percentage: {
uptimePoor: error.main.dark,
+4 -3
View File
@@ -47,7 +47,8 @@ const baseTheme = (palette) => ({
{
props: (props) => props.variant === "group",
style: {
color: theme.palette.secondary.contrastText,
/* color: theme.palette.secondary.contrastText, */
color: theme.palette.neutral.contrastText,
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
@@ -238,8 +239,8 @@ const baseTheme = (palette) => ({
},
},
"& .MuiInputBase-input:-webkit-autofill": {
transition: "background-color 5000s ease-in-out 0s",
WebkitBoxShadow: `0 0 0px 1000px ${theme.palette.background.main} inset`,
transition: "background-color 5000s ease-in-out 0s",
WebkitBoxShadow: `0 0 0px 1000px ${theme.palette.background.main} inset`,
WebkitTextFillColor: theme.palette.text.primary,
},
"& .MuiInputBase-input.MuiOutlinedInput-input": {
+4
View File
@@ -13,6 +13,7 @@ const {
success,
error,
warning,
neutral,
gradient: {
color1: { light: color1 },
color2: { light: color2 },
@@ -57,6 +58,9 @@ const palette = {
contrastText: warning.contrastText.light,
dark: warning.dark.light,
},
neutral: {
contrastText: neutral.contrastText.light,
},
/* From this part on, try to create semantic structure, not feature based structure */
percentage: {
uptimePoor: error.main.light,
+2
View File
@@ -1,5 +1,7 @@
FROM node:20-alpine
ENV NODE_OPTIONS="--max-old-space-size=2048"
WORKDIR /app
COPY ./Server/package*.json ./
+31 -25
View File
@@ -2,41 +2,44 @@
**Checkmate is on [GitHub trending](https://github.com/trending) and #1 trending for JavaScript apps on Fri Dec 13th & 15th**
<img width="1259" alt="Frame 34" src="https://github.com/user-attachments/assets/b9339bd3-8f80-4919-8255-88db8d841a3d" />
**If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback.**
![](https://img.shields.io/github/license/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/repo-size/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/commit-activity/w/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/last-commit/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/languages/top/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/issues-pr/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/issues/bluewave-labs/bluewave-uptime)
![Frame 34](https://github.com/user-attachments/assets/4bf57845-3f47-4759-835b-285a5486191d)
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate)
![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>An open source server monitoring application</strong></p>
<p align="center"><strong>An open source server and infrastructure monitoring application</strong></p>
![Dashboard-dark](https://github.com/user-attachments/assets/db875138-164f-453c-a75e-889f88747578)
Checkmate is an open source uptime manager, server & Docker monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
Checkmate is an open source monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insigths about your servers' CPU, RAM, disk and temperature status.
We've run stress tests with 1000+ active monitors without any particular issues or performance bottlenecks.
Checkmate has ben stress tested with 1000+ active monitors without any particular issues or performance bottlenecks.
We **love** what we are building here, and learn a few things about Reactjs, Nodejs, MongoDB and Docker while building Checkmate. **If you would like to support us, please consider giving it a ⭐, think about contributing or providing feedback.** Please note that we do not operate a monetary donation program, make money by deploying servers, or run a SaaS business.
We **love** what we are building here, and we contibuously learn a few things about Reactjs, Nodejs, MongoDB and Docker while building Checkmate.
## 📦 Demo
See [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1!
See [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on Discussions channel).
## 🔗 User's guide
Usage instructions can be found [here](https://bluewavelabs.gitbook.io/checkmate).
Usage instructions can be found [here](https://bluewavelabs.gitbook.io/checkmate). It's still WIP and some of the information there might be outdated. Rest assured, we are doing our best! :)
## 🛠️ Installation
See installation instructions in [Checkmate documentation portal](https://bluewavelabs.gitbook.io/checkmate/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) for a one click Docker deployment.
See installation instructions in [Checkmate documentation portal](https://bluewavelabs.gitbook.io/checkmate/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) for a one click Docker deployment. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
## 💚 Questions & ideas
@@ -49,7 +52,6 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- Page speed monitoring
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
- Docker monitoring
- Port monitoring
- Ping monitoring
- Incidents at a glance
- E-mail notifications
@@ -58,11 +60,13 @@ If you have any questions, suggestions or comments, please use our [Discord chan
**Short term roadmap:**
- Global (distributed) uptime checking on Solana network
- Status pages
- Better notification options
- Better notification options (think Discord, Telegram, Slack)
- More configuration options
- DNS monitoring
- SSL monitoring
- Port monitoring
## 🏗️ Screenshots
@@ -78,10 +82,11 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- [MUI (React framework)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
- Lots of other open source components!
## 🤝 Contributing
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 1900 stars and attracted 35+ contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 2700 stars and attracted almost 40 contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
Here's how you can contribute:
@@ -93,20 +98,21 @@ Here's how you can contribute:
5. Check for good-first-issue's if you are a newcomer.
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
<a href="https://github.com/bluewave-labs/bluewave-uptime/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/bluewave-uptime" />
<!-- <a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
-->
![Alt](https://repobeats.axiom.co/api/embed/c35d999c82dbb31e967427ea4166c14da4172e73.svg "Repobeats analytics image")
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/bluewave-uptime&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
Also check other developer and contributor-friendly projects of BlueWave:
- [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm)
- [BlueWave Onboarding](https://github.com/bluewave-labs/bluewave-onboarding)
- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom)
- [VerifyWise](https://github.com/bluewave-labs/verifywise)
- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
- [BlueWave Onboarding](https://github.com/bluewave-labs/bluewave-onboarding), an application that helps new users learn how to use your product via hints, tours, popups and banners.
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
+1 -1
View File
@@ -361,7 +361,7 @@ const requestRecovery = async (req, res, next) => {
url,
},
email,
"Bluewave Uptime Password Reset"
"Checkmate Password Reset"
);
return res.status(200).json({
+10 -3
View File
@@ -19,18 +19,25 @@ const createHardwareCheck = async (hardwareCheckData) => {
return null;
}
let newUptimePercentage;
if (monitor.uptimePercentage === undefined) {
monitor.uptimePercentage = status === true ? 1 : 0;
newUptimePercentage = status === true ? 1 : 0;
} else {
monitor.uptimePercentage =
newUptimePercentage =
(monitor.uptimePercentage * (n - 1) + (status === true ? 1 : 0)) / n;
}
await monitor.save();
await Monitor.findOneAndUpdate(
{ _id: monitorId },
{ uptimePercentage: newUptimePercentage }
);
const hardwareCheck = await new HardwareCheck({
...hardwareCheckData,
}).save();
return hardwareCheck;
} catch (error) {
console.log("error creating hardware check", error);
error.service = SERVICE_NAME;
error.method = "createHardwareCheck";
throw error;
+29
View File
@@ -0,0 +1,29 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import mochaPlugin from "eslint-plugin-mocha";
/*
Please do not forget to look at the latest eslint configurations and rules.
ESlint v9 configuration is different than v8.
"https://eslint.org/docs/latest/use/configure/"
*/
/** @type {import('eslint').Linter.Config[]} */
export default [
{
languageOptions: {
globals: {
...globals.node, // Add Node.js globals
},
ecmaVersion: 2023,
sourceType: "module",
},
},
pluginJs.configs.recommended, // Core JS rules
mochaPlugin.configs.flat.recommended, // Mocha rules
{
rules: {
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
},
},
];
+5 -5
View File
@@ -1,9 +1,9 @@
{
"openapi": "3.1.0",
"info": {
"title": "BlueWave Uptime",
"summary": "BlueWave Uptime OpenAPI Specifications",
"description": "BlueWave Uptime is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.",
"title": "Checkmate",
"summary": "Checkmate OpenAPI Specifications",
"description": "Checkmate is an open source monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.",
"contact": {
"name": "API Support",
"url": "mailto:support@bluewavelabs.ca",
@@ -11,7 +11,7 @@
},
"license": {
"name": "AGPLv3",
"url": "https://github.com/bluewave-labs/bluewave-uptime/tree/HEAD/LICENSE"
"url": "https://github.com/bluewave-labs/checkmate/tree/HEAD/LICENSE"
},
"version": "1.0"
},
@@ -45,7 +45,7 @@
},
{
"url": "https://checkmate-demo.bluewavelabs.ca/{API_PATH}",
"description": "Bluewave Demo Server",
"description": "Checkmate Demo Server",
"variables": {
"PORT": {
"description": "API Port",
+2182 -1071
View File
File diff suppressed because it is too large. Load diff
+11 -3
View File
@@ -6,15 +6,19 @@
"type": "module",
"scripts": {
"test": "c8 mocha",
"dev": "nodemon index.js"
"dev": "nodemon index.js",
"lint": "eslint .",
"lint-fix": "eslint --fix .",
"format": "prettier --write .",
"format-check": "prettier --check ."
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"bullmq": "5.34.2",
"bcrypt": "5.1.1",
"bullmq": "5.34.4",
"cors": "^2.8.5",
"dockerode": "4.0.2",
"dotenv": "^16.4.5",
@@ -35,9 +39,13 @@
"winston": "^3.13.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"c8": "10.1.3",
"chai": "5.1.2",
"eslint": "^9.17.0",
"eslint-plugin-mocha": "^10.5.0",
"esm": "3.2.25",
"globals": "^15.14.0",
"mocha": "11.0.1",
"nodemon": "3.1.9",
"prettier": "^3.3.3",
+3 -3
View File
@@ -22,7 +22,7 @@
<mj-text
align="left"
font-size="10px"
>Message from BlueWave Uptime Service</mj-text
>Message from Checkmate Service</mj-text
>
</mj-column>
</mj-section>
@@ -31,7 +31,7 @@
<mj-text>
<p>Hello {{name}}!</p>
<p>
One of the admins created an account for you on the BlueWave Uptime server.
One of the admins created an account for you on the Checkmate server.
</p>
<p>You can go ahead and create your account using this link.</p>
<p><a href="{{link}}">{{link}}</a></p>
@@ -44,7 +44,7 @@
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
+2 -2
View File
@@ -21,7 +21,7 @@
<mj-text
align="left"
font-size="10px"
>Message from BlueWave Uptime Service</mj-text
>Message from Checkmate Service</mj-text
>
</mj-column>
<mj-column
@@ -58,7 +58,7 @@
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
+2 -2
View File
@@ -24,7 +24,7 @@
align="left"
font-size="10px"
>
Message from BlueWave Uptime Service
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
@@ -48,7 +48,7 @@
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
+2 -2
View File
@@ -11,7 +11,7 @@
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from BlueWave Uptime Service
Message from Checkmate Service
</mj-text>
</mj-column>
<mj-column width="45%" padding-top="20px">
@@ -37,7 +37,7 @@
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
+2 -2
View File
@@ -11,7 +11,7 @@
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from BlueWave Uptime Service
Message from Checkmate Service
</mj-text>
</mj-column>
<mj-column width="45%" padding-top="20px">
@@ -34,7 +34,7 @@
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
+4 -4
View File
@@ -23,7 +23,7 @@
align="left"
font-size="10px"
>
Message from BlueWave Uptime Service
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
@@ -32,11 +32,11 @@
<mj-text>
<p>Hello {{name}}!</p>
<p>
Thank you for trying out BlueWave Uptime! We developed it with great care to
Thank you for trying out Checkmate! We developed it with great care to
meet our own needs, and we're excited to share it with you.
</p>
<p>
BlueWave Uptime is an automated way of checking whether a service such as a
Checkmate is an automated way of checking whether a service such as a
website or an application is available or not.
</p>
<p>We hope you find our service as valuable as we do.</p>
@@ -49,7 +49,7 @@
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
@@ -649,7 +649,7 @@ describe("Auth Controller - requestRecovery", () => {
url: "http://localhost/set-new-password/recovery-token",
},
"test@test.com",
"Bluewave Uptime Password Reset"
"Checkmate Password Reset"
)
).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
-1
View File
@@ -100,7 +100,6 @@ const successMessages = {
// Queue Controller
QUEUE_GET_METRICS: "Got metrics successfully",
QUEUE_GET_METRICS: "Got job stats successfully",
QUEUE_ADD_JOB: "Job added successfully",
QUEUE_OBLITERATE: "Queue obliterated",