mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2025-03-14 10:33:19 +00:00
Merge pull request #1287 from bluewave-labs/develop
Develop -> Master 2.0 release
This commit is contained in:
@ -1,23 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
href="./bluewave_favicon.ico"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>BlueWave Uptime</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/main.jsx"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="./checkmate_favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Checkmate</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
1208
Client/package-lock.json
generated
1208
Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,18 +11,17 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@mui/icons-material": "^5.15.17",
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.16",
|
||||
"@mui/icons-material": "6.1.10",
|
||||
"@mui/lab": "6.0.0-beta.18",
|
||||
"@mui/material": "6.1.10",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.22.0",
|
||||
"@mui/x-date-pickers": "7.22.0",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"@mui/x-data-grid": "7.23.0",
|
||||
"@mui/x-date-pickers": "7.23.0",
|
||||
"@reduxjs/toolkit": "2.4.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
"dayjs": "1.11.13",
|
||||
"joi": "17.13.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
@ -32,7 +31,7 @@
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recharts": "2.13.1",
|
||||
"recharts": "2.14.1",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
4
Client/public/checkmate_favicon.svg
Normal file
4
Client/public/checkmate_favicon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" fill="#1570EF"/>
|
||||
<path d="M21.8849 14.3622H19.2287C19.1529 13.9266 19.0133 13.5407 18.8097 13.2045C18.6061 12.8636 18.3527 12.5748 18.0497 12.3381C17.7467 12.1013 17.401 11.9238 17.0128 11.8054C16.6293 11.6823 16.215 11.6207 15.7699 11.6207C14.9792 11.6207 14.2784 11.8196 13.6676 12.2173C13.0568 12.6103 12.5786 13.188 12.233 13.9503C11.8873 14.7079 11.7145 15.6335 11.7145 16.7273C11.7145 17.84 11.8873 18.7775 12.233 19.5398C12.5833 20.2973 13.0616 20.8703 13.6676 21.2585C14.2784 21.642 14.9768 21.8338 15.7628 21.8338C16.1984 21.8338 16.6056 21.777 16.9844 21.6634C17.3679 21.545 17.7112 21.3722 18.0142 21.1449C18.322 20.9176 18.58 20.6383 18.7884 20.3068C19.0014 19.9754 19.1482 19.5966 19.2287 19.1705L21.8849 19.1847C21.7855 19.8759 21.5701 20.5246 21.2386 21.1307C20.9119 21.7367 20.4834 22.2718 19.9531 22.7358C19.4228 23.1951 18.8026 23.5549 18.0923 23.8153C17.3821 24.071 16.5938 24.1989 15.7273 24.1989C14.4489 24.1989 13.3078 23.9029 12.304 23.3111C11.3002 22.7192 10.5095 21.8646 9.93182 20.7472C9.35417 19.6297 9.06534 18.2898 9.06534 16.7273C9.06534 15.16 9.35653 13.8201 9.93892 12.7074C10.5213 11.59 11.3144 10.7353 12.3182 10.1435C13.322 9.55161 14.4583 9.25568 15.7273 9.25568C16.5369 9.25568 17.2898 9.36932 17.9858 9.59659C18.6818 9.82386 19.3021 10.1577 19.8466 10.598C20.3911 11.0336 20.8385 11.5687 21.1889 12.2031C21.544 12.8329 21.776 13.5526 21.8849 14.3622Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -1,7 +0,0 @@
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
@ -1,14 +1,16 @@
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
// import "./App.css";
|
||||
import NotFound from "./Pages/NotFound";
|
||||
import Login from "./Pages/Auth/Login";
|
||||
import Register from "./Pages/Auth/Register/Register";
|
||||
import HomeLayout from "./Layouts/HomeLayout";
|
||||
import Account from "./Pages/Account";
|
||||
import Monitors from "./Pages/Monitors/Home";
|
||||
import CreateMonitor from "./Pages/Monitors/CreateMonitor";
|
||||
import CreateInfrastructureMonitor from "./Pages/Infrastructure/CreateMonitor";
|
||||
import Incidents from "./Pages/Incidents";
|
||||
import Status from "./Pages/Status";
|
||||
import Integrations from "./Pages/Integrations";
|
||||
@ -21,24 +23,24 @@ import ProtectedRoute from "./Components/ProtectedRoute";
|
||||
import Details from "./Pages/Monitors/Details";
|
||||
import AdvancedSettings from "./Pages/AdvancedSettings";
|
||||
import Maintenance from "./Pages/Maintenance";
|
||||
import withAdminCheck from "./HOC/withAdminCheck";
|
||||
import withAdminProp from "./HOC/withAdminProp";
|
||||
import Configure from "./Pages/Monitors/Configure";
|
||||
import PageSpeed from "./Pages/PageSpeed";
|
||||
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
|
||||
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenance";
|
||||
import PageSpeedDetails from "./Pages/PageSpeed/Details";
|
||||
import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
|
||||
import HomeLayout from "./Components/Layouts/HomeLayout";
|
||||
import withAdminCheck from "./Components/HOC/withAdminCheck";
|
||||
import withAdminProp from "./Components/HOC/withAdminProp";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import lightTheme from "./Utils/Theme/lightTheme";
|
||||
import darkTheme from "./Utils/Theme/darkTheme";
|
||||
import { useSelector } from "react-redux";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { getAppSettings, updateAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||
import { getAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
import { Infrastructure } from "./Pages/Infrastructure";
|
||||
import InfrastructureDetails from "./Pages/Infrastructure/Details";
|
||||
function App() {
|
||||
const AdminCheckedRegister = withAdminCheck(Register);
|
||||
const MonitorsWithAdminProp = withAdminProp(Monitors);
|
||||
@ -48,6 +50,7 @@ function App() {
|
||||
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
|
||||
const SettingsWithAdminProp = withAdminProp(Settings);
|
||||
const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
|
||||
const InfrastructureDetailsWithAdminProp = withAdminProp(InfrastructureDetails);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
@ -67,8 +70,19 @@ function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
/* Extract Themeprovider, baseline and global styles to Styles */
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
<CssBaseline />
|
||||
<GlobalStyles
|
||||
styles={({ palette }) => {
|
||||
return {
|
||||
body: {
|
||||
backgroundImage: `radial-gradient(circle, ${palette.gradient.color1}, ${palette.gradient.color2}, ${palette.gradient.color3}, ${palette.gradient.color4}, ${palette.gradient.color5})`,
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
{/* Extract Routes to Routes */}
|
||||
<Routes>
|
||||
<Route
|
||||
exact
|
||||
@ -78,7 +92,7 @@ function App() {
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
|
||||
element={<Navigate to="/monitors" />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors"
|
||||
@ -96,6 +110,35 @@ function App() {
|
||||
path="/monitors/configure/:monitorId/"
|
||||
element={<ProtectedRoute Component={Configure} />}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed"
|
||||
element={<ProtectedRoute Component={PageSpeedWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/create"
|
||||
element={<ProtectedRoute Component={CreatePageSpeed} />}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/:monitorId"
|
||||
element={<ProtectedRoute Component={PageSpeedDetailsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/configure/:monitorId"
|
||||
element={<ProtectedRoute Component={PageSpeedConfigure} />}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure"
|
||||
element={<ProtectedRoute Component={Infrastructure} />}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/create"
|
||||
element={<ProtectedRoute Component={CreateInfrastructureMonitor} />}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/:monitorId"
|
||||
element={<ProtectedRoute Component={InfrastructureDetailsWithAdminProp} />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="incidents/:monitorId?"
|
||||
element={<ProtectedRoute Component={Incidents} />}
|
||||
@ -152,22 +195,6 @@ function App() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed"
|
||||
element={<ProtectedRoute Component={PageSpeedWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/create"
|
||||
element={<ProtectedRoute Component={CreatePageSpeed} />}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/:monitorId"
|
||||
element={<ProtectedRoute Component={PageSpeedDetailsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/configure/:monitorId"
|
||||
element={<ProtectedRoute Component={PageSpeedConfigure} />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
@ -181,16 +208,18 @@ function App() {
|
||||
path="/register"
|
||||
element={<AdminCheckedRegister />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/register/:token"
|
||||
element={<Register />}
|
||||
/>
|
||||
{/* <Route path="/toast" element={<ToastComponent />} /> */}
|
||||
|
||||
<Route
|
||||
path="*"
|
||||
element={<NotFound />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/forgot-password"
|
||||
element={<ForgotPassword />}
|
||||
|
@ -31,6 +31,9 @@ const icons = {
|
||||
|
||||
const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
const theme = useTheme();
|
||||
/* TODO
|
||||
This needs fixing. text bg and border not necessarilly exist. Probably text becomes contrastText. border becomes contrastText. bg becomes dark.
|
||||
Check possible variants, see implementation in light and dark theme, and adjust */
|
||||
const { text, bg, border } = theme.palette[variant];
|
||||
const icon = icons[variant];
|
||||
|
||||
|
@ -27,7 +27,7 @@
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1) var(--env-var-spacing-2);
|
||||
padding: var(--env-var-spacing-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root span {
|
||||
@ -43,7 +43,7 @@
|
||||
height: 20px;
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: 6px var(--env-var-spacing-2);
|
||||
padding: var(--env-var-spacing-1);
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableRow-root {
|
||||
height: 50px;
|
||||
|
214
Client/src/Components/Charts/AreaChart/index.jsx
Normal file
214
Client/src/Components/Charts/AreaChart/index.jsx
Normal file
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* CustomAreaChart component for rendering an area chart with optional gradient and custom ticks.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {Array} props.data - The data array for the chart.
|
||||
* @param {Array} props.dataKeys - An array of data keys to be plotted as separate areas.
|
||||
* @param {string} props.xKey - The key for the x-axis data.
|
||||
* @param {string} [props.yKey] - The key for the y-axis data (optional).
|
||||
* @param {Object} [props.xTick] - Custom tick component for the x-axis.
|
||||
* @param {Object} [props.yTick] - Custom tick component for the y-axis.
|
||||
* @param {string} [props.strokeColor] - The base stroke color for the areas.
|
||||
* If not provided, uses a predefined color palette.
|
||||
* @param {string} [props.fillColor] - The base fill color for the areas.
|
||||
* @param {boolean} [props.gradient=false] - Whether to apply a gradient fill to the areas.
|
||||
* @param {string} [props.gradientDirection="vertical"] - The direction of the gradient.
|
||||
* @param {string} [props.gradientStartColor] - The start color of the gradient.
|
||||
* Defaults to the area's stroke color if not provided.
|
||||
* @param {string} [props.gradientEndColor] - The end color of the gradient.
|
||||
* @param {Object} [props.customTooltip] - Custom tooltip component for the chart.
|
||||
* @param {string|number} [props.height="100%"] - Height of the chart container.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered area chart component.
|
||||
*
|
||||
* @example
|
||||
* // Single series chart
|
||||
* <CustomAreaChart
|
||||
* data={temperatureData}
|
||||
* dataKeys={["temperature"]}
|
||||
* xKey="date"
|
||||
* yKey="temperature"
|
||||
* gradient={true}
|
||||
* gradientStartColor="#ff6b6b"
|
||||
* gradientEndColor="#4ecdc4"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Multi-series chart with custom tooltip
|
||||
* <CustomAreaChart
|
||||
* data={performanceData}
|
||||
* dataKeys={["cpu.usage", "memory.usage"]}
|
||||
* xKey="timestamp"
|
||||
* xTick={<CustomTimeTick />}
|
||||
* yTick={<PercentageTick />}
|
||||
* gradient={true}
|
||||
* customTooltip={({ active, payload, label }) => (
|
||||
* <CustomTooltip
|
||||
* label={label}
|
||||
* payload={payload}
|
||||
* active={active}
|
||||
* />
|
||||
* )}
|
||||
* />
|
||||
*/
|
||||
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { createGradient } from "../Utils/gradientUtils";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useId } from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
const CustomAreaChart = ({
|
||||
data,
|
||||
dataKeys,
|
||||
xKey,
|
||||
xDomain,
|
||||
yKey,
|
||||
yDomain,
|
||||
xTick,
|
||||
yTick,
|
||||
strokeColor,
|
||||
fillColor,
|
||||
gradient = false,
|
||||
gradientDirection = "vertical",
|
||||
gradientStartColor,
|
||||
gradientEndColor,
|
||||
customTooltip,
|
||||
height = "100%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const uniqueId = useId();
|
||||
|
||||
const AREA_COLORS = [
|
||||
// Blues
|
||||
"#3182bd", // Deep blue
|
||||
"#6baed6", // Medium blue
|
||||
"#9ecae1", // Light blue
|
||||
|
||||
// Greens
|
||||
"#74c476", // Soft green
|
||||
"#a1d99b", // Light green
|
||||
"#c7e9c0", // Pale green
|
||||
|
||||
// Oranges
|
||||
"#fdae6b", // Warm orange
|
||||
"#fdd0a2", // Light orange
|
||||
"#feedde", // Pale orange
|
||||
|
||||
// Purples
|
||||
"#9467bd", // Lavender
|
||||
"#a55194", // Deep magenta
|
||||
"#c994c7", // Soft magenta
|
||||
|
||||
// Reds
|
||||
"#ff9896", // Soft red
|
||||
"#de2d26", // Deep red
|
||||
"#fc9272", // Medium red
|
||||
|
||||
// Cyans/Teals
|
||||
"#17becf", // Cyan
|
||||
"#7fcdbb", // Teal
|
||||
"#a1dab4", // Light teal
|
||||
|
||||
// Yellows
|
||||
"#fec44f", // Mustard
|
||||
"#fee391", // Light yellow
|
||||
"#ffffd4", // Pale yellow
|
||||
|
||||
// Additional colors
|
||||
"#e377c2", // Soft pink
|
||||
"#bcbd22", // Olive
|
||||
"#2ca02c", // Vibrant green
|
||||
];
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={height}
|
||||
// FE team HELP! Why does this overflow if set to 100%?
|
||||
>
|
||||
<AreaChart data={data}>
|
||||
<XAxis
|
||||
dataKey={xKey}
|
||||
{...(xDomain && { domain: xDomain })}
|
||||
{...(xTick && { tick: xTick })}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey={yKey}
|
||||
{...(yDomain && { domain: yDomain })}
|
||||
{...(yTick && { tick: yTick })}
|
||||
/>
|
||||
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.border.light}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
{dataKeys.map((dataKey, index) => {
|
||||
const gradientId = `gradient-${uniqueId}-${index}`;
|
||||
|
||||
return (
|
||||
<Fragment key={dataKey}>
|
||||
{gradient === true &&
|
||||
createGradient({
|
||||
id: gradientId,
|
||||
startColor: gradientStartColor || AREA_COLORS[index],
|
||||
endColor: gradientEndColor,
|
||||
direction: gradientDirection,
|
||||
})}
|
||||
<Area
|
||||
yKey={dataKey}
|
||||
key={dataKey}
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={strokeColor || AREA_COLORS[index]}
|
||||
fill={gradient === true ? `url(#${gradientId})` : fillColor}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{customTooltip ? (
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={customTooltip}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip />
|
||||
)}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
CustomAreaChart.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
dataKeys: PropTypes.array.isRequired,
|
||||
xTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
|
||||
yTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
|
||||
xKey: PropTypes.string.isRequired,
|
||||
xDomain: PropTypes.array,
|
||||
yKey: PropTypes.string,
|
||||
yDomain: PropTypes.array,
|
||||
fillColor: PropTypes.string,
|
||||
strokeColor: PropTypes.string,
|
||||
gradient: PropTypes.bool,
|
||||
gradientDirection: PropTypes.string,
|
||||
gradientStartColor: PropTypes.string,
|
||||
gradientEndColor: PropTypes.string,
|
||||
customTooltip: PropTypes.object,
|
||||
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default CustomAreaChart;
|
@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import "./index.css";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
/* TODO add prop validation and jsdocs */
|
||||
const BarChart = ({ checks = [] }) => {
|
||||
const theme = useTheme();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
@ -43,7 +44,7 @@ const BarChart = ({ checks = [] }) => {
|
||||
position="relative"
|
||||
width={theme.spacing(4.5)}
|
||||
height="100%"
|
||||
backgroundColor={theme.palette.background.fill}
|
||||
backgroundColor={theme.palette.border.light}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
}}
|
||||
@ -65,7 +66,9 @@ const BarChart = ({ checks = [] }) => {
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={
|
||||
check.status ? theme.palette.success.main : theme.palette.error.text
|
||||
check.status
|
||||
? theme.palette.success.main
|
||||
: theme.palette.error.contrastText
|
||||
}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
@ -111,7 +114,7 @@ const BarChart = ({ checks = [] }) => {
|
||||
],
|
||||
sx: {
|
||||
"& .MuiTooltip-tooltip": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
@ -138,7 +141,9 @@ const BarChart = ({ checks = [] }) => {
|
||||
width="9px"
|
||||
height="100%"
|
||||
backgroundColor={
|
||||
check.status ? theme.palette.success.bg : theme.palette.error.bg
|
||||
check.status
|
||||
? theme.palette.success.contrastText
|
||||
: theme.palette.error.dark
|
||||
}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
@ -153,7 +158,7 @@ const BarChart = ({ checks = [] }) => {
|
||||
width="100%"
|
||||
height={`${animate ? check.responseTime : 0}%`}
|
||||
backgroundColor={
|
||||
check.status ? theme.palette.success.main : theme.palette.error.text
|
||||
check.status ? theme.palette.success.main : theme.palette.error.main
|
||||
}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
|
14
Client/src/Components/Charts/CustomGauge/index.css
Normal file
14
Client/src/Components/Charts/CustomGauge/index.css
Normal file
@ -0,0 +1,14 @@
|
||||
.radial-chart {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.radial-chart-base {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.radial-chart-progress {
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: center;
|
||||
transition: stroke-dashoffset 1.5s ease-in-out;
|
||||
}
|
118
Client/src/Components/Charts/CustomGauge/index.jsx
Normal file
118
Client/src/Components/Charts/CustomGauge/index.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
|
||||
/**
|
||||
* A Performant SVG based circular gauge
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component properties
|
||||
* @param {number} [props.progress=0] - Progress percentage (0-100)
|
||||
* @param {number} [props.radius=60] - Radius of the gauge circle
|
||||
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
|
||||
* @param {number} [props.threshold=50] - Threshold for color change
|
||||
*
|
||||
* @example
|
||||
* <CustomGauge
|
||||
* progress={75}
|
||||
* radius={50}
|
||||
* strokeWidth={10}
|
||||
* threshold={50}
|
||||
* />
|
||||
*
|
||||
* @returns {React.ReactElement} Rendered CustomGauge component
|
||||
*/
|
||||
const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
|
||||
const theme = useTheme();
|
||||
// Calculate the length of the stroke for the circle
|
||||
const { circumference, totalSize, strokeLength } = useMemo(
|
||||
() => ({
|
||||
circumference: 2 * Math.PI * radius,
|
||||
totalSize: radius * 2 + strokeWidth * 2,
|
||||
strokeLength: (progress / 100) * (2 * Math.PI * radius),
|
||||
}),
|
||||
[radius, strokeWidth, progress]
|
||||
);
|
||||
|
||||
// Handle initial animation
|
||||
const [offset, setOffset] = useState(circumference);
|
||||
useEffect(() => {
|
||||
setOffset(circumference);
|
||||
const timer = setTimeout(() => {
|
||||
setOffset(circumference - strokeLength);
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [progress, circumference, strokeLength]);
|
||||
|
||||
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
|
||||
|
||||
const fillColor =
|
||||
progressWithinRange > threshold
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: theme.palette.primary.main;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="radial-chart"
|
||||
width={radius}
|
||||
height={radius}
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${totalSize} ${totalSize}`}
|
||||
width={radius}
|
||||
height={radius}
|
||||
>
|
||||
<circle
|
||||
className="radial-chart-base"
|
||||
stroke={theme.palette.background.fill}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
cx={totalSize / 2} // Center the circle
|
||||
cy={totalSize / 2} // Center the circle
|
||||
r={radius}
|
||||
/>
|
||||
<circle
|
||||
className="radial-chart-progress"
|
||||
stroke={fillColor}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
cx={totalSize / 2}
|
||||
cy={totalSize / 2}
|
||||
r={radius}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<Typography
|
||||
className="radial-chart-text"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
...theme.typography.body2,
|
||||
fill: theme.typography.body2.color,
|
||||
}}
|
||||
>
|
||||
{`${progressWithinRange.toFixed(1)}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomGauge;
|
||||
|
||||
CustomGauge.propTypes = {
|
||||
progress: PropTypes.number,
|
||||
radius: PropTypes.number,
|
||||
strokeWidth: PropTypes.number,
|
||||
threshold: PropTypes.number,
|
||||
};
|
111
Client/src/Components/Charts/Gauge/index.jsx
Normal file
111
Client/src/Components/Charts/Gauge/index.jsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/* TODO delete component */
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
const CHART_MAXIMUM_DATA = {
|
||||
value: MAXIMUM_VALUE,
|
||||
fill: "transparent",
|
||||
};
|
||||
const PROGRESS_THRESHOLD = 50;
|
||||
const DEFAULT_WIDTH = 60;
|
||||
|
||||
const RADIUS_SIZE = "90%";
|
||||
const START_ANGLE = 90;
|
||||
const CHART_RANGE = 360;
|
||||
const RADIAL_BAR_CHART_PROPS = {
|
||||
innerRadius: RADIUS_SIZE,
|
||||
outerRadius: RADIUS_SIZE,
|
||||
barSize: 6,
|
||||
startAngle: START_ANGLE,
|
||||
endAngle: START_ANGLE - CHART_RANGE,
|
||||
margin: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
};
|
||||
|
||||
const RADIAL_BAR_PROPS = {
|
||||
dataKey: "value",
|
||||
cornerRadius: 8,
|
||||
};
|
||||
|
||||
Gauge.propTypes = {
|
||||
progressValue: PropTypes.number.isRequired,
|
||||
width: PropTypes.number,
|
||||
};
|
||||
|
||||
/**
|
||||
* A circular gauge component that displays a progress value and text.
|
||||
* The gauge fills based on the progress value and changes color at a 50% threshold.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component props
|
||||
* @param {number} props.progressValue - The value to display in the gauge (0-100)
|
||||
* @param {number} props.width - Width of the gauge container in pixels
|
||||
* @returns {JSX.Element} A circular gauge chart with progress value and text
|
||||
*
|
||||
* @example
|
||||
* <Gauge
|
||||
* progressValue={75}
|
||||
* width={200}
|
||||
* />
|
||||
*/
|
||||
|
||||
function Gauge({ progressValue, width = DEFAULT_WIDTH }) {
|
||||
const theme = useTheme();
|
||||
const myProgressValue = Math.max(MINIMUM_VALUE, Math.min(progressValue, MAXIMUM_VALUE));
|
||||
const chartColor =
|
||||
progressValue > PROGRESS_THRESHOLD
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.percentage.uptimePoor;
|
||||
const chartData = [
|
||||
CHART_MAXIMUM_DATA,
|
||||
{
|
||||
value: myProgressValue,
|
||||
fill: chartColor,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
aspect={1}
|
||||
width={width}
|
||||
style={{ marginInline: "auto" }}
|
||||
>
|
||||
<RadialBarChart
|
||||
{...RADIAL_BAR_CHART_PROPS}
|
||||
data={chartData}
|
||||
>
|
||||
<RadialBar
|
||||
{...RADIAL_BAR_PROPS}
|
||||
fill={
|
||||
progressValue > PROGRESS_THRESHOLD
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.percentage.uptimePoor
|
||||
}
|
||||
background={{ fill: theme.palette.background.fill }}
|
||||
label={{
|
||||
position: "center",
|
||||
content: () => (
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
style={{
|
||||
...theme.typography.body2,
|
||||
fill: theme.typography.body2.color,
|
||||
}}
|
||||
>
|
||||
{`${myProgressValue}%`}
|
||||
</text>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export { Gauge };
|
277
Client/src/Components/Charts/Utils/chartUtils.jsx
Normal file
277
Client/src/Components/Charts/Utils/chartUtils.jsx
Normal file
@ -0,0 +1,277 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { Text } from "recharts";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
|
||||
/**
|
||||
* Custom tick component for rendering time with timezone.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {number} props.x - The x-coordinate for the tick.
|
||||
* @param {number} props.y - The y-coordinate for the tick.
|
||||
* @param {Object} props.payload - The payload object containing tick data.
|
||||
* @param {number} props.index - The index of the tick.
|
||||
* @returns {JSX.Element} The rendered tick component.
|
||||
*/
|
||||
export const TzTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 10}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.tertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
TzTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tick component for rendering percentage values.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {number} props.x - The x-coordinate for the tick.
|
||||
* @param {number} props.y - The y-coordinate for the tick.
|
||||
* @param {Object} props.payload - The payload object containing tick data.
|
||||
* @param {number} props.index - The index of the tick.
|
||||
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
|
||||
*/
|
||||
export const PercentTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
if (index === 0) return null;
|
||||
return (
|
||||
<Text
|
||||
x={x - 20}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.tertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{`${(payload?.value * 100).toFixed()}%`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
PercentTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a decimal value to a formatted percentage string.
|
||||
*
|
||||
* @param {number} value - The decimal value to convert (e.g., 0.75)
|
||||
* @returns {string} Formatted percentage string (e.g., "75.00%") or original input if not a number
|
||||
*
|
||||
* @example
|
||||
* getFormattedPercentage(0.7543) // Returns "75.43%"
|
||||
* getFormattedPercentage(1) // Returns "100.00%"
|
||||
* getFormattedPercentage("test") // Returns "test"
|
||||
*/
|
||||
const getFormattedPercentage = (value) => {
|
||||
if (typeof value !== "number") return value;
|
||||
return `${(value * 100).toFixed(2)}.%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tooltip component for displaying infrastructure data.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {boolean} props.active - Indicates if the tooltip is active.
|
||||
* @param {Array} props.payload - The payload array containing tooltip data.
|
||||
* @param {string} props.label - The label for the tooltip.
|
||||
* @param {string} props.yKey - The key for the y-axis data.
|
||||
* @param {string} props.yLabel - The label for the y-axis data.
|
||||
* @param {string} props.dotColor - The color of the dot in the tooltip.
|
||||
* @returns {JSX.Element|null} The rendered tooltip component or null if inactive.
|
||||
*/
|
||||
export const InfrastructureTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
yKey,
|
||||
yIdx = -1,
|
||||
yLabel,
|
||||
dotColor,
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
if (active && payload && payload.length) {
|
||||
const [hardwareType, metric] = yKey.split(".");
|
||||
return (
|
||||
<Box
|
||||
className="area-tooltip"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={dotColor}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(3)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{yIdx >= 0
|
||||
? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}`
|
||||
: `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][metric])}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{/* Display original value */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
InfrastructureTooltip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.array,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Date),
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
yKey: PropTypes.string,
|
||||
yIdx: PropTypes.number,
|
||||
yLabel: PropTypes.string,
|
||||
dotColor: PropTypes.string,
|
||||
};
|
||||
|
||||
export const TemperatureTooltip = ({ active, payload, label, keys, dotColor }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
const formatCoreKey = (key) => {
|
||||
return key.replace(/^core(\d+)$/, "Core $1");
|
||||
};
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<Box
|
||||
className="area-tooltip"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="column">
|
||||
{keys.map((key) => {
|
||||
return (
|
||||
<Stack
|
||||
key={key}
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(3)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={dotColor}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{`${formatCoreKey(key)}: ${payload[0].payload[key]} °C`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography component="span"></Typography>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
TemperatureTooltip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
keys: PropTypes.array,
|
||||
payload: PropTypes.array,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Date),
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
};
|
47
Client/src/Components/Charts/Utils/gradientUtils.jsx
Normal file
47
Client/src/Components/Charts/Utils/gradientUtils.jsx
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Creates an SVG gradient definition for use in charts
|
||||
* @param {Object} params - The gradient parameters
|
||||
* @param {string} [params.id="colorUv"] - Unique identifier for the gradient
|
||||
* @param {string} params.startColor - Starting color of the gradient (hex, rgb, or color name)
|
||||
* @param {string} params.endColor - Ending color of the gradient (hex, rgb, or color name)
|
||||
* @param {number} [params.startOpacity=0.8] - Starting opacity (0-1)
|
||||
* @param {number} [params.endOpacity=0] - Ending opacity (0-1)
|
||||
* @param {('vertical'|'horizontal')} [params.direction="vertical"] - Direction of the gradient
|
||||
* @returns {JSX.Element} SVG gradient definition element
|
||||
* @example
|
||||
* createCustomGradient({
|
||||
* startColor: "#1976D2",
|
||||
* endColor: "#42A5F5",
|
||||
* direction: "horizontal"
|
||||
* })
|
||||
*/
|
||||
|
||||
export const createGradient = ({
|
||||
id,
|
||||
startColor,
|
||||
endColor,
|
||||
startOpacity = 0.8,
|
||||
endOpacity = 0,
|
||||
direction = "vertical", // or "horizontal"
|
||||
}) => (
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={id}
|
||||
x1={direction === "vertical" ? "0" : "0"}
|
||||
y1={direction === "vertical" ? "0" : "0"}
|
||||
x2={direction === "vertical" ? "0" : "1"}
|
||||
y2={direction === "vertical" ? "1" : "0"}
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={startColor}
|
||||
stopOpacity={startOpacity}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={endColor}
|
||||
stopOpacity={endOpacity}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
);
|
@ -20,14 +20,13 @@ import { useTheme } from "@emotion/react";
|
||||
*
|
||||
* @returns {React.Element} The `Check` component with a check icon and a label, defined by the `text` prop.
|
||||
*/
|
||||
const Check = ({ text, variant = "info", outlined = false }) => {
|
||||
const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => {
|
||||
const theme = useTheme();
|
||||
const colors = {
|
||||
success: theme.palette.success.main,
|
||||
error: theme.palette.error.text,
|
||||
error: theme.palette.error.main,
|
||||
info: theme.palette.info.border,
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@ -54,6 +53,7 @@ const Check = ({ text, variant = "info", outlined = false }) => {
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{noHighlightText && <Typography component="span">{noHighlightText}</Typography>}{" "}
|
||||
{text}
|
||||
</Typography>
|
||||
</Stack>
|
||||
@ -62,6 +62,7 @@ const Check = ({ text, variant = "info", outlined = false }) => {
|
||||
|
||||
Check.propTypes = {
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||
noHighlightText: PropTypes.string,
|
||||
variant: PropTypes.oneOf(["info", "error", "success"]),
|
||||
outlined: PropTypes.bool,
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
|
||||
aria-describedby={ariaDescribedBy}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onClick={(e)=>e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
@ -62,7 +62,8 @@ GenericDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
|
||||
.isRequired,
|
||||
};
|
||||
|
||||
export { GenericDialog };
|
||||
|
@ -51,7 +51,6 @@ Dialog.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
confirmationButtonLabel: PropTypes.string.isRequired,
|
||||
|
@ -16,11 +16,11 @@ import "./index.css";
|
||||
* @param {string} props.title - The title to be displayed in the fallback UI.
|
||||
* @param {Array<string>} props.checks - An array of strings representing the checks to display.
|
||||
* @param {string} [props.link="/"] - The link to navigate to.
|
||||
*
|
||||
* @param {boolean} [props.vowelStart=false] - Whether the title starts with a vowel.
|
||||
* @returns {JSX.Element} The rendered fallback UI.
|
||||
*/
|
||||
|
||||
const Fallback = ({ title, checks, link = "/", isAdmin }) => {
|
||||
const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
@ -48,7 +48,7 @@ const Fallback = ({ title, checks, link = "/", isAdmin }) => {
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
maxWidth={"275px"}
|
||||
maxWidth={"300px"}
|
||||
zIndex={1}
|
||||
>
|
||||
<Typography
|
||||
@ -56,7 +56,7 @@ const Fallback = ({ title, checks, link = "/", isAdmin }) => {
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
A {title} is used to:
|
||||
{vowelStart ? "An" : "A"} {title} is used to:
|
||||
</Typography>
|
||||
{checks.map((check, index) => (
|
||||
<Check
|
||||
@ -86,6 +86,7 @@ Fallback.propTypes = {
|
||||
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
link: PropTypes.string,
|
||||
isAdmin: PropTypes.bool,
|
||||
vowelStart: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Fallback;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { logger } from "../Utils/Logger";
|
||||
import { networkService } from "../main";
|
||||
import { logger } from "../../Utils/Logger";
|
||||
import { networkService } from "../../main";
|
||||
|
||||
const withAdminCheck = (WrappedComponent) => {
|
||||
const WithAdminCheck = (props) => {
|
29
Client/src/Components/Heading/index.jsx
Normal file
29
Client/src/Components/Heading/index.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* A heading component that renders text with a specific heading level.
|
||||
*
|
||||
* @param {Object} props - The properties passed to the Heading component.
|
||||
* @param {('h1'|'h2'|'h3')} props.component - The heading level for the component.
|
||||
* @param {string} props.children - The content to display inside the heading.
|
||||
* @returns {JSX.Element} The Typography component with specified heading properties.
|
||||
*/
|
||||
|
||||
function Heading({ component, children }) {
|
||||
return (
|
||||
<Typography
|
||||
component={component}
|
||||
variant="h2"
|
||||
fontWeight={600}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
Heading.propTypes = {
|
||||
component: PropTypes.oneOf(["h1", "h2", "h3"]).isRequired,
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
export { Heading };
|
71
Client/src/Components/HttpStatusLabel/index.jsx
Normal file
71
Client/src/Components/HttpStatusLabel/index.jsx
Normal file
@ -0,0 +1,71 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { BaseLabel } from "../Label";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {number} props.status - The http status for the label
|
||||
* @param {Styles} props.customStyles - CSS Styles passed from parent component
|
||||
* @returns {JSX.Element}
|
||||
* @example
|
||||
* // Render a http status label
|
||||
* <HttpStatusLabel status={404} />
|
||||
*/
|
||||
|
||||
const DEFAULT_CODE = 9999; // Default code for unknown status
|
||||
|
||||
const handleStatusCode = (status) => {
|
||||
if (status >= 100 && status < 600) {
|
||||
return status;
|
||||
}
|
||||
return DEFAULT_CODE;
|
||||
};
|
||||
|
||||
const getRoundedStatusCode = (status) => {
|
||||
return Math.floor(status / 100) * 100;
|
||||
};
|
||||
|
||||
const HttpStatusLabel = ({ status, customStyles }) => {
|
||||
const theme = useTheme();
|
||||
const colors = {
|
||||
400: {
|
||||
dotColor: theme.palette.warning.main,
|
||||
bgColor: theme.palette.warning.dark,
|
||||
borderColor: theme.palette.warning.light,
|
||||
},
|
||||
500: {
|
||||
dotColor: theme.palette.error.main,
|
||||
bgColor: theme.palette.error.dark,
|
||||
borderColor: theme.palette.error.light,
|
||||
},
|
||||
default: {
|
||||
dotColor: theme.palette.unresolved.main,
|
||||
bgColor: theme.palette.unresolved.bg,
|
||||
borderColor: theme.palette.unresolved.light,
|
||||
},
|
||||
};
|
||||
|
||||
const statusCode = handleStatusCode(status);
|
||||
|
||||
const { borderColor, bgColor, dotColor } =
|
||||
colors[getRoundedStatusCode(statusCode)] || colors.default;
|
||||
return (
|
||||
<BaseLabel
|
||||
label={String(statusCode)}
|
||||
styles={{
|
||||
color: dotColor,
|
||||
backgroundColor: bgColor,
|
||||
borderColor: borderColor,
|
||||
...customStyles,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
HttpStatusLabel.propTypes = {
|
||||
status: PropTypes.number,
|
||||
customStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
export { HttpStatusLabel };
|
@ -37,9 +37,9 @@ const Checkbox = ({
|
||||
onChange,
|
||||
isDisabled,
|
||||
}) => {
|
||||
/* TODO move sizes to theme */
|
||||
const sizes = { small: "14px", medium: "16px", large: "18px" };
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="checkbox-wrapper"
|
||||
@ -54,9 +54,10 @@ const Checkbox = ({
|
||||
"aria-label": "controlled checkbox",
|
||||
id: id,
|
||||
}}
|
||||
sx={{
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"& svg": { width: sizes[size], height: sizes[size] },
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@ -65,7 +66,6 @@ const Checkbox = ({
|
||||
sx={{
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
p: theme.spacing(2.5),
|
||||
m: theme.spacing(-2.5),
|
||||
"& .MuiButtonBase-root": {
|
||||
width: theme.spacing(10),
|
||||
p: 0,
|
||||
@ -78,6 +78,10 @@ const Checkbox = ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
".MuiFormControlLabel-label.Mui-disabled": {
|
||||
color: theme.palette.text.tertiary,
|
||||
opacity: 0.25,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -85,7 +89,7 @@ const Checkbox = ({
|
||||
|
||||
Checkbox.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||
isChecked: PropTypes.bool.isRequired,
|
||||
value: PropTypes.string,
|
||||
|
@ -1,40 +0,0 @@
|
||||
.field {
|
||||
min-width: 250px;
|
||||
}
|
||||
.field h3.MuiTypography-root,
|
||||
.field h5.MuiTypography-root,
|
||||
.field input,
|
||||
.field textarea,
|
||||
.field .input-error {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.field h5.MuiTypography-root {
|
||||
position: relative;
|
||||
opacity: 0.8;
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
.field .MuiInputBase-root:has(input) {
|
||||
height: 34px;
|
||||
}
|
||||
.field .MuiInputBase-root:has(.MuiInputAdornment-root) {
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
|
||||
.field input {
|
||||
height: 100%;
|
||||
padding: 0 var(--env-var-spacing-1-minus);
|
||||
}
|
||||
.field .MuiInputBase-root:has(textarea) {
|
||||
padding: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
|
||||
.register-page .field .MuiOutlinedInput-root fieldset,
|
||||
.register-page .field input,
|
||||
.login-page .field .MuiOutlinedInput-root fieldset,
|
||||
.login-page .field input,
|
||||
.forgot-password-page .field .MuiOutlinedInput-root fieldset,
|
||||
.forgot-password-page .field input,
|
||||
.set-new-password-page .field .MuiOutlinedInput-root fieldset,
|
||||
.set-new-password-page .field input {
|
||||
border-radius: var(--env-var-radius-2);
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { forwardRef, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { IconButton, InputAdornment, Stack, TextField, Typography } from "@mui/material";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {string} [props.type] - Type of input field (e.g., 'text', 'password').
|
||||
* @param {string} props.id - ID of the input field.
|
||||
* @param {string} props.name - Name of the input field.
|
||||
* @param {string} [props.label] - Label for the input field.
|
||||
* @param {boolean} [props.https] - Indicates if it should display http or https.
|
||||
* @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk.
|
||||
* @param {boolean} [props.isOptional] - Indicates if the field is optional, will display optional text.
|
||||
* @param {string} [props.optionalLabel] - Optional label for the input field.
|
||||
* @param {string} [props.autoComplete] - Autocomplete value for the input field.
|
||||
* @param {string} [props.placeholder] - Placeholder text for the input field.
|
||||
* @param {string} props.value - Value of the input field.
|
||||
* @param {function} props.onChange - Function called on input change.
|
||||
* @param {string} [props.error] - Error message to display for the input field.
|
||||
* @param {boolean} [props.disabled] - Indicates if the input field is disabled.
|
||||
* @param {boolean} [props.hidden] - Indicates if the input field is hidden.
|
||||
* @param {React.Ref} [ref] - Ref forwarded to the underlying `TextField` component. Allows for direct interactions such as focusing.
|
||||
*/
|
||||
|
||||
const Field = forwardRef(
|
||||
(
|
||||
{
|
||||
type = "text",
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
https,
|
||||
isRequired,
|
||||
isOptional,
|
||||
optionalLabel,
|
||||
autoComplete,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
onInput,
|
||||
error,
|
||||
disabled,
|
||||
hidden,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
className={`field field-${type}`}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"&:not(:has(.Mui-disabled)):not(:has(.input-error)) .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
|
||||
{
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
"&:has(.input-error) .MuiOutlinedInput-root fieldset": {
|
||||
borderColor: theme.palette.error.text,
|
||||
},
|
||||
display: hidden ? "none" : "",
|
||||
}}
|
||||
>
|
||||
{label && (
|
||||
<Typography
|
||||
component="h3"
|
||||
color={theme.palette.text.secondary}
|
||||
fontWeight={500}
|
||||
>
|
||||
{label}
|
||||
{isRequired ? (
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.spacing(1)}
|
||||
color={theme.palette.error.text}
|
||||
>
|
||||
*
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{isOptional ? (
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight={400}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{optionalLabel || "(optional)"}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<TextField
|
||||
type={type === "password" ? (isVisible ? "text" : type) : type}
|
||||
name={name}
|
||||
id={id}
|
||||
autoComplete={autoComplete}
|
||||
placeholder={placeholder}
|
||||
multiline={type === "description"}
|
||||
rows={type === "description" ? 4 : 1}
|
||||
value={value}
|
||||
onInput={onInput}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
inputRef={ref}
|
||||
inputProps={{
|
||||
sx: {
|
||||
color: theme.palette.text.secondary,
|
||||
"&:-webkit-autofill": {
|
||||
WebkitBoxShadow: `0 0 0 100px ${theme.palette.other.autofill} inset`,
|
||||
WebkitTextFillColor: theme.palette.text.secondary,
|
||||
borderRadius: "0 5px 5px 0",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={
|
||||
type === "url"
|
||||
? {
|
||||
"& .MuiInputBase-root": { padding: 0 },
|
||||
"& .MuiStack-root": {
|
||||
borderTopLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: type === "url" && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="100%"
|
||||
sx={{
|
||||
borderRight: `solid 1px ${theme.palette.border.dark}`,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
pl: theme.spacing(6),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="h5"
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{ lineHeight: 1 }}
|
||||
>
|
||||
{https ? "https" : "http"}://
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
endAdornment: type === "password" && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setVisible((show) => !show)}
|
||||
tabIndex={-1}
|
||||
sx={{
|
||||
color: theme.palette.border.dark,
|
||||
padding: theme.spacing(1),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!isVisible ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Field.displayName = "Field";
|
||||
|
||||
Field.propTypes = {
|
||||
type: PropTypes.oneOf(["text", "password", "url", "email", "description", "number"]),
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
https: PropTypes.bool,
|
||||
isRequired: PropTypes.bool,
|
||||
isOptional: PropTypes.bool,
|
||||
optionalLabel: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onInput: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Field;
|
@ -41,7 +41,7 @@ const Radio = (props) => {
|
||||
color: "transparent",
|
||||
width: 16,
|
||||
height: 16,
|
||||
boxShadow: "inset 0 0 0 1px #656a74",
|
||||
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
|
||||
mt: theme.spacing(0.5),
|
||||
}}
|
||||
/>
|
||||
|
@ -99,7 +99,7 @@ const Search = ({
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
color={theme.palette.error.contrastText}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
@ -161,7 +161,7 @@ const Search = ({
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
height: 34,
|
||||
/* height: 34,*/
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
|
||||
...sx,
|
||||
}}
|
||||
|
65
Client/src/Components/Inputs/TextInput/Adornments/index.jsx
Normal file
65
Client/src/Components/Inputs/TextInput/Adornments/index.jsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
export const HttpAdornment = ({ https }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="100%"
|
||||
sx={{
|
||||
borderRight: `solid 1px ${theme.palette.border.dark}`,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
pl: theme.spacing(6),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="h5"
|
||||
paddingRight={"var(--env-var-spacing-1-minus)"}
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{ lineHeight: 1, opacity: 0.8 }}
|
||||
>
|
||||
{https ? "https" : "http"}://
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
HttpAdornment.propTypes = {
|
||||
https: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setFieldType(fieldType === "password" ? "text" : "password")}
|
||||
sx={{
|
||||
color: theme.palette.border.dark,
|
||||
padding: theme.spacing(1),
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{fieldType === "password" ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
};
|
||||
|
||||
PasswordEndAdornment.propTypes = {
|
||||
fieldType: PropTypes.string,
|
||||
setFieldType: PropTypes.func,
|
||||
};
|
166
Client/src/Components/Inputs/TextInput/index.jsx
Normal file
166
Client/src/Components/Inputs/TextInput/index.jsx
Normal file
@ -0,0 +1,166 @@
|
||||
import { Stack, TextField, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { forwardRef, useState, cloneElement } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const getSx = (theme, type, maxWidth) => {
|
||||
const sx = {
|
||||
maxWidth: maxWidth,
|
||||
"& .MuiFormHelperText-root": {
|
||||
position: "absolute",
|
||||
bottom: `-${theme.spacing(24)}`,
|
||||
minHeight: theme.spacing(24),
|
||||
},
|
||||
};
|
||||
|
||||
if (type === "url") {
|
||||
return {
|
||||
...sx,
|
||||
"& .MuiInputBase-root": { padding: 0 },
|
||||
"& .MuiStack-root": {
|
||||
borderTopLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
},
|
||||
};
|
||||
}
|
||||
return sx;
|
||||
};
|
||||
|
||||
const Required = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.spacing(1)}
|
||||
color={theme.palette.error.main}
|
||||
>
|
||||
*
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const Optional = ({ optionalLabel }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight={400}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{optionalLabel || "(optional)"}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
Optional.propTypes = {
|
||||
optionalLabel: PropTypes.string,
|
||||
};
|
||||
|
||||
const TextInput = forwardRef(
|
||||
(
|
||||
{
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
placeholder,
|
||||
isRequired,
|
||||
isOptional,
|
||||
optionalLabel,
|
||||
onChange,
|
||||
onBlur,
|
||||
error = false,
|
||||
helperText = null,
|
||||
startAdornment = null,
|
||||
endAdornment = null,
|
||||
label = null,
|
||||
maxWidth = "100%",
|
||||
flex,
|
||||
marginTop,
|
||||
marginRight,
|
||||
marginBottom,
|
||||
marginLeft,
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [fieldType, setFieldType] = useState(type);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
flex={flex}
|
||||
display={hidden ? "none" : ""}
|
||||
marginTop={marginTop}
|
||||
marginRight={marginRight}
|
||||
marginBottom={marginBottom}
|
||||
marginLeft={marginLeft}
|
||||
>
|
||||
<Typography
|
||||
component="h3"
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.text.secondary}
|
||||
fontWeight={500}
|
||||
>
|
||||
{label}
|
||||
{isRequired && <Required />}
|
||||
{isOptional && <Optional optionalLabel={optionalLabel} />}
|
||||
</Typography>
|
||||
<TextField
|
||||
id={id}
|
||||
name={name}
|
||||
type={fieldType}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
error={error}
|
||||
helperText={helperText}
|
||||
inputRef={ref}
|
||||
sx={getSx(theme, type, maxWidth)}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: startAdornment,
|
||||
endAdornment: endAdornment
|
||||
? cloneElement(endAdornment, { fieldType, setFieldType })
|
||||
: null,
|
||||
},
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TextInput.displayName = "TextInput";
|
||||
|
||||
TextInput.propTypes = {
|
||||
type: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
isRequired: PropTypes.bool,
|
||||
isOptional: PropTypes.bool,
|
||||
optionalLabel: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
error: PropTypes.bool,
|
||||
helperText: PropTypes.string,
|
||||
startAdornment: PropTypes.node,
|
||||
endAdornment: PropTypes.node,
|
||||
label: PropTypes.string,
|
||||
maxWidth: PropTypes.string,
|
||||
flex: PropTypes.number,
|
||||
marginTop: PropTypes.string,
|
||||
marginRight: PropTypes.string,
|
||||
marginBottom: PropTypes.string,
|
||||
marginLeft: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TextInput;
|
@ -1,5 +1,4 @@
|
||||
.label {
|
||||
border: 1px solid #000;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -31,7 +31,7 @@ const BaseLabel = ({ label, styles, children }) => {
|
||||
className="label"
|
||||
sx={{
|
||||
borderRadius: borderRadius,
|
||||
borderColor: theme.palette.text.tertiary,
|
||||
border: `1px solid ${theme.palette.text.tertiary}`,
|
||||
color: theme.palette.text.tertiary,
|
||||
padding: padding,
|
||||
...styles,
|
||||
@ -127,22 +127,22 @@ const StatusLabel = ({ status, text, customStyles }) => {
|
||||
const colors = {
|
||||
up: {
|
||||
dotColor: theme.palette.success.main,
|
||||
bgColor: theme.palette.success.bg,
|
||||
borderColor: theme.palette.success.light,
|
||||
bgColor: theme.palette.success.contrastText /* dark */,
|
||||
borderColor: theme.palette.success.main /* light */,
|
||||
},
|
||||
down: {
|
||||
dotColor: theme.palette.error.text,
|
||||
bgColor: theme.palette.error.bg,
|
||||
dotColor: theme.palette.error.main,
|
||||
bgColor: theme.palette.error.dark,
|
||||
borderColor: theme.palette.error.light,
|
||||
},
|
||||
paused: {
|
||||
dotColor: theme.palette.warning.main,
|
||||
bgColor: theme.palette.warning.bg,
|
||||
bgColor: theme.palette.warning.dark,
|
||||
borderColor: theme.palette.warning.light,
|
||||
},
|
||||
pending: {
|
||||
dotColor: theme.palette.warning.main,
|
||||
bgColor: theme.palette.warning.bg,
|
||||
bgColor: theme.palette.warning.dark,
|
||||
borderColor: theme.palette.warning.light,
|
||||
},
|
||||
"cannot resolve": {
|
||||
@ -182,4 +182,4 @@ StatusLabel.propTypes = {
|
||||
customStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
export { ColoredLabel, StatusLabel };
|
||||
export { BaseLabel, ColoredLabel, StatusLabel };
|
||||
|
@ -6,6 +6,13 @@
|
||||
padding: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
|
||||
/* @media (max-width: 1000px) {
|
||||
.home-layout {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
} */
|
||||
|
||||
.home-layout aside {
|
||||
position: sticky;
|
||||
top: var(--env-var-spacing-2);
|
@ -1,4 +1,4 @@
|
||||
import Sidebar from "../../Components/Sidebar";
|
||||
import Sidebar from "../../Sidebar";
|
||||
import { Outlet } from "react-router";
|
||||
import { Stack } from "@mui/material";
|
||||
|
@ -34,11 +34,11 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
"&:has(.input-error)": {
|
||||
borderColor: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error.bg,
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
py: theme.spacing(4),
|
||||
px: theme.spacing(8),
|
||||
"& > .MuiStack-root > svg": {
|
||||
fill: theme.palette.error.text,
|
||||
fill: theme.palette.error.contrastText,
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
},
|
||||
@ -85,7 +85,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
color={theme.palette.error.contrastText}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
|
@ -19,7 +19,7 @@ import { useLocation, useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { setMode, toggleSidebar } from "../../Features/UI/uiSlice";
|
||||
import { toggleSidebar } from "../../Features/UI/uiSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Avatar from "../Avatar";
|
||||
import LockSvg from "../../assets/icons/lock.svg?react";
|
||||
@ -29,7 +29,6 @@ import LogoutSvg from "../../assets/icons/logout.svg?react";
|
||||
import Support from "../../assets/icons/support.svg?react";
|
||||
import Dashboard from "../../assets/icons/dashboard.svg?react";
|
||||
import Account from "../../assets/icons/user-edit.svg?react";
|
||||
import StatusPages from "../../assets/icons/status-pages.svg?react";
|
||||
import Maintenance from "../../assets/icons/maintenance.svg?react";
|
||||
import Monitors from "../../assets/icons/monitors.svg?react";
|
||||
import Incidents from "../../assets/icons/incidents.svg?react";
|
||||
@ -48,14 +47,9 @@ import Folder from "../../assets/icons/folder.svg?react";
|
||||
import "./index.css";
|
||||
|
||||
const menu = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
icon: <Dashboard />,
|
||||
nested: [
|
||||
{ name: "Monitors", path: "monitors", icon: <Monitors /> },
|
||||
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
|
||||
],
|
||||
},
|
||||
{ name: "Monitors", path: "monitors", icon: <Monitors /> },
|
||||
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
|
||||
{ name: "Infrastructure", path: "infrastructure", icon: <Integrations /> },
|
||||
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
|
||||
// { name: "Status pages", path: "status", icon: <StatusPages /> },
|
||||
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
|
||||
@ -83,13 +77,14 @@ const menu = [
|
||||
|
||||
const URL_MAP = {
|
||||
support: "https://github.com/bluewave-labs/bluewave-uptime/issues",
|
||||
docs: "https://bluewavelabs.gitbook.io/uptime-manager",
|
||||
docs: "https://bluewavelabs.gitbook.io/checkmate",
|
||||
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
|
||||
};
|
||||
|
||||
const PATH_MAP = {
|
||||
monitors: "Dashboard",
|
||||
pagespeed: "Dashboard",
|
||||
infrastructure: "Dashboard",
|
||||
account: "Account",
|
||||
settings: "Other",
|
||||
};
|
||||
@ -113,7 +108,6 @@ function Sidebar() {
|
||||
const [popup, setPopup] = useState();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
// Remove demo password if demo
|
||||
const accountMenuItem = menu.find((item) => item.name === "Account");
|
||||
if (user.role?.includes("demo") && accountMenuItem) {
|
||||
accountMenuItem.nested = accountMenuItem.nested.filter((item) => {
|
||||
@ -148,7 +142,9 @@ function Sidebar() {
|
||||
if (matchedKey) {
|
||||
setOpen((prev) => ({ ...prev, [PATH_MAP[matchedKey]]: true }));
|
||||
}
|
||||
}, []);
|
||||
}, [location]);
|
||||
|
||||
/* TODO refactor this, there are a some ternaries and comments in the return */
|
||||
|
||||
return (
|
||||
<Stack
|
||||
@ -181,7 +177,13 @@ function Sidebar() {
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={() => window.open("https://github.com/bluewave-labs/bluewave-uptime", "_blank", "noreferrer")}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://github.com/bluewave-labs/bluewave-uptime",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
)
|
||||
}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Stack
|
||||
@ -199,14 +201,14 @@ function Sidebar() {
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
BU
|
||||
C
|
||||
</Stack>
|
||||
<Typography
|
||||
component="span"
|
||||
mt={theme.spacing(2)}
|
||||
sx={{ opacity: 0.8, fontWeight: 500 }}
|
||||
>
|
||||
BlueWave Uptime
|
||||
Checkmate
|
||||
</Typography>
|
||||
</Stack>
|
||||
<IconButton
|
||||
@ -297,6 +299,7 @@ function Sidebar() {
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
) : collapsed ? (
|
||||
/* TODO Do we ever get here? */
|
||||
<React.Fragment key={item.name}>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
@ -616,22 +619,6 @@ function Sidebar() {
|
||||
</MenuItem>
|
||||
)}
|
||||
{collapsed && <Divider />}
|
||||
{/* <MenuItem
|
||||
onClick={() => {
|
||||
dispatch(setMode("light"));
|
||||
closePopup();
|
||||
}}
|
||||
>
|
||||
Light
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
dispatch(setMode("dark"));
|
||||
closePopup();
|
||||
}}
|
||||
>
|
||||
Dark
|
||||
</MenuItem> */}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={logout}
|
||||
|
@ -3,12 +3,20 @@ import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import Field from "../../Inputs/Field";
|
||||
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import Alert from "../../Alert";
|
||||
import { update } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { getTouchedFieldErrors } from "../../../Validation/error";
|
||||
|
||||
const defaultPasswordsState = {
|
||||
password: "",
|
||||
newPassword: "",
|
||||
confirm: "",
|
||||
};
|
||||
|
||||
/**
|
||||
* PasswordPanel component manages the form for editing password.
|
||||
@ -29,36 +37,40 @@ const PasswordPanel = () => {
|
||||
"edit-confirm-password": "confirm",
|
||||
};
|
||||
|
||||
const [localData, setLocalData] = useState({
|
||||
password: "",
|
||||
newPassword: "",
|
||||
confirm: "",
|
||||
const [localData, setLocalData] = useState(defaultPasswordsState);
|
||||
const [errors, setErrors] = useState(defaultPasswordsState);
|
||||
const [touchedFields, setTouchedFields] = useState({
|
||||
password: false,
|
||||
newPassword: false,
|
||||
confirm: false,
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
|
||||
const updatedData = {
|
||||
...localData,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
const updatedTouchedFields = {
|
||||
...touchedFields,
|
||||
[name]: true,
|
||||
};
|
||||
|
||||
const validation = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { password: localData.newPassword } }
|
||||
{ ...updatedData },
|
||||
{ abortEarly: false, context: { password: updatedData.newPassword } }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
const updatedErrors = getTouchedFieldErrors(validation, updatedTouchedFields);
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[name];
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
if (!touchedFields[name]) {
|
||||
setTouchedFields(updatedTouchedFields);
|
||||
}
|
||||
|
||||
setLocalData(updatedData);
|
||||
setErrors(updatedErrors);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
@ -110,67 +122,105 @@ const PasswordPanel = () => {
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(20)}
|
||||
gap={theme.spacing(26)}
|
||||
maxWidth={"80ch"}
|
||||
marginInline={"auto"}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Current password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="text"
|
||||
id="hidden-username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
hidden={true}
|
||||
value=""
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="hidden-username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
hidden={true}
|
||||
value=""
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent={"flex-start"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
width="20ch"
|
||||
>
|
||||
Current password
|
||||
</Typography>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="edit-current-password"
|
||||
placeholder="Enter your current password"
|
||||
autoComplete="current-password"
|
||||
value={localData.password}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-current-password"]]}
|
||||
error={errors[idToName["edit-current-password"]] ? true : false}
|
||||
helperText={errors[idToName["edit-current-password"]]}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
flex={1}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">New password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
width="20ch"
|
||||
>
|
||||
New password
|
||||
</Typography>
|
||||
|
||||
<TextInput
|
||||
type="password"
|
||||
id="edit-new-password"
|
||||
placeholder="Enter your new password"
|
||||
autoComplete="new-password"
|
||||
value={localData.newPassword}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-new-password"]]}
|
||||
error={errors[idToName["edit-new-password"]] ? true : false}
|
||||
helperText={errors[idToName["edit-new-password"]]}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
flex={1}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Confirm new password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
width="20ch"
|
||||
>
|
||||
Confirm new password
|
||||
</Typography>
|
||||
|
||||
<TextInput
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
placeholder="Reenter your new password"
|
||||
autoComplete="new-password"
|
||||
value={localData.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-confirm-password"]]}
|
||||
error={errors[idToName["edit-confirm-password"]] ? true : false}
|
||||
helperText={errors[idToName["edit-confirm-password"]]}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
flex={1}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}></Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<Box sx={{ maxWidth: "70ch" }}>
|
||||
<Alert
|
||||
variant="warning"
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol."
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character."
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
@ -178,10 +228,13 @@ const PasswordPanel = () => {
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
loadingIndicator="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
disabled={
|
||||
Object.keys(errors).length > 0 ||
|
||||
Object.values(localData).filter((value) => value === "").length > 0
|
||||
}
|
||||
sx={{
|
||||
px: theme.spacing(12),
|
||||
mt: theme.spacing(20),
|
||||
|
@ -3,7 +3,7 @@ import { useRef, useState } from "react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
|
||||
import Avatar from "../../Avatar";
|
||||
import Field from "../../Inputs/Field";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import ImageField from "../../Inputs/Image";
|
||||
import { credentials, imageValidation } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@ -229,26 +229,30 @@ const ProfilePanel = () => {
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">First name</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
<TextInput
|
||||
id="edit-first-name"
|
||||
value={localData.firstName}
|
||||
placeholder="Enter your first name"
|
||||
autoComplete="given-name"
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-first-name"]]}
|
||||
error={errors[idToName["edit-first-name"]] ? true : false}
|
||||
helperText={errors[idToName["edit-first-name"]]}
|
||||
flex={1}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Last name</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
<TextInput
|
||||
id="edit-last-name"
|
||||
placeholder="Enter your last name"
|
||||
autoComplete="family-name"
|
||||
value={localData.lastName}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-last-name"]]}
|
||||
error={errors[idToName["edit-last-name"]] ? true : false}
|
||||
helperText={errors[idToName["edit-last-name"]]}
|
||||
flex={1}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
@ -261,14 +265,14 @@ const ProfilePanel = () => {
|
||||
This is your current email address — it cannot be changed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Field
|
||||
<TextInput
|
||||
id="edit-email"
|
||||
value={user.email}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
onChange={() => logger.warn("disabled")}
|
||||
// error={errors[idToName["edit-email"]]}
|
||||
disabled={true}
|
||||
flex={1}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
@ -370,8 +374,6 @@ const ProfilePanel = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
{/* TODO - Update ModalPopup Component with @mui for reusability */}
|
||||
|
||||
<Dialog
|
||||
open={isModalOpen("delete")}
|
||||
theme={theme}
|
||||
|
@ -2,7 +2,7 @@ import { useTheme } from "@emotion/react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import Field from "../../Inputs/Field";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
@ -301,7 +301,7 @@ const TeamPanel = () => {
|
||||
filled={(filter === "admin").toString()}
|
||||
onClick={() => setFilter("admin")}
|
||||
>
|
||||
Administrator
|
||||
Super admin
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
@ -338,13 +338,15 @@ const TeamPanel = () => {
|
||||
onClose={closeInviteModal}
|
||||
theme={theme}
|
||||
>
|
||||
<Field
|
||||
<TextInput
|
||||
marginBottom={theme.spacing(12)}
|
||||
type="email"
|
||||
id="input-team-member"
|
||||
placeholder="Email"
|
||||
value={toInvite.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
/>
|
||||
<Select
|
||||
id="team-member-role"
|
||||
|
@ -0,0 +1,411 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/createMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const res = await networkService.createMonitor({
|
||||
authToken: authToken,
|
||||
monitor: monitor,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const checkInfrastructureEndpointResolution = createAsyncThunk(
|
||||
"infrastructureMonitors/CheckEndpoint",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorURL } = data;
|
||||
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
authToken: authToken,
|
||||
monitorURL: monitorURL,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getInfrastructureMonitorById = createAsyncThunk(
|
||||
"infrastructureMonitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorId } = data;
|
||||
const res = await networkService.getMonitorById({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getInfrastructureMonitorsByTeamId = createAsyncThunk(
|
||||
"infrastructureMonitors/getMonitorsByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
authToken: token,
|
||||
teamId: user.teamId,
|
||||
types: ["hardware"],
|
||||
limit: 1,
|
||||
rowsPerPage: 0,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/updateMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
threshold: monitor.threshold,
|
||||
};
|
||||
const res = await networkService.updateMonitor({
|
||||
authToken: authToken,
|
||||
monitorId: monitor._id,
|
||||
updatedFields: updatedFields,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const res = await networkService.deleteMonitorById({
|
||||
authToken: authToken,
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const pauseInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/pauseMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorId } = data;
|
||||
const res = await networkService.pauseMonitorById({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteInfrastructureMonitorChecksByTeamId = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteChecksByTeamId",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, teamId } = data;
|
||||
const res = await networkService.deleteChecksByTeamId({
|
||||
authToken: authToken,
|
||||
teamId: teamId,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAllInfrastructureMonitors = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteAllMonitors",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken } = data;
|
||||
const res = await networkService.deleteAllMonitors({
|
||||
authToken: authToken,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const infrastructureMonitorsSlice = createSlice({
|
||||
name: "infrastructureMonitors",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearInfrastructureMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getInfrastructureMonitorsByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorsByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorsByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting infrastructure monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkInfrastructureEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkInfrastructureEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkInfrastructureEndpointResolution.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to check endpoint resolution";
|
||||
})
|
||||
// *****************************************************
|
||||
// Get Monitor By Id
|
||||
// *****************************************************
|
||||
.addCase(getInfrastructureMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to get infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updateInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updateInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update infrastructure monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deleteInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete Monitor checks by Team ID
|
||||
// *****************************************************
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete monitor checks";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pauseInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pauseInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pauseInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete all Monitors
|
||||
// *****************************************************
|
||||
.addCase(deleteAllInfrastructureMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteAllInfrastructureMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteAllInfrastructureMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setInfrastructureMonitors, clearInfrastructureMonitorState } =
|
||||
infrastructureMonitorsSlice.actions;
|
||||
|
||||
export default infrastructureMonitorsSlice.reducer;
|
@ -40,7 +40,7 @@ export const checkEndpointResolution = createAsyncThunk(
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
authToken: authToken,
|
||||
monitorURL: monitorURL,
|
||||
})
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@ -53,7 +53,7 @@ export const checkEndpointResolution = createAsyncThunk(
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const getUptimeMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
@ -86,7 +86,7 @@ export const getUptimeMonitorsByTeamId = createAsyncThunk(
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
authToken: token,
|
||||
teamId: user.teamId,
|
||||
types: ["http", "ping"],
|
||||
types: ["http", "ping", "docker"],
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
@ -221,6 +221,7 @@ export const addDemoMonitors = createAsyncThunk(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAllMonitors = createAsyncThunk(
|
||||
"monitors/deleteAllMonitors",
|
||||
async (data, thunkApi) => {
|
||||
|
11
Client/src/Hooks/useIsAdmin.js
Normal file
11
Client/src/Hooks/useIsAdmin.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const useIsAdmin = () => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const isAdmin =
|
||||
(user?.role?.includes("admin") ?? false) ||
|
||||
(user?.role?.includes("superadmin") ?? false);
|
||||
return isAdmin;
|
||||
};
|
||||
|
||||
export { useIsAdmin };
|
@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useSelector } from "react-redux";
|
||||
@ -14,10 +15,10 @@ import "./index.css";
|
||||
* @param {string} [props.open] - Specifies the initially open tab: 'profile', 'password', or 'team'.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
const Account = ({ open = "profile" }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const [focusedTab, setFocusedTab] = useState(null); // Track focused tab
|
||||
const tab = open;
|
||||
const handleTabChange = (event, newTab) => {
|
||||
navigate(`/account/${newTab}`);
|
||||
@ -36,6 +37,21 @@ const Account = ({ open = "profile" }) => {
|
||||
tabList = ["Profile"];
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
const currentIndex = tabList.findIndex((label) => label.toLowerCase() === tab);
|
||||
if (event.key === "Tab") {
|
||||
const nextIndex = (currentIndex + 1) % tabList.length;
|
||||
setFocusedTab(tabList[nextIndex].toLowerCase());
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
navigate(`/account/${focusedTab}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (tabName) => {
|
||||
setFocusedTab(tabName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="account"
|
||||
@ -63,6 +79,9 @@ const Account = ({ open = "profile" }) => {
|
||||
label={label}
|
||||
key={index}
|
||||
value={label.toLowerCase()}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => handleFocus(label.toLowerCase())}
|
||||
tabIndex={index}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.tertiary,
|
||||
@ -74,7 +93,11 @@ const Account = ({ open = "profile" }) => {
|
||||
fontWeight: 400,
|
||||
marginRight: theme.spacing(8),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
borderBottom: `2px solid ${theme.palette.border.light}`,
|
||||
},
|
||||
|
||||
"&:hover": {
|
||||
borderBottom: `2px solid ${theme.palette.border.light}`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Link from "../../Components/Link";
|
||||
import "./index.css";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@ -160,13 +160,14 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
<TextInput
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.apiBaseUrl}
|
||||
error={errors.apiBaseUrl ? true : false}
|
||||
helperText={errors.apiBaseUrl}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
@ -189,7 +190,7 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="System email host"
|
||||
@ -197,9 +198,10 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailHost}
|
||||
error={errors.systemEmailHost ? true : false}
|
||||
helperText={errors.systemEmailHost}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email port"
|
||||
@ -207,18 +209,21 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
value={localSettings.systemEmailPort?.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailPort}
|
||||
error={errors.systemEmailPort ? true : false}
|
||||
helperText={errors.systemEmailPort}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailAddress}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailAddress ? true : false}
|
||||
helperText={errors.systemEmailAddress}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
@ -226,7 +231,8 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailPassword}
|
||||
error={errors.systemEmailPassword ? true : false}
|
||||
helperText={errors.systemEmailPassword}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@ -242,7 +248,7 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
direction="row"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Field
|
||||
<TextInput
|
||||
type="number"
|
||||
id="jwtTTLNum"
|
||||
label="JWT time to live"
|
||||
@ -250,7 +256,8 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
value={localSettings.jwtTTLNum.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.jwtTTLNum}
|
||||
error={errors.jwtTTLNum ? true : false}
|
||||
helperText={errors.jwtTTLNum}
|
||||
/>
|
||||
<Select
|
||||
id="jwtTTLUnits"
|
||||
@ -265,7 +272,7 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
error={errors.jwtTTLUnits}
|
||||
/>
|
||||
</Stack>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
@ -273,9 +280,10 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.dbType}
|
||||
error={errors.dbType ? true : false}
|
||||
helperText={errors.dbType}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
@ -283,9 +291,10 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.redisHost}
|
||||
error={errors.redisHost ? true : false}
|
||||
helperText={errors.redisHost}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
@ -293,9 +302,10 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
value={localSettings.redisPort?.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.redisPort}
|
||||
error={errors.redisPort ? true : false}
|
||||
helperText={errors.redisPort}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
@ -303,7 +313,8 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.pagespeedApiKey}
|
||||
error={errors.pagespeedApiKey ? true : false}
|
||||
helperText={errors.pagespeedApiKey}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
|
@ -7,7 +7,7 @@ import { useEffect, useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { IconBox } from "./styled";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
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";
|
||||
@ -160,7 +160,7 @@ const ForgotPassword = () => {
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
<TextInput
|
||||
type="email"
|
||||
id="forgot-password-email-input"
|
||||
label="Email"
|
||||
@ -168,7 +168,8 @@ const ForgotPassword = () => {
|
||||
placeholder="Enter your email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
/>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
|
@ -4,10 +4,12 @@ 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 Field from "../../Components/Inputs/Field";
|
||||
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";
|
||||
@ -51,6 +53,10 @@ const LandingPage = ({ onContinue }) => {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Mail />
|
||||
@ -130,7 +136,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
@ -139,57 +145,67 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="left"
|
||||
mb={theme.spacing(5)}
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
<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"
|
||||
>
|
||||
<Field
|
||||
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}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
<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
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
@ -218,6 +234,7 @@ 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) {
|
||||
@ -235,7 +252,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
position="relative"
|
||||
textAlign="center"
|
||||
>
|
||||
@ -244,64 +261,79 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
<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) },
|
||||
}}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
<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"
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
<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>
|
||||
<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),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.password && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "104%",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
transform: `translate(-50%, 150%)`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
@ -523,23 +555,6 @@ const Login = () => {
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">Don't have an account? —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Sign Up
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { StepOne } from "./StepOne";
|
||||
import { StepTwo } from "./StepTwo";
|
||||
import { StepThree } from "./StepThree";
|
||||
import { networkService } from "../../../main";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { register } from "../../../Features/Auth/authSlice";
|
||||
import { useParams } from "react-router-dom";
|
||||
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 Check from "../../../Components/Check/Check";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import { networkService } from "../../../main";
|
||||
import "../index.css";
|
||||
|
||||
/**
|
||||
@ -54,6 +53,10 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Mail />
|
||||
@ -110,389 +113,6 @@ LandingPage.propTypes = {
|
||||
onSignup: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the first step of the sign up 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(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your personal details</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
mb={theme.spacing(10)}
|
||||
>
|
||||
<Field
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
isRequired={true}
|
||||
placeholder="Jordan"
|
||||
autoComplete="given-name"
|
||||
value={form.firstName}
|
||||
onChange={onChange}
|
||||
error={errors.firstName}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
mb={theme.spacing(5)}
|
||||
>
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
value={form.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<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),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={(errors.firstName || errors.lastName) && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StepOne.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the second step of the sign up 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 StepTwo = ({ 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(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
mb={theme.spacing(5)}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
id="register-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}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<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),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StepTwo.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the third step of the sign up 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 StepThree = ({ 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(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Create your password</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="left"
|
||||
sx={{
|
||||
"& .input-error": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="Create a password"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must be at least</Typography> 8 characters
|
||||
long
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: form.password.length < 8
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain</Typography> one special
|
||||
character and a number
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain at least</Typography> one
|
||||
upper and lower character
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
<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),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StepThree.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
const Register = ({ isSuperAdmin }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
@ -596,14 +216,16 @@ const Register = ({ isSuperAdmin }) => {
|
||||
// Attempts account registration
|
||||
const handleStepThree = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { password, confirm } = e.target.elements;
|
||||
let registerForm = {
|
||||
...form,
|
||||
password: password.value,
|
||||
confirm: confirm.value,
|
||||
role: isSuperAdmin ? ["superadmin"] : form.role,
|
||||
inviteToken: token ? token : "", // Add the token to the request for verification
|
||||
};
|
||||
let error = validateForm(registerForm, {
|
||||
context: { password: form.password },
|
||||
context: { password: registerForm.password },
|
||||
});
|
||||
if (error) {
|
||||
handleError(error);
|
||||
@ -615,18 +237,16 @@ const Register = ({ isSuperAdmin }) => {
|
||||
if (action.payload.success) {
|
||||
const authToken = action.payload.data;
|
||||
localStorage.setItem("token", authToken);
|
||||
navigate("/");
|
||||
navigate("/monitors");
|
||||
createToast({
|
||||
body: "Welcome! Your account was created successfully.",
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
@ -732,10 +352,10 @@ const Register = ({ isSuperAdmin }) => {
|
||||
/>
|
||||
) : step === 3 ? (
|
||||
<StepThree
|
||||
form={form}
|
||||
errors={errors}
|
||||
/* form={form}
|
||||
errors={errors} */
|
||||
onSubmit={handleStepThree}
|
||||
onChange={handleChange}
|
||||
/* onChange={handleChange} */
|
||||
onBack={() => setStep(2)}
|
||||
/>
|
||||
) : (
|
||||
|
135
Client/src/Pages/Auth/Register/StepOne/index.jsx
Normal file
135
Client/src/Pages/Auth/Register/StepOne/index.jsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import TextInput from "../../../../Components/Inputs/TextInput";
|
||||
|
||||
StepOne.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the first step of the sign up 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}
|
||||
*/
|
||||
|
||||
function StepOne({ form, errors, onSubmit, onChange, onBack }) {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO this stack should be a component */}
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your personal details</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
textAlign="left"
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
>
|
||||
<Box
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
>
|
||||
<TextInput
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
isRequired={true}
|
||||
placeholder="Jordan"
|
||||
autoComplete="given-name"
|
||||
value={form.firstName}
|
||||
onChange={onChange}
|
||||
error={errors.firstName ? true : false}
|
||||
helperText={errors.firstName}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<TextInput
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
value={form.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName ? true : false}
|
||||
helperText={errors.lastName}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{/* TODO buttons should be a component should be a component */}
|
||||
<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.firstName || errors.lastName) && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { StepOne };
|
173
Client/src/Pages/Auth/Register/StepThree/index.jsx
Normal file
173
Client/src/Pages/Auth/Register/StepThree/index.jsx
Normal file
@ -0,0 +1,173 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import TextInput from "../../../../Components/Inputs/TextInput";
|
||||
import Check from "../../../../Components/Check/Check";
|
||||
import { useValidatePassword } from "../../hooks/useValidatePassword";
|
||||
|
||||
StepThree.propTypes = {
|
||||
onSubmit: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the third step of the sign up process.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onSubmit - Callback function to handle form submission.
|
||||
* @param {Function} props.onBack - Callback function to handle "Back" button click.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function StepThree({ onSubmit, onBack }) {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { handleChange, feedbacks, form, errors } = useValidatePassword();
|
||||
console.log(errors);
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Create your password</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
textAlign="left"
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
sx={{
|
||||
"& .input-error": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="Create a password"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password && errors.password[0] ? true : false}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
name="confirm"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm && errors.confirm[0] ? true : false}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
noHighlightText={"Must be at least"}
|
||||
text={"8 characters long"}
|
||||
variant={feedbacks.length}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one special character"}
|
||||
variant={feedbacks.special}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one number"}
|
||||
variant={feedbacks.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one upper character"}
|
||||
variant={feedbacks.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one lower character"}
|
||||
variant={feedbacks.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Confirm password and password"}
|
||||
text={"must match"}
|
||||
variant={feedbacks.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
<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.dark}`,
|
||||
outlineOffset: "4px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={
|
||||
form.password.length === 0 ||
|
||||
form.confirm.length === 0 ||
|
||||
Object.keys(errors).length !== 0
|
||||
}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { StepThree };
|
117
Client/src/Pages/Auth/Register/StepTwo/index.jsx
Normal file
117
Client/src/Pages/Auth/Register/StepTwo/index.jsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import TextInput from "../../../../Components/Inputs/TextInput";
|
||||
|
||||
StepTwo.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the second step of the sign up 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}
|
||||
*/
|
||||
function StepTwo({ 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(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
textAlign="left"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
mb={theme.spacing(5)}
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
>
|
||||
<TextInput
|
||||
type="email"
|
||||
id="register-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"
|
||||
onClick={onSubmit}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { StepTwo };
|
@ -1,56 +1,45 @@
|
||||
import { useId } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { setNewPassword } from "../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Check from "../../Components/Check/Check";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import LockIcon from "../../assets/icons/lock.svg?react";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import "./index.css";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
|
||||
|
||||
import { IconBox } from "./styled";
|
||||
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";
|
||||
import "./index.css";
|
||||
import { useValidatePassword } from "./hooks/useValidatePassword";
|
||||
|
||||
const SetNewPassword = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
password: "",
|
||||
confirm: "",
|
||||
});
|
||||
const passwordId = useId();
|
||||
const confirmPasswordId = useId();
|
||||
|
||||
const idMap = {
|
||||
"register-password-input": "password",
|
||||
"confirm-password-input": "confirm",
|
||||
};
|
||||
const { form, errors, handleChange, feedbacks } = useValidatePassword();
|
||||
|
||||
const { isLoading } = useSelector((state) => state.auth);
|
||||
const { token } = useParams();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const passwordForm = { ...form };
|
||||
const { error } = credentials.validate(passwordForm, {
|
||||
const { error } = credentials.validate(form, {
|
||||
abortEarly: false,
|
||||
context: { password: form.password },
|
||||
});
|
||||
|
||||
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
|
||||
@ -58,50 +47,23 @@ const SetNewPassword = () => {
|
||||
: "Error validating data.",
|
||||
});
|
||||
} else {
|
||||
delete passwordForm.confirm;
|
||||
const action = await dispatch(setNewPassword({ token: token, form: passwordForm }));
|
||||
const action = await dispatch(setNewPassword({ token, form }));
|
||||
if (action.payload.success) {
|
||||
navigate("/new-password-confirmed");
|
||||
createToast({
|
||||
body: "Your password was reset successfully.",
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
const errorMessage = action.payload
|
||||
? action.payload.msg
|
||||
: "Unable to reset password. Please try again later or contact support.";
|
||||
createToast({
|
||||
body: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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, context: { password: form.password } }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="set-new-password-page auth"
|
||||
@ -187,15 +149,18 @@ const SetNewPassword = () => {
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
<TextInput
|
||||
id={passwordId}
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
error={errors.password ? true : false}
|
||||
helperText={errors.password}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
@ -204,15 +169,18 @@ const SetNewPassword = () => {
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
<TextInput
|
||||
id={confirmPasswordId}
|
||||
type="password"
|
||||
id="confirm-password-input"
|
||||
name="confirm"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm}
|
||||
error={errors.confirm ? true : false}
|
||||
helperText={errors.confirm}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
@ -220,55 +188,34 @@ const SetNewPassword = () => {
|
||||
mb={theme.spacing(12)}
|
||||
>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must be at least</Typography> 8
|
||||
characters long
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: form.password.length < 8
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
noHighlightText={"Must be at least"}
|
||||
text={"8 characters long"}
|
||||
variant={feedbacks.length}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain</Typography> one special
|
||||
character and a number
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one special character"}
|
||||
variant={feedbacks.special}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain at least</Typography> one
|
||||
upper and lower character
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one number"}
|
||||
variant={feedbacks.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one upper character"}
|
||||
variant={feedbacks.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Must contain at least"}
|
||||
text={"one lower character"}
|
||||
variant={feedbacks.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={"Confirm password and password"}
|
||||
text={"must match"}
|
||||
variant={feedbacks.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
@ -277,7 +224,11 @@ const SetNewPassword = () => {
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
onClick={handleSubmit}
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
disabled={
|
||||
form.password.length === 0 ||
|
||||
form.confirm.length === 0 ||
|
||||
Object.keys(errors).length !== 0
|
||||
}
|
||||
sx={{ width: "100%", maxWidth: 400 }}
|
||||
>
|
||||
Reset password
|
||||
|
70
Client/src/Pages/Auth/hooks/useValidatePassword.jsx
Normal file
70
Client/src/Pages/Auth/hooks/useValidatePassword.jsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
|
||||
const getFeedbackStatus = (form, errors, field, criteria) => {
|
||||
const fieldErrors = errors[field];
|
||||
const isFieldEmpty = form[field].length === 0;
|
||||
const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
|
||||
const isCorrect = !isFieldEmpty && !hasError;
|
||||
|
||||
if (isCorrect) {
|
||||
return "success";
|
||||
} else if (hasError) {
|
||||
return "error";
|
||||
} else {
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
|
||||
function useValidatePassword() {
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
password: "",
|
||||
confirm: "",
|
||||
});
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, name } = event.target;
|
||||
setForm((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
const validateValue = { [name]: value };
|
||||
const validateOptions = { abortEarly: false, context: { password: form.password } };
|
||||
if (name === "password" && form.confirm.length > 0) {
|
||||
validateValue.confirm = form.confirm;
|
||||
validateOptions.context = { password: value };
|
||||
} else if (name === "confirm") {
|
||||
validateValue.password = form.password;
|
||||
}
|
||||
|
||||
const { error } = credentials.validate(validateValue, validateOptions);
|
||||
const errors = error?.details.map((error) => ({
|
||||
path: error.path[0],
|
||||
type: error.type,
|
||||
}));
|
||||
const errorsByPath =
|
||||
errors &&
|
||||
errors.reduce((acc, { path, type }) => {
|
||||
if (!acc[path]) {
|
||||
acc[path] = [];
|
||||
}
|
||||
acc[path].push(type);
|
||||
return acc;
|
||||
}, {});
|
||||
setErrors(() => (errorsByPath ? { ...errorsByPath } : {}));
|
||||
};
|
||||
|
||||
const feedbacks = useMemo(
|
||||
() => ({
|
||||
length: getFeedbackStatus(form, errors, "password", "string.min"),
|
||||
special: getFeedbackStatus(form, errors, "password", "special"),
|
||||
number: getFeedbackStatus(form, errors, "password", "number"),
|
||||
uppercase: getFeedbackStatus(form, errors, "password", "uppercase"),
|
||||
lowercase: getFeedbackStatus(form, errors, "password", "lowercase"),
|
||||
confirm: getFeedbackStatus(form, errors, "confirm", "different"),
|
||||
}),
|
||||
[form, errors]
|
||||
);
|
||||
|
||||
return { handleChange, feedbacks, form, errors };
|
||||
}
|
||||
export { useValidatePassword };
|
@ -24,6 +24,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
|
||||
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
|
||||
import { HttpStatusLabel } from "../../../Components/HttpStatusLabel";
|
||||
import { Empty } from "./Empty/Empty";
|
||||
import { IncidentSkeleton } from "./Skeleton/Skeleton";
|
||||
|
||||
@ -39,7 +40,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
page: 0,
|
||||
rowsPerPage: 14,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
@ -184,7 +185,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
|
||||
@ -196,7 +197,9 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formattedDate}</TableCell>
|
||||
<TableCell>{check.statusCode ? check.statusCode : "N/A"}</TableCell>
|
||||
<TableCell>
|
||||
<HttpStatusLabel status={check.statusCode} />
|
||||
</TableCell>
|
||||
<TableCell>{check.message}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
@ -0,0 +1,92 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import TextInput from "../../../../Components/Inputs/TextInput";
|
||||
import Checkbox from "../../../../Components/Inputs/Checkbox";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* `CustomThreshold` is a functional React component that displays a
|
||||
* group of CheckBox with a label and its correspondant threshold input field.
|
||||
*
|
||||
* @param {{ checkboxId: any; checkboxLabel: any; onCheckboxChange: any; fieldId: any; onFieldChange: any; onFieldBlur: any; alertUnit: any; infrastructureMonitor: any; errors: any; }} param0
|
||||
* @param {string} param0.checkboxId - The text is the id of the checkbox.
|
||||
* @param {string} param0.checkboxLabel - The text to be displayed as the label next to the check icon.
|
||||
* @param {func} param0.onCheckboxChange - The function to invoke when checkbox is checked or unchecked.
|
||||
* @param {string} param0.fieldId - The text is the id of the input field.
|
||||
* @param {func} param0.onFieldChange - The function to invoke when input field is changed.
|
||||
* @param {func} param0.onFieldBlur - The function to invoke when input field is losing focus.
|
||||
* @param {string} param0.alertUnit the threshold unit such as usage percentage '%' etc
|
||||
* @param {object} param0.infrastructureMonitor the form object of the create infrastrcuture monitor page
|
||||
* @param {object} param0.errors the object that holds all the errors of the form page
|
||||
* @returns A compound React component that renders the custom threshold alert section
|
||||
*
|
||||
*/
|
||||
|
||||
export const CustomThreshold = ({
|
||||
checkboxId,
|
||||
checkboxLabel,
|
||||
onCheckboxChange,
|
||||
fieldId,
|
||||
onFieldChange,
|
||||
onFieldBlur,
|
||||
alertUnit,
|
||||
infrastructureMonitor,
|
||||
errors,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
width: "50%",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
label={checkboxLabel}
|
||||
isChecked={infrastructureMonitor[checkboxId]}
|
||||
onChange={onCheckboxChange}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
maxWidth="var(--env-var-width-4)"
|
||||
type="number"
|
||||
id={fieldId}
|
||||
value={infrastructureMonitor[fieldId]}
|
||||
onBlur={onFieldBlur}
|
||||
onChange={onFieldChange}
|
||||
error={errors[fieldId] ? true : false}
|
||||
disabled={!infrastructureMonitor[checkboxId]}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
component="p"
|
||||
m={theme.spacing(3)}
|
||||
>
|
||||
{alertUnit}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
CustomThreshold.propTypes = {
|
||||
checkboxId: PropTypes.string.isRequired,
|
||||
checkboxLabel: PropTypes.string.isRequired,
|
||||
onCheckboxChange: PropTypes.func.isRequired,
|
||||
fieldId: PropTypes.string.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onFieldBlur: PropTypes.func.isRequired,
|
||||
alertUnit: PropTypes.string.isRequired,
|
||||
infrastructureMonitor: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
};
|
402
Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
Normal file
402
Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
Normal file
@ -0,0 +1,402 @@
|
||||
import { useState } from "react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { infrastructureMonitorValidation } from "../../../Validation/validation";
|
||||
import { parseDomainName } from "../../../Utils/monitorUtils";
|
||||
import {
|
||||
createInfrastructureMonitor,
|
||||
checkInfrastructureEndpointResolution,
|
||||
} from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Link from "../../../Components/Link";
|
||||
import { ConfigBox } from "../../Monitors/styled";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
|
||||
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
|
||||
import { CustomThreshold } from "../CreateMonitor/CustomThreshold";
|
||||
|
||||
const CreateInfrastructureMonitor = () => {
|
||||
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
interval: 0.25,
|
||||
cpu: false,
|
||||
usage_cpu: "",
|
||||
memory: false,
|
||||
usage_memory: "",
|
||||
disk: false,
|
||||
usage_disk: "",
|
||||
temperature: false,
|
||||
usage_temperature: "",
|
||||
secret: "",
|
||||
});
|
||||
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const THRESHOLD_FIELD_PREFIX = "usage_";
|
||||
const HARDWARE_MONITOR_TYPES = ["cpu", "memory", "disk", "temperature"];
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const monitorState = useSelector((state) => state.infrastructureMonitor);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const alertErrKeyLen = Object.keys(errors).filter((k) =>
|
||||
k.startsWith(THRESHOLD_FIELD_PREFIX)
|
||||
).length;
|
||||
|
||||
const handleCustomAlertCheckChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setInfrastructureMonitor((prev) => {
|
||||
const newState = {
|
||||
[id]: prev[id] == undefined && value == "on" ? true : !prev[id],
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
...newState,
|
||||
[THRESHOLD_FIELD_PREFIX + id]: newState[id]
|
||||
? prev[THRESHOLD_FIELD_PREFIX + id]
|
||||
: "",
|
||||
};
|
||||
});
|
||||
// Remove the error if unchecked
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, [THRESHOLD_FIELD_PREFIX + id]);
|
||||
});
|
||||
};
|
||||
|
||||
const handleBlur = (event, appendID) => {
|
||||
event.preventDefault();
|
||||
const { value, id } = event.target;
|
||||
|
||||
let name = idMap[id] ?? id;
|
||||
if (name === "url" && infrastructureMonitor.name === "") {
|
||||
setInfrastructureMonitor((prev) => ({
|
||||
...prev,
|
||||
name: parseDomainName(value),
|
||||
}));
|
||||
}
|
||||
|
||||
if (id?.startsWith("notify-email-")) return;
|
||||
const { error } = infrastructureMonitorValidation.validate(
|
||||
{ [id ?? appendID]: value },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, id ?? appendID, error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (event, appendedId) => {
|
||||
event.preventDefault();
|
||||
const { value, id } = event.target;
|
||||
let name = appendedId ?? idMap[id] ?? id;
|
||||
if (name.includes("notification-")) {
|
||||
name = name.replace("notification-", "");
|
||||
let hasNotif = infrastructureMonitor.notifications.some(
|
||||
(notification) => notification.type === name
|
||||
);
|
||||
setInfrastructureMonitor((prev) => {
|
||||
const notifs = [...prev.notifications];
|
||||
if (hasNotif) {
|
||||
return {
|
||||
...prev,
|
||||
notifications: notifs.filter((notif) => notif.type !== name),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...prev,
|
||||
notifications: [
|
||||
...notifs,
|
||||
name === "email"
|
||||
? { type: name, address: value }
|
||||
: // TODO - phone number
|
||||
{ type: name, phone: value },
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setInfrastructureMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const generatePayload = (form) => {
|
||||
let thresholds = {};
|
||||
Object.keys(form)
|
||||
.filter((k) => k.startsWith(THRESHOLD_FIELD_PREFIX))
|
||||
.map((k) => {
|
||||
if (form[k]) thresholds[k] = form[k] / 100;
|
||||
delete form[k];
|
||||
delete form[k.substring(THRESHOLD_FIELD_PREFIX.length)];
|
||||
});
|
||||
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
type: "hardware",
|
||||
notifications: infrastructureMonitor.notifications,
|
||||
thresholds,
|
||||
};
|
||||
return form;
|
||||
};
|
||||
const handleCreateInfrastructureMonitor = async (event) => {
|
||||
event.preventDefault();
|
||||
let form = {
|
||||
...infrastructureMonitor,
|
||||
name:
|
||||
infrastructureMonitor.name === ""
|
||||
? infrastructureMonitor.url
|
||||
: infrastructureMonitor.name,
|
||||
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
|
||||
delete form.notifications;
|
||||
if (hasValidationErrors(form, infrastructureMonitorValidation, setErrors)) {
|
||||
return;
|
||||
} else {
|
||||
const checkEndpointAction = await dispatch(
|
||||
checkInfrastructureEndpointResolution({ authToken, monitorURL: form.url })
|
||||
);
|
||||
if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
createToast({
|
||||
body: "The endpoint you entered doesn't resolve. Check the URL again.",
|
||||
});
|
||||
setErrors({ url: "The entered URL is not reachable." });
|
||||
return;
|
||||
}
|
||||
const action = await dispatch(
|
||||
createInfrastructureMonitor({ authToken, monitor: generatePayload(form) })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Infrastructure monitor created successfully!" });
|
||||
navigate("/infrastructure");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//select values
|
||||
const frequencies = [
|
||||
{ _id: 0.25, name: "15 seconds" },
|
||||
{ _id: 0.5, name: "30 seconds" },
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box className="create-infrastructure-monitor">
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "Infrastructure monitors", path: "infrastructure" },
|
||||
{ name: "create", path: `infrastructure/create` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-infrastructure-monitor-form"
|
||||
onSubmit={handleCreateInfrastructureMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
>
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h2"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
infrastructure monitor
|
||||
</Typography>
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
Here you can select the URL of the host, together with the friendly name
|
||||
and authorization secret to connect to the server agent.
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
The server you are monitoring must be running the{" "}
|
||||
<Link
|
||||
level="primary"
|
||||
url="https://github.com/bluewave-labs/checkmate-agent"
|
||||
label="Checkmate Monitoring Agent"
|
||||
/>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="url"
|
||||
label="Server URL"
|
||||
placeholder="https://"
|
||||
value={infrastructureMonitor.url}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
error={errors["url"] ? true : false}
|
||||
helperText={errors["url"]}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="name"
|
||||
label="Display name"
|
||||
placeholder="Google"
|
||||
isOptional={true}
|
||||
value={infrastructureMonitor.name}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="secret"
|
||||
label="Authorization secret"
|
||||
value={infrastructureMonitor.secret}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
error={errors["secret"] ? true : false}
|
||||
helperText={errors["secret"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p">
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">When there is a new incident,</Typography>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={infrastructureMonitor.notifications.some(
|
||||
(notification) => notification.type === "email"
|
||||
)}
|
||||
value={user?.email}
|
||||
onChange={(e) => handleChange(e)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Customize alerts</Typography>
|
||||
<Typography component="p">
|
||||
Send a notification to user(s) when thresholds exceed a specified
|
||||
percentage.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
{HARDWARE_MONITOR_TYPES.map((type, idx) => (
|
||||
<CustomThreshold
|
||||
key={idx}
|
||||
checkboxId={type}
|
||||
checkboxLabel={
|
||||
type !== "cpu" ? capitalizeFirstLetter(type) : type.toUpperCase()
|
||||
}
|
||||
onCheckboxChange={handleCustomAlertCheckChange}
|
||||
fieldId={THRESHOLD_FIELD_PREFIX + type}
|
||||
fieldValue={infrastructureMonitor[THRESHOLD_FIELD_PREFIX + type] ?? ""}
|
||||
onFieldChange={handleChange}
|
||||
onFieldBlur={handleBlur}
|
||||
// TODO: need BE, maybe in another PR
|
||||
alertUnit={type == "temperature" ? "°C" : "%"}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
errors={errors}
|
||||
/>
|
||||
))}
|
||||
{alertErrKeyLen > 0 && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{
|
||||
errors[
|
||||
THRESHOLD_FIELD_PREFIX +
|
||||
HARDWARE_MONITOR_TYPES.filter(
|
||||
(type) => errors[THRESHOLD_FIELD_PREFIX + type]
|
||||
)[0]
|
||||
]
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
id="interval"
|
||||
label="Check frequency"
|
||||
value={infrastructureMonitor.interval || 15}
|
||||
onChange={(e) => handleChange(e, "interval")}
|
||||
onBlur={(e) => handleBlur(e, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateInfrastructureMonitor}
|
||||
loading={monitorState?.isLoading}
|
||||
>
|
||||
Create infrastructure monitor
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateInfrastructureMonitor;
|
37
Client/src/Pages/Infrastructure/Details/empty.jsx
Normal file
37
Client/src/Pages/Infrastructure/Details/empty.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
|
||||
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
|
||||
import { Box, Typography, Stack } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import { useSelector } from "react-redux";
|
||||
const Empty = ({ styles }) => {
|
||||
const theme = useTheme();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
return (
|
||||
<Box sx={{ ...styles, marginTop: theme.spacing(24) }}>
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(8)}
|
||||
alignItems="center"
|
||||
>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
|
||||
<Typography variant="h2">Your infrastructure dashboard will show here</Typography>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Hang tight! When we receive data, we'll show it here. Please check back in a few
|
||||
minutes.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Empty.propTypes = {
|
||||
styles: PropTypes.object,
|
||||
mode: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Empty;
|
589
Client/src/Pages/Infrastructure/Details/index.jsx
Normal file
589
Client/src/Pages/Infrastructure/Details/index.jsx
Normal file
@ -0,0 +1,589 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { Stack, Box, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CustomGauge from "../../../Components/Charts/CustomGauge";
|
||||
import AreaChart from "../../../Components/Charts/AreaChart";
|
||||
import { useSelector } from "react-redux";
|
||||
import { networkService } from "../../../main";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import useUtils from "../../Monitors/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Empty from "./empty";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
|
||||
import {
|
||||
TzTick,
|
||||
PercentTick,
|
||||
InfrastructureTooltip,
|
||||
TemperatureTooltip,
|
||||
} from "../../../Components/Charts/Utils/chartUtils";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const BASE_BOX_PADDING_VERTICAL = 4;
|
||||
const BASE_BOX_PADDING_HORIZONTAL = 8;
|
||||
const TYPOGRAPHY_PADDING = 8;
|
||||
/**
|
||||
* Converts bytes to gigabytes
|
||||
* @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 GB = bytes / (1024 * 1024 * 1024);
|
||||
const MB = bytes / (1024 * 1024);
|
||||
|
||||
if (GB >= 1) {
|
||||
return `${Number(GB.toFixed(0))} GB`;
|
||||
} else {
|
||||
return `${Number(MB.toFixed(0))} MB`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a decimal value to a percentage
|
||||
*
|
||||
* @function decimalToPercentage
|
||||
* @param {number} value - Decimal value to convert
|
||||
* @returns {number} Percentage representation
|
||||
*
|
||||
* @example
|
||||
* decimalToPercentage(0.75) // Returns 75
|
||||
* decimalToPercentage(null) // Returns 0
|
||||
*/
|
||||
const decimalToPercentage = (value) => {
|
||||
if (value === null || value === undefined) return 0;
|
||||
return value * 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a base box with consistent styling
|
||||
* @param {Object} props - Component properties
|
||||
* @param {React.ReactNode} props.children - Child components to render inside the box
|
||||
* @param {Object} props.sx - Additional styling for the box
|
||||
* @returns {React.ReactElement} Styled box component
|
||||
*/
|
||||
const BaseBox = ({ children, sx = {} }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
padding: `${theme.spacing(BASE_BOX_PADDING_VERTICAL)} ${theme.spacing(BASE_BOX_PADDING_HORIZONTAL)}`,
|
||||
minWidth: 200,
|
||||
width: 225,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
BaseBox.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
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
|
||||
* @param {number} props.value - Percentage value for gauge
|
||||
* @param {string} props.heading - Box heading
|
||||
* @param {string} props.metricOne - First metric label
|
||||
* @param {string} props.valueOne - First metric value
|
||||
* @param {string} props.metricTwo - Second metric label
|
||||
* @param {string} props.valueTwo - Second metric value
|
||||
* @returns {React.ReactElement} Gauge box component
|
||||
*/
|
||||
const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BaseBox>
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
>
|
||||
<CustomGauge
|
||||
progress={value}
|
||||
radius={100}
|
||||
color={theme.palette.primary.main}
|
||||
/>
|
||||
<Typography component="h2">{heading}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
borderTop: `1px solid ${theme.palette.border.light}`,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{metricOne}</Typography>
|
||||
<Typography>{valueOne}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{metricTwo}</Typography>
|
||||
<Typography>{valueTwo}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
GaugeBox.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
heading: PropTypes.string.isRequired,
|
||||
metricOne: PropTypes.string.isRequired,
|
||||
valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
metricTwo: PropTypes.string.isRequired,
|
||||
valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the infrastructure details page
|
||||
* @returns {React.ReactElement} Infrastructure details page component
|
||||
*/
|
||||
const InfrastructureDetails = () => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { monitorId } = useParams();
|
||||
const navList = [
|
||||
{ name: "infrastructure monitors", path: "/infrastructure" },
|
||||
{ name: "details", path: `/infrastructure/${monitorId}` },
|
||||
];
|
||||
const [monitor, setMonitor] = useState(null);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [dateRange, setDateRange] = useState("all");
|
||||
const { statusColor, determineState } = useUtils();
|
||||
// These calculations are needed because ResponsiveContainer
|
||||
// doesn't take padding of parent/siblings into account
|
||||
// when calculating height.
|
||||
const chartContainerHeight = 300;
|
||||
const totalChartContainerPadding =
|
||||
parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2;
|
||||
const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2;
|
||||
const areaChartHeight =
|
||||
(chartContainerHeight - totalChartContainerPadding - totalTypographyPadding) * 0.95;
|
||||
// end height calculations
|
||||
|
||||
const buildStatBoxes = (checks) => {
|
||||
let latestCheck = checks[0] ?? null;
|
||||
if (latestCheck === null) return [];
|
||||
|
||||
// Extract values from latest check
|
||||
const physicalCores = latestCheck?.cpu?.physical_core ?? 0;
|
||||
const logicalCores = latestCheck?.cpu?.logical_core ?? 0;
|
||||
const cpuFrequency = latestCheck?.cpu?.frequency ?? 0;
|
||||
const cpuTemperature =
|
||||
latestCheck?.cpu?.temperature?.length > 0
|
||||
? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) /
|
||||
latestCheck.cpu.temperature.length
|
||||
: 0;
|
||||
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
|
||||
const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0;
|
||||
const os = latestCheck?.host?.os ?? null;
|
||||
const platform = latestCheck?.host?.platform ?? null;
|
||||
const osPlatform = os === null && platform === null ? null : `${os} ${platform}`;
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
heading: "CPU (Physical)",
|
||||
subHeading: `${physicalCores} cores`,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
heading: "CPU (Logical)",
|
||||
subHeading: `${logicalCores} cores`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
heading: "CPU Frequency",
|
||||
subHeading: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
heading: "Average CPU Temperature",
|
||||
subHeading: `${cpuTemperature.toFixed(2)} C`,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
heading: "Memory",
|
||||
subHeading: formatBytes(memoryTotalBytes),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
heading: "Disk",
|
||||
subHeading: formatBytes(diskTotalBytes),
|
||||
},
|
||||
{ id: 6, heading: "Uptime", subHeading: "100%" },
|
||||
{
|
||||
id: 7,
|
||||
heading: "Status",
|
||||
subHeading: monitor?.status === true ? "Active" : "Inactive",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
heading: "OS",
|
||||
subHeading: osPlatform,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const buildGaugeBoxConfigs = (checks) => {
|
||||
let latestCheck = checks[0] ?? null;
|
||||
if (latestCheck === null) return [];
|
||||
|
||||
// Extract values from latest check
|
||||
const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0;
|
||||
const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0;
|
||||
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
|
||||
const cpuUsagePercent = latestCheck?.cpu?.usage_percent ?? 0;
|
||||
const cpuPhysicalCores = latestCheck?.cpu?.physical_core ?? 0;
|
||||
const cpuFrequency = latestCheck?.cpu?.frequency ?? 0;
|
||||
return [
|
||||
{
|
||||
type: "memory",
|
||||
value: decimalToPercentage(memoryUsagePercent),
|
||||
heading: "Memory Usage",
|
||||
metricOne: "Used",
|
||||
valueOne: formatBytes(memoryUsedBytes),
|
||||
metricTwo: "Total",
|
||||
valueTwo: formatBytes(memoryTotalBytes),
|
||||
},
|
||||
{
|
||||
type: "cpu",
|
||||
value: decimalToPercentage(cpuUsagePercent),
|
||||
heading: "CPU Usage",
|
||||
metricOne: "Cores",
|
||||
valueOne: cpuPhysicalCores ?? 0,
|
||||
metricTwo: "Frequency",
|
||||
valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
|
||||
},
|
||||
...(latestCheck?.disk ?? []).map((disk, idx) => ({
|
||||
type: "disk",
|
||||
diskIndex: idx,
|
||||
value: decimalToPercentage(disk.usage_percent),
|
||||
heading: `Disk${idx} usage`,
|
||||
metricOne: "Used",
|
||||
valueOne: formatBytes(disk.total_bytes - disk.free_bytes),
|
||||
metricTwo: "Total",
|
||||
valueTwo: formatBytes(disk.total_bytes),
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
||||
const buildTemps = (checks) => {
|
||||
let numCores = 1;
|
||||
if (checks === null) return { temps: [], tempKeys: [] };
|
||||
|
||||
for (const check of checks) {
|
||||
if (check?.cpu?.temperature?.length > numCores) {
|
||||
numCores = check.cpu.temperature.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const temps = checks.map((check) => {
|
||||
// If there's no data, set the temperature to 0
|
||||
if (
|
||||
check?.cpu?.temperature?.length === 0 ||
|
||||
check?.cpu?.temperature === undefined ||
|
||||
check?.cpu?.temperature === null
|
||||
) {
|
||||
check.cpu.temperature = Array(numCores).fill(0);
|
||||
}
|
||||
const res = check?.cpu?.temperature?.reduce(
|
||||
(acc, cur, idx) => {
|
||||
acc[`core${idx + 1}`] = cur;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
createdAt: check.createdAt,
|
||||
}
|
||||
);
|
||||
return res;
|
||||
});
|
||||
if (temps.length === 0 || !temps[0]) {
|
||||
return { temps: [], tempKeys: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
tempKeys: Object.keys(temps[0] || {}).filter((key) => key !== "createdAt"),
|
||||
temps,
|
||||
};
|
||||
};
|
||||
|
||||
const buildAreaChartConfigs = (checks) => {
|
||||
let latestCheck = checks[0] ?? null;
|
||||
if (latestCheck === null) return [];
|
||||
const reversedChecks = checks.toReversed();
|
||||
const tempData = buildTemps(reversedChecks);
|
||||
return [
|
||||
{
|
||||
type: "memory",
|
||||
data: reversedChecks,
|
||||
dataKeys: ["memory.usage_percent"],
|
||||
heading: "Memory usage",
|
||||
strokeColor: theme.palette.primary.main,
|
||||
gradientStartColor: theme.palette.primary.main,
|
||||
yLabel: "Memory Usage",
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.primary.main}
|
||||
yKey={"memory.usage_percent"}
|
||||
yLabel={"Memory Usage"}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "cpu",
|
||||
data: reversedChecks,
|
||||
dataKeys: ["cpu.usage_percent"],
|
||||
heading: "CPU usage",
|
||||
strokeColor: theme.palette.success.main,
|
||||
gradientStartColor: theme.palette.success.main,
|
||||
yLabel: "CPU Usage",
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.success.main}
|
||||
yKey={"cpu.usage_percent"}
|
||||
yLabel={"CPU Usage"}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "temperature",
|
||||
data: tempData.temps,
|
||||
dataKeys: tempData.tempKeys,
|
||||
strokeColor: theme.palette.error.main,
|
||||
gradientStartColor: theme.palette.error.main,
|
||||
heading: "CPU Temperature",
|
||||
yLabel: "Temperature",
|
||||
xTick: <TzTick />,
|
||||
yDomain: [
|
||||
0,
|
||||
Math.max(
|
||||
Math.max(
|
||||
...tempData.temps.flatMap((t) => tempData.tempKeys.map((k) => t[k]))
|
||||
) * 1.1,
|
||||
200
|
||||
),
|
||||
],
|
||||
toolTip: (
|
||||
<TemperatureTooltip
|
||||
keys={tempData.tempKeys}
|
||||
dotColor={theme.palette.error.main}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(latestCheck?.disk?.map((disk, idx) => ({
|
||||
type: "disk",
|
||||
data: reversedChecks,
|
||||
diskIndex: idx,
|
||||
dataKeys: [`disk[${idx}].usage_percent`],
|
||||
heading: `Disk${idx} usage`,
|
||||
strokeColor: theme.palette.warning.main,
|
||||
gradientStartColor: theme.palette.warning.main,
|
||||
yLabel: "Disk Usage",
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.warning.main}
|
||||
yKey={`disk.usage_percent`}
|
||||
yLabel={"Disc usage"}
|
||||
yIdx={idx}
|
||||
/>
|
||||
),
|
||||
})) || []),
|
||||
];
|
||||
};
|
||||
|
||||
// Fetch data
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await networkService.getStatsByMonitorId({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
sortOrder: null,
|
||||
limit: null,
|
||||
dateRange: dateRange,
|
||||
numToDisplay: 50,
|
||||
normalize: false,
|
||||
});
|
||||
setMonitor(response.data.data);
|
||||
} catch (error) {
|
||||
navigate("/not-found", { replace: true });
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [authToken, monitorId, dateRange, navigate]);
|
||||
|
||||
const statBoxConfigs = buildStatBoxes(monitor?.checks ?? []);
|
||||
const gaugeBoxConfigs = buildGaugeBoxConfigs(monitor?.checks ?? []);
|
||||
const areaChartConfigs = buildAreaChartConfigs(monitor?.checks ?? []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Breadcrumbs list={navList} />
|
||||
{monitor?.checks?.length > 0 ? (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(10)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
<Typography
|
||||
alignSelf="end"
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Typography alignSelf="end">{monitor.url || "..."}</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Typography alignSelf="end">
|
||||
Checking every {formatDurationRounded(monitor?.interval)}
|
||||
</Typography>
|
||||
<Typography alignSelf="end">
|
||||
Last checked {formatDurationSplit(monitor?.lastChecked).time}{" "}
|
||||
{formatDurationSplit(monitor?.lastChecked).format} ago
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
{statBoxConfigs.map((statBox) => (
|
||||
<StatBox
|
||||
key={statBox.id}
|
||||
{...statBox}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
{gaugeBoxConfigs.map((config) => (
|
||||
<GaugeBox
|
||||
key={`${config.type}-${config.diskIndex ?? ""}`}
|
||||
value={config.value}
|
||||
heading={config.heading}
|
||||
metricOne={config.metricOne}
|
||||
valueOne={config.valueOne}
|
||||
metricTwo={config.metricTwo}
|
||||
valueTwo={config.valueTwo}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
height={chartContainerHeight} // FE team HELP!
|
||||
gap={theme.spacing(8)} // FE team HELP!
|
||||
flexWrap="wrap" // //FE team HELP! Better way to do this?
|
||||
sx={{
|
||||
"& > *": {
|
||||
flexBasis: `calc(50% - ${theme.spacing(8)})`,
|
||||
maxWidth: `calc(50% - ${theme.spacing(8)})`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{areaChartConfigs.map((config) => {
|
||||
return (
|
||||
<BaseBox key={`${config.type}-${config.diskIndex ?? ""}`}>
|
||||
<Typography
|
||||
component="h2"
|
||||
padding={theme.spacing(8)}
|
||||
>
|
||||
{config.heading}
|
||||
</Typography>
|
||||
<AreaChart
|
||||
height={areaChartHeight}
|
||||
data={config.data}
|
||||
dataKeys={config.dataKeys}
|
||||
xKey="createdAt"
|
||||
yDomain={config.yDomain}
|
||||
customTooltip={config.toolTip}
|
||||
xTick={config.xTick}
|
||||
yTick={config.yTick}
|
||||
strokeColor={config.strokeColor}
|
||||
gradient={true}
|
||||
gradientStartColor={config.gradientStartColor}
|
||||
gradientEndColor="#ffffff"
|
||||
/>
|
||||
</BaseBox>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<Empty
|
||||
styles={{
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
p: theme.spacing(30),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureDetails;
|
223
Client/src/Pages/Infrastructure/components/Menu/index.jsx
Normal file
223
Client/src/Pages/Infrastructure/components/Menu/index.jsx
Normal file
@ -0,0 +1,223 @@
|
||||
/* TODO I basically copied and pasted this component from the actionsMenu. Check how we can make it reusable */
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import Settings from "../../../../assets/icons/settings-bold.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
import Dialog from "../../../../Components/Dialog";
|
||||
import { networkService } from "../../../../Utils/NetworkService.js";
|
||||
|
||||
/**
|
||||
* InfrastructureMenu Component
|
||||
* Provides a dropdown menu for managing infrastructure monitors.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {Object} props.monitor - The monitor object containing details about the infrastructure monitor.
|
||||
* @param {string} props.monitor.id - Unique ID of the monitor.
|
||||
* @param {string} [props.monitor.url] - URL associated with the monitor.
|
||||
* @param {string} props.monitor.type - Type of monitor (e.g., uptime, infrastructure).
|
||||
* @param {boolean} props.monitor.isActive - Indicates if the monitor is currently active.
|
||||
* @param {boolean} props.isAdmin - Whether the user has admin privileges.
|
||||
* @param {Function} props.updateCallback - Callback to trigger when the monitor data is updated.
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
const anchor = useRef(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const authToken = authState.authToken;
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
|
||||
const openMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openRemove = (e) => {
|
||||
closeMenu(e);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
const cancelRemove = () => {
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
function openDetails(id) {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
}
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
await networkService.deleteMonitorById({
|
||||
authToken,
|
||||
monitorId: monitor.id,
|
||||
});
|
||||
createToast({ body: "Monitor deleted successfully." });
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
} finally {
|
||||
setIsDialogOpen(false);
|
||||
updateCallback();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={openMenu}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
ref={anchor}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchor.current}
|
||||
open={isOpen}
|
||||
onClose={closeMenu}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/*
|
||||
Open site action. Not necessary for infrastructure?
|
||||
|
||||
{actions.url !== null ? (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
window.open(actions.url, "_blank", "noreferrer");
|
||||
}}
|
||||
>
|
||||
Open site
|
||||
</MenuItem>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
*/}
|
||||
<MenuItem onClick={() => openDetails(monitor.id)}>Details</MenuItem>
|
||||
{/*
|
||||
Incidents. Necessary?
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/incidents/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Incidents
|
||||
</MenuItem> */}
|
||||
{/*
|
||||
Configure. Necessary?
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
navigate(`/monitors/configure/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
</MenuItem>
|
||||
)} */}
|
||||
{/*
|
||||
Clone. Necessary?
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/monitors/create/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Clone
|
||||
</MenuItem>
|
||||
)} */}
|
||||
{/*
|
||||
Pause. Necessary?
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
const action = await dispatch(
|
||||
pauseUptimeMonitor({ authToken, monitorId: monitor._id })
|
||||
);
|
||||
if (pauseUptimeMonitor.fulfilled.match(action)) {
|
||||
updateCallback();
|
||||
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
|
||||
createToast({ body: `Monitor ${state} successfully.` });
|
||||
} else {
|
||||
throw new Error(action?.error?.message ?? "Failed to pause monitor.");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor:", monitor._id, error);
|
||||
createToast({ body: "Failed to pause monitor." });
|
||||
}
|
||||
};
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePause(e);
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive === true ? "Pause" : "Resume"}
|
||||
</MenuItem>
|
||||
)} */}
|
||||
{isAdmin && <MenuItem onClick={openRemove}>Remove</MenuItem>}
|
||||
</Menu>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
theme={theme}
|
||||
title="Do you really want to delete this monitor?"
|
||||
description="Once deleted, this monitor cannot be retrieved."
|
||||
onCancel={cancelRemove}
|
||||
confirmationButtonLabel="Delete"
|
||||
onConfirm={handleRemove}
|
||||
isLoading={isLoading}
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InfrastructureMenu.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
}).isRequired,
|
||||
isAdmin: PropTypes.bool,
|
||||
updateCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
export { InfrastructureMenu };
|
@ -0,0 +1,80 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import LeftArrowDouble from "../../../../../assets/icons/left-arrow-double.svg?react";
|
||||
import RightArrowDouble from "../../../../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../../../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../../../../assets/icons/right-arrow.svg?react";
|
||||
|
||||
TablePaginationActions.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component for pagination actions (first, previous, next, last).
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {number} props.count - Total number of items.
|
||||
* @param {number} props.page - Current page number.
|
||||
* @param {number} props.rowsPerPage - Number of rows per page.
|
||||
* @param {function} props.onPageChange - Callback function to handle page change.
|
||||
*
|
||||
* @returns {JSX.Element} Pagination actions component.
|
||||
*/
|
||||
|
||||
function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
|
||||
const handleFirstPageButtonClick = (event) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
const handleBackButtonClick = (event) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
const handleNextButtonClick = (event) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
const handleLastPageButtonClick = (event) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: "24px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
<LeftArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
<RightArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
<RightArrowDouble />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { TablePaginationActions };
|
@ -0,0 +1,117 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Stack, TablePagination, Typography } from "@mui/material";
|
||||
import { TablePaginationActions } from "./Actions";
|
||||
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
|
||||
|
||||
Pagination.propTypes = {
|
||||
monitorCount: PropTypes.number.isRequired, // Total number of items for pagination.
|
||||
page: PropTypes.number.isRequired, // Current page index.
|
||||
rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page.
|
||||
handleChangePage: PropTypes.func.isRequired, // Function to handle page changes.
|
||||
handleChangeRowsPerPage: PropTypes.func.isRequired, // Function to handle changes in rows per page.
|
||||
};
|
||||
|
||||
const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
|
||||
|
||||
/**
|
||||
* Pagination component for table navigation with customized styling and behavior.
|
||||
*
|
||||
* @param {object} props - Component properties.
|
||||
* @param {number} props.monitorCount - Total number of monitors to paginate.
|
||||
* @param {number} props.page - Current page index (0-based).
|
||||
* @param {number} props.rowsPerPage - Number of rows to display per page.
|
||||
* @param {function} props.handleChangePage - Callback for handling page changes.
|
||||
* @param {function} props.handleChangeRowsPerPage - Callback for handling changes to rows per page.
|
||||
* @returns {JSX.Element} The Pagination component.
|
||||
*/
|
||||
function Pagination({
|
||||
monitorCount,
|
||||
page,
|
||||
rowsPerPage,
|
||||
handleChangePage,
|
||||
handleChangeRowsPerPage,
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const start = page * rowsPerPage + 1;
|
||||
const end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount);
|
||||
const range = `${start} - ${end}`;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(4)}
|
||||
marginTop={8}
|
||||
>
|
||||
<Typography
|
||||
px={theme.spacing(2)}
|
||||
variant="body2"
|
||||
sx={{ opacity: 0.7 }}
|
||||
>
|
||||
Showing {range} of {monitorCount} monitor(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={monitorCount}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
labelRowsPerPage="Rows per page"
|
||||
labelDisplayedRows={({ page, count }) =>
|
||||
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
|
||||
}
|
||||
slotProps={{
|
||||
select: {
|
||||
MenuProps: {
|
||||
keepMounted: true,
|
||||
disableScrollLock: true,
|
||||
PaperProps: {
|
||||
className: "pagination-dropdown",
|
||||
sx: {
|
||||
mt: 0,
|
||||
mb: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
sx: {
|
||||
mt: theme.spacing(-2),
|
||||
},
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
sx: {
|
||||
ml: theme.spacing(4),
|
||||
mr: theme.spacing(12),
|
||||
minWidth: theme.spacing(20),
|
||||
textAlign: "left",
|
||||
"&.Mui-focused > div": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
"& svg path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.3,
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export { Pagination };
|
368
Client/src/Pages/Infrastructure/index.jsx
Normal file
368
Client/src/Pages/Infrastructure/index.jsx
Normal file
@ -0,0 +1,368 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { /* useDispatch, */ useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import useUtils from "../Monitors/utils";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
// import GearIcon from "../../Assets/icons/settings-bold.svg?react";
|
||||
import CPUChipIcon from "../../assets/icons/cpu-chip.svg?react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { Heading } from "../../Components/Heading";
|
||||
import { Pagination } from "./components/TablePagination";
|
||||
// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
|
||||
import { networkService } from "../../Utils/NetworkService.js";
|
||||
import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx";
|
||||
import Host from "../Monitors/Home/host.jsx";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin.js";
|
||||
import { InfrastructureMenu } from "./components/Menu";
|
||||
|
||||
const columns = [
|
||||
{ label: "Host" },
|
||||
{ label: "Status" },
|
||||
{ label: "Frequency" },
|
||||
{ label: "CPU" },
|
||||
{ label: "Mem" },
|
||||
{ label: "Disk" },
|
||||
{ label: "Actions" },
|
||||
];
|
||||
|
||||
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
|
||||
|
||||
/* TODO
|
||||
Create reusable table component.
|
||||
It should receive as a parameter the following object:
|
||||
tableData = [
|
||||
columns = [
|
||||
{
|
||||
id: example,
|
||||
label: Example Extendable,
|
||||
align: "center" | "left" (default)
|
||||
}
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
**Number of keys will be equal to number of columns**
|
||||
key1: string,
|
||||
key2: number,
|
||||
key3: Component
|
||||
}
|
||||
]
|
||||
]
|
||||
Apply to Monitor Table, and Account/Team.
|
||||
Analyze existing BasicTable
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is the Infrastructure monitoring page. This is a work in progress
|
||||
*
|
||||
* @param - Define params.
|
||||
* @returns {JSX.Element} The infrastructure monitoring page.
|
||||
*/
|
||||
|
||||
function Infrastructure() {
|
||||
/* Adding this custom hook so we can avoid using the HOC approach that can lower performance (we are calling the admin logic N times on initializing the project. using a custom hook will cal it ass needed ) */
|
||||
const isAdmin = useIsAdmin();
|
||||
const theme = useTheme();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigateToCreate = () => navigate("/infrastructure/create");
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
/* TODO refactor this, so it is not aware of the MUI implementation. First argument only exists because of MUI. This should require onlu the new page. Adapting for MUI should happen inside of table pagination component */
|
||||
const handleChangePage = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value));
|
||||
setPage(0);
|
||||
};
|
||||
const [monitorState, setMonitorState] = useState({ monitors: [], total: 0 });
|
||||
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const user = jwtDecode(authToken);
|
||||
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getMonitorsByTeamId({
|
||||
authToken,
|
||||
teamId: user.teamId,
|
||||
limit: 1,
|
||||
types: ["hardware"],
|
||||
status: null,
|
||||
checkOrder: "desc",
|
||||
normalize: true,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
});
|
||||
setMonitorState({
|
||||
monitors: response?.data?.data?.monitors ?? [],
|
||||
total: response?.data?.data?.monitorCount ?? 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMonitors();
|
||||
}, [page, rowsPerPage]);
|
||||
|
||||
const { determineState } = useUtils();
|
||||
const { monitors, total: totalMonitors } = monitorState;
|
||||
// do it here
|
||||
function openDetails(id) {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
}
|
||||
function handleActionMenuDelete() {
|
||||
fetchMonitors();
|
||||
}
|
||||
|
||||
const monitorsAsRows = monitors.map((monitor) => {
|
||||
const processor =
|
||||
((monitor.checks[0]?.cpu?.usage_frequency ?? 0) / 1000).toFixed(2) + " GHz";
|
||||
const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100;
|
||||
const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100;
|
||||
const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100;
|
||||
const status = determineState(monitor);
|
||||
const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100)
|
||||
.toFixed(2)
|
||||
.toString();
|
||||
const percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
return {
|
||||
id: monitor._id,
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
processor,
|
||||
cpu,
|
||||
mem,
|
||||
disk,
|
||||
status,
|
||||
uptimePercentage,
|
||||
percentageColor,
|
||||
};
|
||||
});
|
||||
|
||||
let isActuallyLoading = isLoading && monitorState.monitors?.length === 0;
|
||||
return (
|
||||
<Box
|
||||
className="infrastructure-monitor"
|
||||
sx={{
|
||||
':has(> [class*="fallback__"])': {
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderStyle: "dashed",
|
||||
backgroundColor: theme.palette.background.main,
|
||||
overflow: "hidden",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isActuallyLoading ? (
|
||||
<SkeletonLayout />
|
||||
) : monitorState.monitors?.length !== 0 ? (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: "end",
|
||||
alignItems: "center",
|
||||
gap: "1rem",
|
||||
flexWrap: "wrap",
|
||||
marginBottom: "2rem",
|
||||
}}
|
||||
>
|
||||
{/*
|
||||
This will be removed from here, but keeping the commented code to remind me to add a max width to the greeting component
|
||||
<Box style={{ maxWidth: "65ch" }}>
|
||||
<Greeting type="uptime" />
|
||||
</Box> */}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={navigateToCreate}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create infrastructure monitor
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack
|
||||
sx={{
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
gap: ".25rem",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Heading component="h2">Infrastructure monitors</Heading>
|
||||
{/* TODO Correct the class current-monitors-counter, there are some unnecessary things there */}
|
||||
<Box
|
||||
component="span"
|
||||
className="current-monitors-counter"
|
||||
color={theme.palette.text.primary}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{totalMonitors}
|
||||
</Box>
|
||||
</Stack>
|
||||
<TableContainer component={Paper}>
|
||||
<Table stickyHeader>
|
||||
<TableHead sx={{ backgroundColor: theme.palette.background.accent }}>
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
align={index === 0 ? "left" : "center"}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
}}
|
||||
>
|
||||
{column.label}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{monitorsAsRows.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => openDetails(row.id)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* TODO iterate over column and get column id, applying row[column.id] */}
|
||||
<TableCell>
|
||||
<Host
|
||||
title={row.name}
|
||||
url={row.url}
|
||||
percentage={row.uptimePercentage}
|
||||
percentageColor={row.percentageColor}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
text={row.status}
|
||||
/* Use capitalize inside of Status Label */
|
||||
/* Update component so we don't need to pass text and status separately*/
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
gap=".25rem"
|
||||
>
|
||||
<CPUChipIcon
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{row.processor}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.cpu} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.mem} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.disk} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{/* Get ActionsMenu from Monitor Table and create a component */}
|
||||
<IconButton
|
||||
sx={{
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InfrastructureMenu
|
||||
monitor={row}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
{/* <GearIcon
|
||||
width={20}
|
||||
height={20}
|
||||
/> */}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Pagination
|
||||
monitorCount={totalMonitors}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<Fallback
|
||||
vowelStart={true}
|
||||
title="infrastructure monitor"
|
||||
checks={[
|
||||
"Track the performance of your servers",
|
||||
"Identify bottlenecks and optimize usage",
|
||||
"Ensure reliability with real-time monitoring",
|
||||
]}
|
||||
link="/infrastructure/create"
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { Infrastructure };
|
73
Client/src/Pages/Infrastructure/skeleton.jsx
Normal file
73
Client/src/Pages/Infrastructure/skeleton.jsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { Box, Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.spacing(12)}
|
||||
>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="25%"
|
||||
height={24}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={19.5}
|
||||
sx={{ mt: theme.spacing(2) }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
@ -1,4 +1,4 @@
|
||||
import { Box, Button, duration, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
@ -13,7 +13,7 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import CalendarIcon from "../../../assets/icons/calendar.svg?react";
|
||||
import "./index.css";
|
||||
@ -215,8 +215,7 @@ const CreateMaintenance = () => {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors))
|
||||
return;
|
||||
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return;
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
@ -467,14 +466,15 @@ const CreateMaintenance = () => {
|
||||
direction="row"
|
||||
spacing={theme.spacing(8)}
|
||||
>
|
||||
<Field
|
||||
<TextInput
|
||||
type="number"
|
||||
id="duration"
|
||||
value={form.duration}
|
||||
onChange={(event) => {
|
||||
handleFormChange("duration", event.target.value);
|
||||
}}
|
||||
error={errors["duration"]}
|
||||
error={errors["duration"] ? true : false}
|
||||
helperText={errors["duration"]}
|
||||
/>
|
||||
<Select
|
||||
id="durationUnit"
|
||||
@ -511,14 +511,15 @@ const CreateMaintenance = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Field
|
||||
<TextInput
|
||||
id="name"
|
||||
placeholder="Maintenance at __ : __ for ___ minutes"
|
||||
value={form.name}
|
||||
onChange={(event) => {
|
||||
handleFormChange("name", event.target.value);
|
||||
}}
|
||||
error={errors["name"]}
|
||||
error={errors["name"] ? true : false}
|
||||
helperText={errors["name"]}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
@ -114,7 +114,7 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.text,
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -14,7 +14,8 @@ import {
|
||||
getUptimeMonitorsByTeamId,
|
||||
deleteUptimeMonitor,
|
||||
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import PauseIcon from "../../../assets/icons/pause-icon.svg?react";
|
||||
import ResumeIcon from "../../../assets/icons/resume-icon.svg?react";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
@ -343,17 +344,17 @@ const Configure = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
<TextInput
|
||||
type={monitor?.type === "http" ? "url" : "text"}
|
||||
https={protocol === "https"}
|
||||
startAdornment={<HttpAdornment https={protocol === "https"} />}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
placeholder="google.com"
|
||||
value={parsedUrl?.host || monitor?.url || ""}
|
||||
disabled={true}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Display name"
|
||||
@ -361,7 +362,8 @@ const Configure = () => {
|
||||
placeholder="Google"
|
||||
value={monitor?.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
error={errors["name"] ? true : false}
|
||||
helperText={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@ -405,7 +407,7 @@ const Configure = () => {
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<Field
|
||||
<TextInput
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
|
@ -1,40 +1,72 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
// React, Redux, Router
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../styled";
|
||||
import Radio from "../../../Components/Inputs/Radio";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
// Utility and Network
|
||||
import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { getUptimeMonitorById } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import "./index.css";
|
||||
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
|
||||
// MUI
|
||||
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
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";
|
||||
|
||||
const CreateMonitor = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { monitors, isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const SELECT_VALUES = [
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 4, name: "4 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
];
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
const monitorTypeMaps = {
|
||||
http: {
|
||||
label: "URL to monitor",
|
||||
placeholder: "google.com",
|
||||
namePlaceholder: "Google",
|
||||
},
|
||||
ping: {
|
||||
label: "IP address to monitor",
|
||||
placeholder: "1.1.1.1",
|
||||
namePlaceholder: "Google",
|
||||
},
|
||||
docker: {
|
||||
label: "Container ID",
|
||||
placeholder: "abc123",
|
||||
namePlaceholder: "My Container",
|
||||
},
|
||||
};
|
||||
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { monitors, isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { monitorId } = useParams();
|
||||
const crumbs = [
|
||||
{ name: "monitors", path: "/monitors" },
|
||||
{ name: "create", path: `/monitors/create` },
|
||||
];
|
||||
|
||||
// State
|
||||
const [errors, setErrors] = useState({});
|
||||
const [https, setHttps] = useState(true);
|
||||
const [monitor, setMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
@ -42,8 +74,105 @@ const CreateMonitor = () => {
|
||||
notifications: [],
|
||||
interval: 1,
|
||||
});
|
||||
const [https, setHttps] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleCreateMonitor = async (event) => {
|
||||
event.preventDefault();
|
||||
let form = {
|
||||
url:
|
||||
//prepending protocol for url
|
||||
monitor.type === "http"
|
||||
? `http${https ? "s" : ""}://` + monitor.url
|
||||
: monitor.url,
|
||||
name: monitor.name === "" ? monitor.url : monitor.name,
|
||||
type: monitor.type,
|
||||
interval: monitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
|
||||
const { error } = monitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Please check the form for errors." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.type === "http") {
|
||||
const checkEndpointAction = await dispatch(
|
||||
checkEndpointResolution({ authToken, monitorURL: form.url })
|
||||
);
|
||||
if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
createToast({
|
||||
body: "The endpoint you entered doesn't resolve. Check the URL again.",
|
||||
});
|
||||
setErrors({ url: "The entered URL is not reachable." });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const action = await dispatch(createUptimeMonitor({ authToken, monitor: form }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
navigate("/monitors");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event, formName) => {
|
||||
const { value } = event.target;
|
||||
setMonitor({
|
||||
...monitor,
|
||||
[formName]: value,
|
||||
});
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [formName]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
...(error ? { [formName]: error.details[0].message } : { [formName]: undefined }),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleNotifications = (event, type) => {
|
||||
const { value } = event.target;
|
||||
let notifications = [...monitor.notifications];
|
||||
const notificationExists = notifications.some((notification) => {
|
||||
if (notification.type === type && notification.address === value) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (notificationExists) {
|
||||
notifications = notifications.filter((notification) => {
|
||||
if (notification.type === type && notification.address === value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
notifications.push({ type, address: value });
|
||||
}
|
||||
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
notifications,
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
@ -71,136 +200,16 @@ const CreateMonitor = () => {
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, authToken, monitors]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
const { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
|
||||
if (name.includes("notification-")) {
|
||||
name = name.replace("notification-", "");
|
||||
let hasNotif = monitor.notifications.some(
|
||||
(notification) => notification.type === name
|
||||
);
|
||||
setMonitor((prev) => {
|
||||
const notifs = [...prev.notifications];
|
||||
if (hasNotif) {
|
||||
return {
|
||||
...prev,
|
||||
notifications: notifs.filter((notif) => notif.type !== name),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...prev,
|
||||
notifications: [
|
||||
...notifs,
|
||||
name === "email"
|
||||
? { type: name, address: value }
|
||||
: // TODO - phone number
|
||||
{ type: name, phone: value },
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
console.log(error);
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) updatedErrors[name] = error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateMonitor = async (event) => {
|
||||
event.preventDefault();
|
||||
//obj to submit
|
||||
let form = {
|
||||
url:
|
||||
//preprending protocol for url
|
||||
monitor.type === "http"
|
||||
? `http${https ? "s" : ""}://` + monitor.url
|
||||
: monitor.url,
|
||||
name: monitor.name === "" ? monitor.url : monitor.name,
|
||||
type: monitor.type,
|
||||
interval: monitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
|
||||
const { error } = monitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validation data." });
|
||||
} else {
|
||||
if (monitor.type === "http") {
|
||||
const checkEndpointAction = await dispatch(
|
||||
checkEndpointResolution({ authToken, monitorURL: form.url })
|
||||
)
|
||||
if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." });
|
||||
setErrors({ url: "The entered URL is not reachable." });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const action = await dispatch(createUptimeMonitor({ authToken, monitor: form }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
navigate("/monitors");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//select values
|
||||
const frequencies = [
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 4, name: "4 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
];
|
||||
}, [monitorId, authToken, monitors, dispatch, navigate]);
|
||||
|
||||
return (
|
||||
<Box className="create-monitor">
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "monitors", path: "/monitors" },
|
||||
{ name: "create", path: `/monitors/create` },
|
||||
]}
|
||||
/>
|
||||
<Breadcrumbs list={crumbs} />
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-monitor-form"
|
||||
onSubmit={handleCreateMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
onSubmit={handleCreateMonitor}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
@ -221,6 +230,7 @@ const CreateMonitor = () => {
|
||||
monitor
|
||||
</Typography>
|
||||
</Typography>
|
||||
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
@ -229,25 +239,29 @@ const CreateMonitor = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Field
|
||||
<TextInput
|
||||
type={monitor.type === "http" ? "url" : "text"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
startAdornment={
|
||||
monitor.type === "http" ? <HttpAdornment https={https} /> : null
|
||||
}
|
||||
label={monitorTypeMaps[monitor.type].label || "URL to monitor"}
|
||||
https={https}
|
||||
placeholder="google.com"
|
||||
placeholder={monitorTypeMaps[monitor.type].placeholder || ""}
|
||||
value={monitor.url}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
onChange={(event) => handleChange(event, "url")}
|
||||
error={errors["url"] ? true : false}
|
||||
helperText={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Display name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor.name}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
onChange={(event) => handleChange(event, "name")}
|
||||
error={errors["name"] ? true : false}
|
||||
helperText={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@ -267,7 +281,7 @@ const CreateMonitor = () => {
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor.type === "http"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
onChange={(event) => handleChange(event, "type")}
|
||||
/>
|
||||
{monitor.type === "http" ? (
|
||||
<ButtonGroup sx={{ ml: theme.spacing(16) }}>
|
||||
@ -297,14 +311,23 @@ const CreateMonitor = () => {
|
||||
size="small"
|
||||
value="ping"
|
||||
checked={monitor.type === "ping"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
onChange={(event) => handleChange(event, "type")}
|
||||
/>
|
||||
<Radio
|
||||
id="monitor-checks-docker"
|
||||
title="Docker container monitoring"
|
||||
desc="Check whether your container is running or not."
|
||||
size="small"
|
||||
value="docker"
|
||||
checked={monitor.type === "docker"}
|
||||
onChange={(event) => handleChange(event, "type")}
|
||||
/>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
color={theme.palette.error.contrastText}
|
||||
>
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
@ -323,14 +346,7 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">When there is a new incident,</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
@ -338,34 +354,8 @@ const CreateMonitor = () => {
|
||||
(notification) => notification.type === "email"
|
||||
)}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
onChange={(event) => handleNotifications(event, "email")}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor.notifications.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.spacing(4)}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
@ -378,7 +368,7 @@ const CreateMonitor = () => {
|
||||
label="Check frequency"
|
||||
value={monitor.interval || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
items={SELECT_VALUES}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@ -386,19 +376,19 @@ const CreateMonitor = () => {
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create monitor
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={!Object.values(errors).every((value) => value === undefined)}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create monitor
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateMonitor;
|
||||
export default CreateMonitor;
|
||||
|
@ -61,7 +61,7 @@ const UpBarChart = memo(({ data, type, onBarHover }) => {
|
||||
? { main: theme.palette.success.main, light: theme.palette.success.light }
|
||||
: uptime > 50
|
||||
? { main: theme.palette.warning.main, light: theme.palette.warning.light }
|
||||
: { main: theme.palette.error.text, light: theme.palette.error.light };
|
||||
: { main: theme.palette.error.contrastText, light: theme.palette.error.light };
|
||||
};
|
||||
|
||||
// TODO - REMOVE THIS LATER
|
||||
@ -201,10 +201,10 @@ const DownBarChart = memo(({ data, type, onBarHover }) => {
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette.error.text
|
||||
? theme.palette.error.contrastText
|
||||
: chartHovered
|
||||
? theme.palette.error.light
|
||||
: theme.palette.error.text
|
||||
: theme.palette.error.contrastText
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
@ -246,24 +246,24 @@ const ResponseGaugeChart = ({ data }) => {
|
||||
? {
|
||||
category: "Excellent",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.bg,
|
||||
bg: theme.palette.success.contrastText,
|
||||
}
|
||||
: responseTime <= 500
|
||||
? {
|
||||
category: "Fair",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.bg,
|
||||
bg: theme.palette.success.contrastText,
|
||||
}
|
||||
: responseTime <= 600
|
||||
? {
|
||||
category: "Acceptable",
|
||||
main: theme.palette.warning.main,
|
||||
bg: theme.palette.warning.bg,
|
||||
bg: theme.palette.warning.dark,
|
||||
}
|
||||
: {
|
||||
category: "Poor",
|
||||
main: theme.palette.error.text,
|
||||
bg: theme.palette.error.bg,
|
||||
main: theme.palette.error.contrastText,
|
||||
bg: theme.palette.error.dark,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import Search from "../../../../Components/Inputs/Search";
|
||||
import MemoizedMonitorTable from "../MonitorTable";
|
||||
import { useState } from "react";
|
||||
import useDebounce from "../../../../Utils/debounce";
|
||||
import PropTypes from "prop-types";
|
||||
import { Heading } from "../../../../Components/Heading";
|
||||
|
||||
const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
@ -18,26 +19,15 @@ const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => {
|
||||
return (
|
||||
<Box
|
||||
flex={1}
|
||||
px={theme.spacing(10)}
|
||||
py={theme.spacing(8)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
letterSpacing={-0.2}
|
||||
>
|
||||
Actively monitoring
|
||||
</Typography>
|
||||
<Heading component="h2">Actively monitoring</Heading>
|
||||
|
||||
<Box
|
||||
className="current-monitors-counter"
|
||||
color={theme.palette.text.primary}
|
||||
|
@ -3,6 +3,7 @@ const ROWS_NUMBER = 7;
|
||||
const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
|
||||
|
||||
const TableBodySkeleton = () => {
|
||||
/* TODO Skeleton does not follow light and dark theme */
|
||||
return (
|
||||
<>
|
||||
{ROWS_ARRAY.map((row) => (
|
||||
|
@ -1,4 +1,15 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, memo, useCallback, useRef } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import useUtils from "../../utils";
|
||||
|
||||
import { setRowsPerPage } from "../../../../Features/UI/uiSlice";
|
||||
import { logger } from "../../../../Utils/Logger";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { networkService } from "../../../../main";
|
||||
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
@ -8,106 +19,18 @@ import {
|
||||
TableBody,
|
||||
Paper,
|
||||
Box,
|
||||
TablePagination,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import ActionsMenu from "../actionsMenu";
|
||||
import Host from "../host";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
import { TableBodySkeleton } from "./Skeleton";
|
||||
import BarChart from "../../../../Components/Charts/BarChart";
|
||||
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
|
||||
|
||||
import { setRowsPerPage } from "../../../../Features/UI/uiSlice";
|
||||
import { useState, useEffect, memo, useCallback, useRef } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { logger } from "../../../../Utils/Logger";
|
||||
import Host from "../host";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { networkService } from "../../../../main";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import BarChart from "../../../../Components/Charts/BarChart";
|
||||
import LeftArrowDouble from "../../../../assets/icons/left-arrow-double.svg?react";
|
||||
import RightArrowDouble from "../../../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
|
||||
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
|
||||
import ActionsMenu from "../actionsMenu";
|
||||
import useUtils from "../../utils";
|
||||
import { TableBodySkeleton } from "./Skeleton";
|
||||
|
||||
/**
|
||||
* Component for pagination actions (first, previous, next, last).
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {number} props.count - Total number of items.
|
||||
* @param {number} props.page - Current page number.
|
||||
* @param {number} props.rowsPerPage - Number of rows per page.
|
||||
* @param {function} props.onPageChange - Callback function to handle page change.
|
||||
*
|
||||
* @returns {JSX.Element} Pagination actions component.
|
||||
*/
|
||||
const TablePaginationActions = (props) => {
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
const handleFirstPageButtonClick = (event) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
const handleBackButtonClick = (event) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
const handleNextButtonClick = (event) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
const handleLastPageButtonClick = (event) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: "24px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
<LeftArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
<RightArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
<RightArrowDouble />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
TablePaginationActions.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
import { Pagination } from "../../../Infrastructure/components/TablePagination";
|
||||
|
||||
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
|
||||
const theme = useTheme();
|
||||
@ -117,6 +40,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
|
||||
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.monitors);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [monitorCount, setMonitorCount] = useState(0);
|
||||
@ -150,7 +74,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
|
||||
authToken,
|
||||
teamId: user.teamId,
|
||||
limit: 25,
|
||||
types: ["http", "ping"],
|
||||
types: ["http", "ping", "docker"],
|
||||
status: null,
|
||||
checkOrder: "desc",
|
||||
normalize: true,
|
||||
@ -191,16 +115,6 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
|
||||
prevFilter.current = filter;
|
||||
}, [filter, fetchPage]);
|
||||
|
||||
/**
|
||||
* Helper function to calculate the range of displayed rows.
|
||||
* @returns {string}
|
||||
*/
|
||||
const getRange = () => {
|
||||
let start = page * rowsPerPage + 1;
|
||||
let end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
const handleSort = async (field) => {
|
||||
let order = "";
|
||||
if (sort.field !== field) {
|
||||
@ -360,7 +274,10 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
|
||||
<TableCell>
|
||||
<Host
|
||||
key={monitor._id}
|
||||
params={params}
|
||||
url={params.url}
|
||||
title={params.title}
|
||||
percentageColor={params.percentageColor}
|
||||
percentage={params.percentage}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@ -392,76 +309,13 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(4)}
|
||||
>
|
||||
<Typography
|
||||
px={theme.spacing(2)}
|
||||
variant="body2"
|
||||
sx={{ opacity: 0.7 }}
|
||||
>
|
||||
Showing {getRange()} of {monitorCount} monitor(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={monitorCount}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
rowsPerPageOptions={[5, 10, 15, 25]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
labelRowsPerPage="Rows per page"
|
||||
labelDisplayedRows={({ page, count }) =>
|
||||
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
|
||||
}
|
||||
slotProps={{
|
||||
select: {
|
||||
MenuProps: {
|
||||
keepMounted: true,
|
||||
disableScrollLock: true,
|
||||
PaperProps: {
|
||||
className: "pagination-dropdown",
|
||||
sx: {
|
||||
mt: 0,
|
||||
mb: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
sx: { mt: theme.spacing(-2) },
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
sx: {
|
||||
ml: theme.spacing(4),
|
||||
mr: theme.spacing(12),
|
||||
minWidth: theme.spacing(20),
|
||||
textAlign: "left",
|
||||
"&.Mui-focused > div": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
mt: theme.spacing(6),
|
||||
color: theme.palette.text.secondary,
|
||||
"& svg path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.3,
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Pagination
|
||||
monitorCount={monitorCount}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -25,7 +25,7 @@ const StatusBox = ({ title, value }) => {
|
||||
</Box>
|
||||
);
|
||||
} else if (title === "down") {
|
||||
color = theme.palette.error.text;
|
||||
color = theme.palette.error.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
|
||||
<Arrow />
|
||||
|
@ -109,7 +109,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.text,
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -6,12 +6,14 @@ import PropTypes from "prop-types";
|
||||
*
|
||||
* @component
|
||||
* @param {Object} params - An object containing the following properties:
|
||||
* @param {string} params.url - The URL of the host.
|
||||
* @param {string} params.title - The name of the host.
|
||||
* @param {string} params.percentageColor - The color of the percentage text.
|
||||
* @param {number} params.percentage - The percentage to display.
|
||||
* @returns {React.ElementType} Returns a div element with the host details.
|
||||
*/
|
||||
const Host = ({ params }) => {
|
||||
const Host = ({ url, title, percentageColor, percentage }) => {
|
||||
const noTitle = title === undefined || title === url;
|
||||
return (
|
||||
<Box className="host">
|
||||
<Box
|
||||
@ -32,30 +34,30 @@ const Host = ({ params }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{params.title}
|
||||
{title}
|
||||
</Box>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: params.percentageColor,
|
||||
fontWeight: 500,
|
||||
ml: "15px",
|
||||
}}
|
||||
>
|
||||
{params.percentage}%
|
||||
</Typography>
|
||||
<Box sx={{ opacity: 0.6 }}>{params.url}</Box>
|
||||
{percentageColor && percentage && (
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: percentageColor,
|
||||
fontWeight: 500,
|
||||
ml: "15px",
|
||||
}}
|
||||
>
|
||||
{percentage}%
|
||||
</Typography>
|
||||
)}
|
||||
{!noTitle && <Box sx={{ opacity: 0.6 }}>{url}</Box>}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Host.propTypes = {
|
||||
params: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
percentageColor: PropTypes.string,
|
||||
percentage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
title: PropTypes.string,
|
||||
percentageColor: PropTypes.string,
|
||||
percentage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Host;
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* TODO take these from here and declare using emotion. Plus, the values should live in the theme */
|
||||
.monitors .MuiStack-root > button:not(.MuiIconButton-root) {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
height: var(--env-var-height-2);
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
const useUtils = () => {
|
||||
const theme = useTheme();
|
||||
const determineState = (monitor) => {
|
||||
if (monitor.isActive === false) return "paused";
|
||||
if (monitor?.status === undefined) return "pending";
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
};
|
||||
|
||||
/* TODO Refactor: from here on shouldn't live in a custom hook, but on theme, or constants */
|
||||
const theme = useTheme();
|
||||
|
||||
const statusColor = {
|
||||
up: theme.palette.success.main,
|
||||
down: theme.palette.error.main,
|
||||
@ -28,20 +30,20 @@ const useUtils = () => {
|
||||
};
|
||||
const statusStyles = {
|
||||
up: {
|
||||
backgroundColor: theme.palette.success.bg,
|
||||
background: `linear-gradient(340deg, ${theme.palette.success.light} -60%, ${theme.palette.success.bg} 35%)`,
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
background: `linear-gradient(340deg, ${theme.palette.success.dark} -60%, ${theme.palette.success.light} 35%)`,
|
||||
borderColor: theme.palette.success.light,
|
||||
"& h2": { color: theme.palette.success.main },
|
||||
},
|
||||
down: {
|
||||
backgroundColor: theme.palette.error.bg,
|
||||
background: `linear-gradient(340deg, ${theme.palette.error.light} -60%, ${theme.palette.error.bg} 35%)`,
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
background: `linear-gradient(340deg, ${theme.palette.error.light} -60%, ${theme.palette.error.dark} 35%)`,
|
||||
borderColor: theme.palette.error.light,
|
||||
"& h2": { color: theme.palette.error.main },
|
||||
},
|
||||
paused: {
|
||||
backgroundColor: theme.palette.warning.bg,
|
||||
background: `linear-gradient(340deg, ${theme.palette.warning.light} -60%, ${theme.palette.warning.bg} 35%)`,
|
||||
backgroundColor: theme.palette.warning.dark,
|
||||
background: `linear-gradient(340deg, ${theme.palette.warning.light} -60%, ${theme.palette.warning.dark} 35%)`,
|
||||
borderColor: theme.palette.warning.light,
|
||||
"& h2": { color: theme.palette.warning.main },
|
||||
},
|
||||
@ -54,22 +56,22 @@ const useUtils = () => {
|
||||
};
|
||||
const pagespeedStyles = {
|
||||
up: {
|
||||
bg: theme.palette.success.bg,
|
||||
bg: theme.palette.success.dark,
|
||||
light: theme.palette.success.light,
|
||||
stroke: theme.palette.success.main,
|
||||
},
|
||||
down: {
|
||||
bg: theme.palette.error.bg,
|
||||
bg: theme.palette.error.dark,
|
||||
light: theme.palette.error.light,
|
||||
stroke: theme.palette.error.main,
|
||||
},
|
||||
paused: {
|
||||
bg: theme.palette.warning.bg,
|
||||
bg: theme.palette.warning.dark,
|
||||
light: theme.palette.warning.light,
|
||||
stroke: theme.palette.warning.main,
|
||||
},
|
||||
pending: {
|
||||
bg: theme.palette.warning.bg,
|
||||
bg: theme.palette.warning.dark,
|
||||
light: theme.palette.warning.light,
|
||||
stroke: theme.palette.warning.main,
|
||||
},
|
||||
|
@ -14,7 +14,7 @@ import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../../Monitors/styled";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
@ -321,17 +321,18 @@ const PageSpeedConfigure = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
<TextInput
|
||||
type="url"
|
||||
id="monitor-url"
|
||||
label="URL"
|
||||
placeholder="random.website.com"
|
||||
value={monitor?.url?.replace("http://", "") || ""}
|
||||
value={monitor?.url || ""}
|
||||
onChange={handleChange}
|
||||
error={errors.url}
|
||||
error={errors.url ? true : false}
|
||||
helperText={errors.url}
|
||||
disabled={true}
|
||||
/>
|
||||
<Field
|
||||
<TextInput
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Monitor display name"
|
||||
@ -339,7 +340,8 @@ const PageSpeedConfigure = () => {
|
||||
isOptional={true}
|
||||
value={monitor?.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors.name}
|
||||
error={errors.name ? true : false}
|
||||
helperText={errors.name}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@ -383,7 +385,7 @@ const PageSpeedConfigure = () => {
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<Field
|
||||
<TextInput
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
|
@ -1,28 +1,33 @@
|
||||
import { useState } from "react";
|
||||
import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { createPageSpeed, checkEndpointResolution } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import {
|
||||
createPageSpeed,
|
||||
checkEndpointResolution,
|
||||
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../../Monitors/styled";
|
||||
import Radio from "../../../Components/Inputs/Radio";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { parseDomainName } from "../../../Utils/monitorUtils";
|
||||
import "./index.css";
|
||||
|
||||
const CreatePageSpeed = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
@ -91,6 +96,16 @@ const CreatePageSpeed = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onUrlBlur = (event) => {
|
||||
const { value } = event.target;
|
||||
if (monitor.name === "") {
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
name: parseDomainName(value),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateMonitor = async (event) => {
|
||||
event.preventDefault();
|
||||
//obj to submit
|
||||
@ -105,252 +120,263 @@ const CreatePageSpeed = () => {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validation data." });
|
||||
} else {
|
||||
const checkEndpointAction = await dispatch(
|
||||
checkEndpointResolution({ authToken, monitorURL: form.url })
|
||||
)
|
||||
if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." });
|
||||
setErrors({ url: "The entered URL is not reachable." });
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validation data." });
|
||||
} else {
|
||||
const checkEndpointAction = await dispatch(
|
||||
checkEndpointResolution({ authToken, monitorURL: form.url })
|
||||
);
|
||||
if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
createToast({
|
||||
body: "The endpoint you entered doesn't resolve. Check the URL again.",
|
||||
});
|
||||
setErrors({ url: "The entered URL is not reachable." });
|
||||
return;
|
||||
}
|
||||
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const action = await dispatch(
|
||||
createPageSpeed({ authToken, monitor: form })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
navigate("/pagespeed");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const action = await dispatch(createPageSpeed({ authToken, monitor: form }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
navigate("/pagespeed");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//select values
|
||||
const frequencies = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
{ _id: 20, name: "20 minutes" },
|
||||
{ _id: 60, name: "1 hour" },
|
||||
{ _id: 1440, name: "1 day" },
|
||||
{ _id: 10080, name: "1 week" },
|
||||
];
|
||||
return (
|
||||
<Box
|
||||
className="create-monitor"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "create", path: `/pagespeed/create` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-monitor-form"
|
||||
onSubmit={handleCreateMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography component="h1" variant="h1">
|
||||
<Typography component="span" fontSize="inherit">
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
pagespeed monitor
|
||||
</Typography>
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
Here you can select the URL of the host, together with the type of
|
||||
monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Field
|
||||
type={"url"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
https={https}
|
||||
placeholder="google.com"
|
||||
value={monitor.url}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Display name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor.name}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Checks to perform</Typography>
|
||||
<Typography component="p">
|
||||
You can always add or remove checks after adding your site.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Radio
|
||||
id="monitor-checks-http"
|
||||
title="Website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor.type === "pagespeed"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
HTTPS
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(!https).toString()}
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
>
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p">
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">When there is a new incident,</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={monitor.notifications.some(
|
||||
(notification) => notification.type === "email"
|
||||
)}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor.notifications.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.spacing(4)}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
id="monitor-interval"
|
||||
label="Check frequency"
|
||||
value={monitor.interval || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create monitor
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
//select values
|
||||
const frequencies = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
{ _id: 20, name: "20 minutes" },
|
||||
{ _id: 60, name: "1 hour" },
|
||||
{ _id: 1440, name: "1 day" },
|
||||
{ _id: 10080, name: "1 week" },
|
||||
];
|
||||
return (
|
||||
<Box
|
||||
className="create-monitor"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "create", path: `/pagespeed/create` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-monitor-form"
|
||||
onSubmit={handleCreateMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
>
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
pagespeed monitor
|
||||
</Typography>
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
Here you can select the URL of the host, together with the type of monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<TextInput
|
||||
type={"url"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
startAdornment={<HttpAdornment https={https} />}
|
||||
placeholder="google.com"
|
||||
value={monitor.url}
|
||||
onChange={handleChange}
|
||||
onBlur={onUrlBlur}
|
||||
error={errors["url"] ? true : false}
|
||||
helperText={errors["url"]}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Display name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor.name}
|
||||
onChange={handleChange}
|
||||
error={errors["name"] ? true : false}
|
||||
helperText={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Checks to perform</Typography>
|
||||
<Typography component="p">
|
||||
You can always add or remove checks after adding your site.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Radio
|
||||
id="monitor-checks-http"
|
||||
title="Website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor.type === "pagespeed"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
HTTPS
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(!https).toString()}
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.contrastText}
|
||||
>
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p">
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">When there is a new incident,</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={monitor.notifications.some(
|
||||
(notification) => notification.type === "email"
|
||||
)}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor.notifications.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<TextInput
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.spacing(4)}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
id="monitor-interval"
|
||||
label="Check frequency"
|
||||
value={monitor.interval || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create monitor
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePageSpeed;
|
||||
|
@ -127,22 +127,22 @@ const PieChart = ({ audits }) => {
|
||||
return {
|
||||
stroke: theme.palette.success.main,
|
||||
strokeBg: theme.palette.success.light,
|
||||
text: theme.palette.success.text,
|
||||
bg: theme.palette.success.bg,
|
||||
text: theme.palette.success.contrastText,
|
||||
bg: theme.palette.success.dark,
|
||||
};
|
||||
else if (value >= 50 && value < 90)
|
||||
return {
|
||||
stroke: theme.palette.warning.main,
|
||||
strokeBg: theme.palette.warning.light,
|
||||
text: theme.palette.warning.text,
|
||||
bg: theme.palette.warning.bg,
|
||||
text: theme.palette.warning.contrastText,
|
||||
bg: theme.palette.warning.dark,
|
||||
};
|
||||
else if (value >= 0 && value < 50)
|
||||
return {
|
||||
stroke: theme.palette.error.text,
|
||||
stroke: theme.palette.error.contrastText,
|
||||
strokeBg: theme.palette.error.light,
|
||||
text: theme.palette.error.text,
|
||||
bg: theme.palette.error.bg,
|
||||
text: theme.palette.error.contrastText,
|
||||
bg: theme.palette.error.dark,
|
||||
};
|
||||
return {
|
||||
stroke: theme.palette.unresolved.main,
|
||||
|
@ -362,7 +362,7 @@ const PageSpeedDetails = ({ isAdmin }) => {
|
||||
: score >= 50
|
||||
? theme.palette.warning.main
|
||||
: score >= 0
|
||||
? theme.palette.error.text
|
||||
? theme.palette.error.contrastText
|
||||
: theme.palette.unresolved.main;
|
||||
|
||||
// Find the position where the number ends and the unit begins
|
||||
|
@ -8,7 +8,6 @@ import "./index.css";
|
||||
import { useNavigate } from "react-router";
|
||||
import PropTypes from "prop-types";
|
||||
import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import Greeting from "../../Utils/greeting";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Card from "./card";
|
||||
import { networkService } from "../../main";
|
||||
@ -82,11 +81,10 @@ const PageSpeed = ({ isAdmin }) => {
|
||||
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
justifyContent="end"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Greeting type="pagespeed" />
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
|
@ -1 +0,0 @@
|
||||
#Pages Folder
|
@ -1,6 +1,6 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Link from "../../Components/Link";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import { logger } from "../../Utils/Logger";
|
||||
@ -240,13 +240,14 @@ const Settings = ({ isAdmin }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
<TextInput
|
||||
id="ttl"
|
||||
label="The days you want to keep monitoring history."
|
||||
optionalLabel="0 for infinite"
|
||||
value={form.ttl}
|
||||
onChange={handleChange}
|
||||
error={errors.ttl}
|
||||
error={errors.ttl ? true : false}
|
||||
helperText={errors.ttl}
|
||||
/>
|
||||
<Box>
|
||||
<Typography>Clear all stats. This is irreversible.</Typography>
|
||||
@ -348,14 +349,14 @@ const Settings = ({ isAdmin }) => {
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime {version}</Typography>
|
||||
<Typography component="h2">Checkmate {version}</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/bluewave-uptime"
|
||||
label="https://github.com/bluewave-labs/bluewave-uptime"
|
||||
url="https://github.com/bluewave-labs/checkmate"
|
||||
label="https://github.com/bluewave-labs/checkmate"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
|
@ -44,6 +44,14 @@ class Logger {
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
if (logLevel === "debug") {
|
||||
this.error = console.error.bind(console);
|
||||
this.warn = console.warn.bind(console);
|
||||
this.info = console.info.bind(console);
|
||||
this.log = console.log.bind(console);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
|
249
Client/src/Utils/Theme/constants.js
Normal file
249
Client/src/Utils/Theme/constants.js
Normal file
@ -0,0 +1,249 @@
|
||||
const typographyBase = 14;
|
||||
|
||||
/* TODO
|
||||
Check for px in codebase. All font sizes should be in REM and should live here.
|
||||
Rest should be checked on each case
|
||||
*/
|
||||
const typographyLevels = {
|
||||
base: typographyBase,
|
||||
xs: `${(typographyBase - 4) / 16}rem`,
|
||||
s: `${(typographyBase - 2) / 16}rem`,
|
||||
m: `${typographyBase / 16}rem`,
|
||||
l: `${(typographyBase + 2) / 16}rem`,
|
||||
xl: `${(typographyBase + 10) / 16}rem`,
|
||||
};
|
||||
|
||||
/* TODO Review color palette and semantic colors */
|
||||
const paletteColors = {
|
||||
white: "#FFFFFF",
|
||||
gray50: "#FEFEFE",
|
||||
gray60: "#FEFDFE",
|
||||
gray70: "#FDFDFD",
|
||||
gray80: "#FDFCFD",
|
||||
gray90: "#FCFCFD",
|
||||
gray100: "#F4F4F4",
|
||||
gray150: "#EFEFEF",
|
||||
gray200: "#E3E3E3",
|
||||
gray300: "#A2A3A3",
|
||||
gray500: "#838C99",
|
||||
gray600: "#454546",
|
||||
gray750: "#36363E",
|
||||
gray800: "#2D2D33",
|
||||
gray850: "#131315",
|
||||
gray860: "#111113",
|
||||
gray870: "#0F0F11",
|
||||
gray880: "#0C0C0E",
|
||||
gray890: "#09090B",
|
||||
blueGray20: "#E8F0FE",
|
||||
blueGray150: "#667085",
|
||||
blueGray200: "#475467",
|
||||
blueGray400: "#344054",
|
||||
blueGray900: "#1c2130",
|
||||
blueBlueWave: "#1570EF",
|
||||
blue700: "#4E5BA6",
|
||||
purple300: "#664EFF",
|
||||
purple400: "#3A1BFF",
|
||||
green50: "#D4F4E1",
|
||||
green150: "#45BB7A",
|
||||
green400: "#079455",
|
||||
green800: "#1C4428",
|
||||
green900: "#12261E",
|
||||
red50: "#F9ECED",
|
||||
red100: "#FBD1D1",
|
||||
red200: "#F04438",
|
||||
red300: "#D32F2F",
|
||||
red700: "#542426",
|
||||
red800: "#301A1F",
|
||||
orange50: "#FEF8EA",
|
||||
orange100: "#FFECBC",
|
||||
orange300: "#FDB022",
|
||||
orange400: "#FF9F00",
|
||||
orange500: "#E88C30",
|
||||
orange600: "#DC6803",
|
||||
orange800: "#624711",
|
||||
};
|
||||
|
||||
const semanticColors = {
|
||||
primary: {
|
||||
main: {
|
||||
light: paletteColors.blueBlueWave,
|
||||
dark: paletteColors.blueBlueWave,
|
||||
},
|
||||
// TODO we dont have primary light, dark or contrast text
|
||||
},
|
||||
secondary: {
|
||||
main: {
|
||||
light: paletteColors.gray100,
|
||||
dark: paletteColors.gray800,
|
||||
},
|
||||
contrastText: {
|
||||
light: paletteColors.blueGray200,
|
||||
dark: paletteColors.blueGray200,
|
||||
},
|
||||
light: {
|
||||
light: paletteColors.gray200,
|
||||
dark: paletteColors.gray200,
|
||||
},
|
||||
dark: {
|
||||
light: paletteColors.gray200,
|
||||
dark: paletteColors.gray200,
|
||||
},
|
||||
},
|
||||
success: {
|
||||
main: {
|
||||
light: paletteColors.green400,
|
||||
dark: paletteColors.green400,
|
||||
},
|
||||
contrastText: {
|
||||
light: paletteColors.green50,
|
||||
dark: paletteColors.green900,
|
||||
},
|
||||
light: {
|
||||
light: paletteColors.green50,
|
||||
dark: paletteColors.green800,
|
||||
},
|
||||
dark: {
|
||||
light: paletteColors.green400,
|
||||
dark: paletteColors.green900,
|
||||
},
|
||||
},
|
||||
error: {
|
||||
main: {
|
||||
light: paletteColors.red300,
|
||||
dark: paletteColors.red300,
|
||||
},
|
||||
contrastText: {
|
||||
light: paletteColors.gray50,
|
||||
dark: paletteColors.gray50,
|
||||
},
|
||||
light: {
|
||||
light: paletteColors.red100,
|
||||
dark: paletteColors.red700,
|
||||
},
|
||||
dark: {
|
||||
light: paletteColors.red50,
|
||||
dark: paletteColors.red800,
|
||||
},
|
||||
},
|
||||
warning: {
|
||||
main: {
|
||||
light: paletteColors.orange300,
|
||||
dark: paletteColors.orange400,
|
||||
},
|
||||
contrastText: {
|
||||
light: paletteColors.orange600,
|
||||
dark: paletteColors.orange500,
|
||||
},
|
||||
light: {
|
||||
light: paletteColors.orange100,
|
||||
dark: paletteColors.orange800,
|
||||
},
|
||||
dark: {
|
||||
light: paletteColors.orange50,
|
||||
dark: paletteColors.gray800,
|
||||
},
|
||||
},
|
||||
/* From this part on, try to create semantic structure, not feature based structure */
|
||||
gradient: {
|
||||
color1: {
|
||||
light: paletteColors.gray90,
|
||||
dark: paletteColors.gray890,
|
||||
},
|
||||
color2: {
|
||||
light: paletteColors.gray80,
|
||||
dark: paletteColors.gray880,
|
||||
},
|
||||
color3: {
|
||||
light: paletteColors.gray70,
|
||||
dark: paletteColors.gray870,
|
||||
},
|
||||
color4: {
|
||||
light: paletteColors.gray60,
|
||||
dark: paletteColors.gray860,
|
||||
},
|
||||
color5: {
|
||||
light: paletteColors.gray50,
|
||||
dark: paletteColors.gray850,
|
||||
},
|
||||
},
|
||||
background: {
|
||||
main: {
|
||||
light: paletteColors.white,
|
||||
dark: paletteColors.gray890,
|
||||
},
|
||||
alt: {
|
||||
light: paletteColors.gray90,
|
||||
dark: paletteColors.gray890,
|
||||
},
|
||||
fill: {
|
||||
light: paletteColors.gray100,
|
||||
dark: paletteColors.gray800,
|
||||
},
|
||||
accent: {
|
||||
light: paletteColors.gray100,
|
||||
dark: paletteColors.gray850,
|
||||
},
|
||||
},
|
||||
text: {
|
||||
primary: {
|
||||
light: paletteColors.blueGray900,
|
||||
dark: paletteColors.gray100,
|
||||
},
|
||||
secondary: {
|
||||
light: paletteColors.blueGray400,
|
||||
dark: paletteColors.gray200,
|
||||
},
|
||||
tertiary: {
|
||||
light: paletteColors.blueGray200,
|
||||
dark: paletteColors.gray300,
|
||||
},
|
||||
accent: {
|
||||
light: paletteColors.gray500,
|
||||
dark: paletteColors.gray500,
|
||||
},
|
||||
},
|
||||
border: {
|
||||
light: {
|
||||
light: paletteColors.gray200,
|
||||
dark: paletteColors.gray800,
|
||||
/* TODO this should live in a different key (border.disabled.light and .dark) */
|
||||
disabled: paletteColors.gray150,
|
||||
},
|
||||
dark: {
|
||||
light: paletteColors.gray200,
|
||||
dark: paletteColors.gray750,
|
||||
/* TODO this should live in a different key (border.disabled.light and .dark) */
|
||||
disabled: paletteColors.gray150,
|
||||
},
|
||||
},
|
||||
unresolved: {
|
||||
main: {
|
||||
light: paletteColors.blue700,
|
||||
dark: paletteColors.purple300,
|
||||
},
|
||||
light: {
|
||||
light: paletteColors.blueGray20,
|
||||
dark: paletteColors.purple400,
|
||||
},
|
||||
bg: {
|
||||
light: paletteColors.gray100,
|
||||
dark: paletteColors.gray100,
|
||||
},
|
||||
},
|
||||
other: {
|
||||
icon: {
|
||||
light: paletteColors.blueGray150,
|
||||
},
|
||||
line: {
|
||||
light: paletteColors.gray200,
|
||||
},
|
||||
grid: {
|
||||
light: paletteColors.gray300,
|
||||
dark: paletteColors.gray600,
|
||||
},
|
||||
autofill: {
|
||||
light: paletteColors.blueGray20,
|
||||
},
|
||||
},
|
||||
};
|
||||
export { typographyLevels, semanticColors as colors };
|
@ -1,255 +1,111 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
import { baseTheme } from "./globalTheme";
|
||||
import { colors } from "./constants";
|
||||
|
||||
const text = {
|
||||
primary: "#fafafa",
|
||||
secondary: "#e6e6e6",
|
||||
tertiary: "#a1a1aa",
|
||||
accent: "#8e8e8f",
|
||||
disabled: "rgba(172, 172, 172, 0.3)",
|
||||
};
|
||||
const background = {
|
||||
main: "#151518",
|
||||
alt: "#09090b",
|
||||
fill: "#2D2D33",
|
||||
accent: "#18181a",
|
||||
};
|
||||
const border = { light: "#27272a", dark: "#36363e" };
|
||||
const {
|
||||
primary,
|
||||
secondary,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
gradient: {
|
||||
color1: { dark: color1 },
|
||||
color2: { dark: color2 },
|
||||
color3: { dark: color3 },
|
||||
color4: { dark: color4 },
|
||||
color5: { dark: color5 },
|
||||
},
|
||||
background,
|
||||
text,
|
||||
border,
|
||||
unresolved,
|
||||
other,
|
||||
} = colors;
|
||||
|
||||
const fontFamilyDefault =
|
||||
'"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif';
|
||||
const shadow =
|
||||
"0px 4px 24px -4px rgba(255, 255, 255, 0.03), 0px 3px 3px -3px rgba(255, 255, 255, 0.01)";
|
||||
const palette = {
|
||||
action: {
|
||||
disabled: border.dark.disabled,
|
||||
},
|
||||
primary: { main: primary.main.dark },
|
||||
secondary: {
|
||||
main: secondary.main.dark,
|
||||
contrastText: secondary.contrastText.dark,
|
||||
light: secondary.dark.dark,
|
||||
dark: secondary.dark.dark,
|
||||
},
|
||||
success: {
|
||||
main: success.main.dark,
|
||||
contrastText: success.contrastText.dark,
|
||||
light: success.light.dark,
|
||||
dark: success.dark.dark,
|
||||
},
|
||||
error: {
|
||||
main: error.main.dark,
|
||||
contrastText: error.contrastText.dark,
|
||||
light: error.light.dark,
|
||||
dark: error.dark.dark,
|
||||
},
|
||||
warning: {
|
||||
main: warning.main.dark,
|
||||
light: warning.light.dark,
|
||||
contrastText: warning.contrastText.dark,
|
||||
dark: warning.dark.dark,
|
||||
},
|
||||
/* From this part on, try to create semantic structure, not feature based structure */
|
||||
percentage: {
|
||||
uptimePoor: error.main.dark,
|
||||
uptimeFair: warning.contrastText.dark,
|
||||
uptimeGood: warning.main.dark /* Change for a success color? ?*/,
|
||||
uptimeExcellent: success.main.dark,
|
||||
},
|
||||
unresolved: {
|
||||
main: unresolved.main.dark,
|
||||
light: unresolved.light.dark,
|
||||
bg: unresolved.bg.dark,
|
||||
},
|
||||
divider: border.light.dark,
|
||||
other: {
|
||||
icon: text.secondary.dark,
|
||||
line: border.light.dark,
|
||||
fill: background.accent.dark,
|
||||
grid: other.grid.dark,
|
||||
autofill: secondary.main.dark,
|
||||
},
|
||||
gradient: {
|
||||
color1,
|
||||
color2,
|
||||
color3,
|
||||
color4,
|
||||
color5,
|
||||
},
|
||||
text: {
|
||||
primary: text.primary.dark,
|
||||
secondary: text.secondary.dark,
|
||||
tertiary: text.tertiary.dark,
|
||||
accent: text.accent.dark,
|
||||
},
|
||||
background: {
|
||||
main: background.main.dark,
|
||||
alt: background.alt.dark,
|
||||
fill: background.fill.dark,
|
||||
accent: background.accent.dark,
|
||||
},
|
||||
border: {
|
||||
light: border.light.dark,
|
||||
dark: border.dark.dark,
|
||||
},
|
||||
info: {
|
||||
text: text.primary.dark,
|
||||
main: text.secondary.dark,
|
||||
bg: background.main.dark,
|
||||
light: background.main.dark,
|
||||
border: border.light.dark,
|
||||
},
|
||||
};
|
||||
|
||||
const darkTheme = createTheme({
|
||||
typography: {
|
||||
fontFamily: fontFamilyDefault,
|
||||
fontSize: 13,
|
||||
h1: { fontSize: 22, color: text.primary, fontWeight: 500 },
|
||||
h2: { fontSize: 14.5, color: text.secondary, fontWeight: 400 },
|
||||
body1: { fontSize: 13, color: text.tertiary, fontWeight: 400 },
|
||||
body2: { fontSize: 12, color: text.tertiary, fontWeight: 400 },
|
||||
},
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: { main: "#1570ef" },
|
||||
secondary: { main: "#2D2D33" },
|
||||
text: text,
|
||||
background: background,
|
||||
border: border,
|
||||
info: {
|
||||
text: text.primary,
|
||||
main: text.secondary,
|
||||
bg: background.main,
|
||||
light: background.main,
|
||||
border: border.light,
|
||||
},
|
||||
success: {
|
||||
text: "#079455",
|
||||
main: "#45bb7a",
|
||||
light: "#1c4428",
|
||||
bg: "#12261e",
|
||||
},
|
||||
error: {
|
||||
text: "#f04438",
|
||||
main: "#d32f2f",
|
||||
light: "#542426",
|
||||
bg: "#301a1f",
|
||||
dark: "#932020",
|
||||
border: "#f04438",
|
||||
},
|
||||
warning: {
|
||||
text: "#e88c30",
|
||||
main: "#FF9F00",
|
||||
light: "#624711",
|
||||
bg: "#262115",
|
||||
border: "#e88c30",
|
||||
},
|
||||
percentage: {
|
||||
uptimePoor: "#d32f2f",
|
||||
uptimeFair: "#e88c30",
|
||||
uptimeGood: "#ffd600",
|
||||
uptimeExcellent: "#079455",
|
||||
},
|
||||
unresolved: { main: "#664eff", light: "#3a1bff", bg: "#f2f4f7" },
|
||||
divider: border.light,
|
||||
other: {
|
||||
icon: "#e6e6e6",
|
||||
line: "#27272a",
|
||||
fill: "#18181a",
|
||||
grid: "#454546",
|
||||
autofill: "#2d2d33",
|
||||
},
|
||||
},
|
||||
spacing: 2,
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
backgroundImage:
|
||||
"radial-gradient(circle, #09090b, #0c0c0e, #0f0f11, #111113, #131315, #131315, #131315, #131315, #111113, #0f0f11, #0c0c0e, #09090b)",
|
||||
lineHeight: "inherit",
|
||||
paddingLeft: "calc(100vw - 100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
disableRipple: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
variants: [
|
||||
{
|
||||
props: (props) => props.variant === "group",
|
||||
style: {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) => props.variant === "group" && props.filled === "true",
|
||||
style: {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) =>
|
||||
props.variant === "contained" && props.color === "secondary",
|
||||
style: {
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
},
|
||||
],
|
||||
fontWeight: 400,
|
||||
borderRadius: 4,
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 4,
|
||||
transition: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: border.light,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
marginTop: 4,
|
||||
padding: 0,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: border.light,
|
||||
borderRadius: 4,
|
||||
boxShadow: shadow,
|
||||
backgroundColor: background.main,
|
||||
backgroundImage: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiList: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 4,
|
||||
backgroundColor: "inherit",
|
||||
padding: "4px 6px",
|
||||
color: text.secondary,
|
||||
fontSize: 13,
|
||||
margin: 2,
|
||||
minWidth: 100,
|
||||
"&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible":
|
||||
{
|
||||
backgroundColor: background.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderBottomColor: border.light,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableHead: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: background.accent,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPagination: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: border.light,
|
||||
"& button": {
|
||||
color: text.tertiary,
|
||||
borderRadius: 4,
|
||||
},
|
||||
"& li:first-of-type button, & li:last-of-type button": {
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: border.light,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaginationItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:not(.MuiPaginationItem-ellipsis):hover, &.Mui-selected": {
|
||||
backgroundColor: background.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSkeleton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: "#151518",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
borderThick: 2,
|
||||
boxShadow: shadow,
|
||||
},
|
||||
palette,
|
||||
...baseTheme(palette),
|
||||
});
|
||||
|
||||
export default darkTheme;
|
||||
|
273
Client/src/Utils/Theme/globalTheme.js
Normal file
273
Client/src/Utils/Theme/globalTheme.js
Normal file
@ -0,0 +1,273 @@
|
||||
import { typographyLevels } from "./constants";
|
||||
|
||||
const fontFamilyPrimary = '"Inter" , sans-serif';
|
||||
// const fontFamilySecondary = '"Avenir", sans-serif';
|
||||
|
||||
/* TODO take the color out from here */
|
||||
const shadow =
|
||||
"0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)";
|
||||
|
||||
const baseTheme = (palette) => ({
|
||||
typography: {
|
||||
fontFamily: fontFamilyPrimary,
|
||||
fontSize: 14,
|
||||
h1: {
|
||||
fontSize: typographyLevels.xl,
|
||||
color: palette.text.primary,
|
||||
fontWeight: 500,
|
||||
},
|
||||
h2: {
|
||||
fontSize: typographyLevels.l,
|
||||
color: palette.text.secondary,
|
||||
fontWeight: 400,
|
||||
},
|
||||
body1: {
|
||||
fontSize: typographyLevels.m,
|
||||
color: palette.text.tertiary,
|
||||
fontWeight: 400,
|
||||
},
|
||||
body2: {
|
||||
fontSize: typographyLevels.s,
|
||||
color: palette.text.tertiary,
|
||||
fontWeight: 400,
|
||||
},
|
||||
},
|
||||
/* TODO we can skip using the callback functions on the next lines since we are already accessing it on line 10. That was the last thing I managed to do, so we are sort of doing it twice*/
|
||||
|
||||
spacing: 2,
|
||||
/* TODO All these should live inside of a component*/
|
||||
components: {
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
disableRipple: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
variants: [
|
||||
{
|
||||
props: (props) => props.variant === "group",
|
||||
style: {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) => props.variant === "group" && props.filled === "true",
|
||||
style: {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) =>
|
||||
props.variant === "contained" && props.color === "secondary",
|
||||
style: {
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) => {
|
||||
return (
|
||||
props.variant === "contained" &&
|
||||
props.disabled &&
|
||||
props.classes.loadingIndicator === undefined // Do not apply to loading button
|
||||
);
|
||||
},
|
||||
style: {
|
||||
backgroundColor: `${theme.palette.secondary.main} !important`,
|
||||
color: `${theme.palette.secondary.contrastText} !important`,
|
||||
},
|
||||
},
|
||||
],
|
||||
fontWeight: 400,
|
||||
borderRadius: 4,
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
"&.MuiLoadingButton-root": {
|
||||
"&:disabled": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
},
|
||||
"&.MuiLoadingButton-loading": {
|
||||
"& .MuiLoadingButton-label": {
|
||||
color: "transparent",
|
||||
},
|
||||
"& .MuiLoadingButton-loadingIndicator": {
|
||||
color: "inherit",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
padding: 4,
|
||||
transition: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => {
|
||||
return {
|
||||
marginTop: 4,
|
||||
padding: 0,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: 4,
|
||||
boxShadow: shadow,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
backgroundImage: "none",
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiList: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
borderRadius: 4,
|
||||
backgroundColor: "inherit",
|
||||
padding: "4px 6px",
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 13,
|
||||
margin: 2,
|
||||
minWidth: 100,
|
||||
"&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible":
|
||||
{
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
borderBottomColor: theme.palette.border.light,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTableHead: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiPagination: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
"& button": {
|
||||
color: theme.palette.text.tertiary,
|
||||
borderRadius: 4,
|
||||
},
|
||||
"& li:first-of-type button, & li:last-of-type button": {
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiPaginationItem: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
"&:not(.MuiPaginationItem-ellipsis):hover, &.Mui-selected": {
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiSkeleton: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
backgroundColor: theme.palette.unresolved.bg,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiInputBase-input": {
|
||||
padding: ".75em",
|
||||
minHeight: "var(--env-var-height-2)",
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
fontWeight: 400,
|
||||
color: palette.text.secondary,
|
||||
"&.Mui-disabled": {
|
||||
opacity: 0.3,
|
||||
WebkitTextFillColor: "unset",
|
||||
},
|
||||
},
|
||||
"& .MuiInputBase-input.MuiOutlinedInput-input": {
|
||||
padding: "0 var(--env-var-spacing-1-minus) !important",
|
||||
},
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: 4,
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
"& .MuiFormHelperText-root": {
|
||||
color: palette.error.main,
|
||||
opacity: 0.8,
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
|
||||
marginLeft: 0,
|
||||
},
|
||||
"& .MuiFormHelperText-root.Mui-error": {
|
||||
opacity: 0.8,
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
color: palette.error.main,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
borderThick: 2,
|
||||
boxShadow: shadow,
|
||||
},
|
||||
});
|
||||
|
||||
export { baseTheme };
|
@ -1,252 +1,118 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
import { baseTheme } from "./globalTheme";
|
||||
import { colors } from "./constants";
|
||||
|
||||
const text = {
|
||||
primary: "#1c2130",
|
||||
secondary: "#344054",
|
||||
tertiary: "#475467",
|
||||
accent: "#838c99",
|
||||
/*
|
||||
TODO
|
||||
Next step: check if all keys here are being used in the codebase. e.g.: Search codebase for palette.primary; also check for destructuring palette ('= theme.palette')
|
||||
*/
|
||||
|
||||
const {
|
||||
primary,
|
||||
secondary,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
gradient: {
|
||||
color1: { light: color1 },
|
||||
color2: { light: color2 },
|
||||
color3: { light: color3 },
|
||||
color4: { light: color4 },
|
||||
color5: { light: color5 },
|
||||
},
|
||||
background,
|
||||
text,
|
||||
border,
|
||||
unresolved,
|
||||
other,
|
||||
} = colors;
|
||||
|
||||
const palette = {
|
||||
/* TODO check if we need the addition of a new color gray150 for this. Also, this color would probably fit for primary contrastText */
|
||||
action: {
|
||||
disabled: border.light.disabled,
|
||||
},
|
||||
primary: { main: primary.main.light },
|
||||
secondary: {
|
||||
main: secondary.main.light,
|
||||
contrastText: secondary.contrastText.light,
|
||||
light: secondary.dark.light,
|
||||
dark: secondary.dark.light,
|
||||
},
|
||||
success: {
|
||||
main: success.main.light,
|
||||
contrastText: success.contrastText.light,
|
||||
light: success.light.light,
|
||||
dark: success.dark.light,
|
||||
},
|
||||
error: {
|
||||
main: error.main.light,
|
||||
contrastText: error.contrastText.light,
|
||||
light: error.light.light,
|
||||
dark: error.dark.light,
|
||||
},
|
||||
warning: {
|
||||
main: warning.main.light,
|
||||
light: warning.light.light,
|
||||
contrastText: warning.contrastText.light,
|
||||
dark: warning.dark.light,
|
||||
},
|
||||
/* From this part on, try to create semantic structure, not feature based structure */
|
||||
percentage: {
|
||||
uptimePoor: error.main.light,
|
||||
uptimeFair: warning.contrastText.light,
|
||||
uptimeGood: warning.main.light /* Change for a success color? */,
|
||||
uptimeExcellent: success.main.light,
|
||||
},
|
||||
unresolved: {
|
||||
main: unresolved.main.light,
|
||||
light: unresolved.light.light,
|
||||
bg: unresolved.bg.light,
|
||||
},
|
||||
divider: border.light.light,
|
||||
other: {
|
||||
icon: other.icon.light,
|
||||
line: other.line.light,
|
||||
fill: secondary.dark.light,
|
||||
grid: other.grid.light,
|
||||
autofill: other.autofill.light,
|
||||
},
|
||||
gradient: {
|
||||
color1,
|
||||
color2,
|
||||
color3,
|
||||
color4,
|
||||
color5,
|
||||
},
|
||||
text: {
|
||||
primary: text.primary.light,
|
||||
secondary: text.secondary.light,
|
||||
tertiary: text.tertiary.light,
|
||||
accent: text.accent.light,
|
||||
},
|
||||
background: {
|
||||
main: background.main.light,
|
||||
alt: background.alt.light,
|
||||
fill: background.fill.light,
|
||||
accent: background.accent.light,
|
||||
},
|
||||
border: {
|
||||
light: border.light.light,
|
||||
dark: border.dark.light,
|
||||
},
|
||||
info: {
|
||||
text: text.primary.light,
|
||||
main: text.tertiary.light,
|
||||
bg: background.main.light,
|
||||
light: background.main.light,
|
||||
border: border.dark.light,
|
||||
},
|
||||
};
|
||||
const background = {
|
||||
main: "#FFFFFF",
|
||||
alt: "#FCFCFD",
|
||||
fill: "#F4F4F4",
|
||||
accent: "#f9fafb",
|
||||
};
|
||||
const border = { light: "#eaecf0", dark: "#d0d5dd" };
|
||||
|
||||
const fontFamilyDefault =
|
||||
'"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif';
|
||||
const shadow =
|
||||
"0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)";
|
||||
|
||||
/* TODO I figured out we could have just one theme by passing mode as parameter for theme function. Implement later */
|
||||
const lightTheme = createTheme({
|
||||
typography: {
|
||||
fontFamily: fontFamilyDefault,
|
||||
fontSize: 13,
|
||||
h1: { fontSize: 22, color: text.primary, fontWeight: 500 },
|
||||
h2: { fontSize: 14.5, color: text.secondary, fontWeight: 400 },
|
||||
body1: { fontSize: 13, color: text.tertiary, fontWeight: 400 },
|
||||
body2: { fontSize: 12, color: text.tertiary, fontWeight: 400 },
|
||||
},
|
||||
palette: {
|
||||
primary: { main: "#1570EF" },
|
||||
secondary: { main: "#F4F4F4", dark: "#e3e3e3", contrastText: "#475467" },
|
||||
text: text,
|
||||
background: background,
|
||||
border: border,
|
||||
info: {
|
||||
text: text.primary,
|
||||
main: text.tertiary,
|
||||
bg: background.main,
|
||||
light: background.main,
|
||||
border: border.dark,
|
||||
},
|
||||
success: {
|
||||
text: "#079455",
|
||||
main: "#17b26a",
|
||||
light: "#d4f4e1",
|
||||
bg: "#ecfdf3",
|
||||
},
|
||||
error: {
|
||||
text: "#f04438",
|
||||
main: "#d32f2f",
|
||||
light: "#fbd1d1",
|
||||
bg: "#f9eced",
|
||||
border: "#f04438",
|
||||
},
|
||||
warning: {
|
||||
text: "#DC6803",
|
||||
main: "#fdb022",
|
||||
light: "#ffecbc",
|
||||
bg: "#fef8ea",
|
||||
border: "#fec84b",
|
||||
},
|
||||
percentage: {
|
||||
uptimePoor: "#d32f2f",
|
||||
uptimeFair: "#ec8013",
|
||||
uptimeGood: "#ffb800",
|
||||
uptimeExcellent: "#079455",
|
||||
},
|
||||
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
|
||||
divider: border.light,
|
||||
other: {
|
||||
icon: "#667085",
|
||||
line: "#d6d9dd",
|
||||
fill: "#e3e3e3",
|
||||
grid: "#a2a3a3",
|
||||
autofill: "#e8f0fe",
|
||||
},
|
||||
},
|
||||
spacing: 2,
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
backgroundImage:
|
||||
"radial-gradient(circle, #fcfcfd, #fdfcfd, #fdfdfd, #fefdfe, #fefefe, #fefefe, #fefefe, #fefefe, #fefdfe, #fdfdfd, #fdfcfd, #fcfcfd)",
|
||||
lineHeight: "inherit",
|
||||
paddingLeft: "calc(100vw - 100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
disableRipple: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
variants: [
|
||||
{
|
||||
props: (props) => props.variant === "group",
|
||||
style: {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) => props.variant === "group" && props.filled === "true",
|
||||
style: {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) =>
|
||||
props.variant === "contained" && props.color === "secondary",
|
||||
style: {
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
},
|
||||
],
|
||||
fontWeight: 400,
|
||||
borderRadius: 4,
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 4,
|
||||
transition: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: background.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
marginTop: 4,
|
||||
padding: 0,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: border.light,
|
||||
borderRadius: 4,
|
||||
boxShadow: shadow,
|
||||
backgroundColor: background.main,
|
||||
backgroundImage: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiList: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 4,
|
||||
backgroundColor: "inherit",
|
||||
padding: "4px 6px",
|
||||
color: text.secondary,
|
||||
fontSize: 13,
|
||||
margin: 2,
|
||||
minWidth: 100,
|
||||
"&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible":
|
||||
{
|
||||
backgroundColor: background.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderBottomColor: border.light,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableHead: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: background.accent,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPagination: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: border.light,
|
||||
"& button": {
|
||||
color: text.tertiary,
|
||||
borderRadius: 4,
|
||||
},
|
||||
"& li:first-of-type button, & li:last-of-type button": {
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: border.light,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaginationItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:not(.MuiPaginationItem-ellipsis):hover, &.Mui-selected": {
|
||||
backgroundColor: background.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSkeleton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: "#f2f4f7",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
borderThick: 2,
|
||||
boxShadow: shadow,
|
||||
},
|
||||
palette,
|
||||
...baseTheme(palette),
|
||||
});
|
||||
|
||||
export default lightTheme;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { capitalizeFirstLetter } from "./stringUtils";
|
||||
|
||||
/**
|
||||
* Helper function to get duration since last check or the last date checked
|
||||
* @param {Array} checks Array of check objects.
|
||||
@ -15,3 +17,22 @@ export const getLastChecked = (checks, duration = true) => {
|
||||
}
|
||||
return new Date() - new Date(checks[0].createdAt);
|
||||
};
|
||||
|
||||
export const parseDomainName = (url) => {
|
||||
url = url.replace(/^https?:\/\//, "");
|
||||
// Remove leading/trailing dots
|
||||
url = url.replace(/^\.+|\.+$/g, "");
|
||||
// Split by dots
|
||||
const parts = url.split(".");
|
||||
// Remove common prefixes and empty parts and exclude the last element of the array (the last element should be the TLD)
|
||||
const cleanParts = parts.filter((part) => part !== "www" && part !== "").slice(0, -1);
|
||||
// If there's more than one part, append the two words and capitalize the first letters (e.g. ["api", "test"] -> "Api Test")
|
||||
const domainPart =
|
||||
cleanParts.length > 1
|
||||
? cleanParts.map((part) => capitalizeFirstLetter(part)).join(" ")
|
||||
: capitalizeFirstLetter(cleanParts[0]);
|
||||
|
||||
if (domainPart) return domainPart;
|
||||
|
||||
return url;
|
||||
};
|
||||
|
17
Client/src/Utils/stringUtils.js
Normal file
17
Client/src/Utils/stringUtils.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Helper function to get first letter capitalized string
|
||||
* @param {string} str String whose first letter is to be capitalized
|
||||
* @returns A string with first letter capitalized
|
||||
*/
|
||||
export const capitalizeFirstLetter = (str) => {
|
||||
if (str === null || str === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Input must be a string");
|
||||
}
|
||||
if (str.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
@ -1,13 +1,38 @@
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message?? "Validation error";
|
||||
updatedErrors[id] = error.details[0].message ?? "Validation error";
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes Joi validation errors and returns a filtered object of error messages for fields that have been touched.
|
||||
*
|
||||
* @param {Object} validation - The Joi validation result object.
|
||||
* @param {Object} validation.error - The error property of the validation result containing details of validation failures.
|
||||
* @param {Object[]} validation.error.details - An array of error details from the Joi validation. Each item contains information about the path and the message.
|
||||
* @param {Object} touchedErrors - An object representing which fields have been interacted with. Keys are field IDs (field names), and values are booleans indicating whether the field has been touched.
|
||||
* @returns {Object} - An object where keys are the field IDs (if they exist in `touchedErrors` and are in the error details) and values are their corresponding error messages.
|
||||
*/
|
||||
const getTouchedFieldErrors = (validation, touchedErrors) => {
|
||||
let newErrors = {};
|
||||
|
||||
if (validation?.error) {
|
||||
newErrors = validation.error.details.reduce((errors, detail) => {
|
||||
const fieldId = detail.path[0];
|
||||
if (touchedErrors[fieldId] && !(fieldId in errors)) {
|
||||
errors[fieldId] = detail.message;
|
||||
}
|
||||
return errors;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
const hasValidationErrors = (form, validation, setErrors) => {
|
||||
const { error } = validation.validate(form, {
|
||||
abortEarly: false,
|
||||
@ -22,16 +47,35 @@ const hasValidationErrors = (form, validation, setErrors) => {
|
||||
"dbConnectionString",
|
||||
"refreshTokenTTL",
|
||||
"jwtTTL",
|
||||
"notify-email-list",
|
||||
].includes(err.path[0])
|
||||
) {
|
||||
newErrors[err.path[0]] = err.message ?? "Validation error";
|
||||
}
|
||||
// Handle conditionally usage number required cases
|
||||
if (!form.cpu || form.usage_cpu) {
|
||||
newErrors["usage_cpu"] = null;
|
||||
}
|
||||
if (!form.memory || form.usage_memory) {
|
||||
newErrors["usage_memory"] = null;
|
||||
}
|
||||
if (!form.disk || form.usage_disk) {
|
||||
newErrors["usage_disk"] = null;
|
||||
}
|
||||
if (!form.temperature || form.usage_temperature) {
|
||||
newErrors["usage_temperature"] = null;
|
||||
}
|
||||
});
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
|
||||
console.log("newErrors", newErrors);
|
||||
if (Object.values(newErrors).some((v) => v)) {
|
||||
setErrors(newErrors);
|
||||
return true;
|
||||
} else return false;
|
||||
} else {
|
||||
setErrors({});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export { buildErrors, hasValidationErrors };
|
||||
export { buildErrors, hasValidationErrors, getTouchedFieldErrors };
|
||||
|
@ -1,6 +1,8 @@
|
||||
import joi from "joi";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number.";
|
||||
|
||||
const nameSchema = joi
|
||||
.string()
|
||||
.max(50)
|
||||
@ -16,25 +18,37 @@ const passwordSchema = joi
|
||||
.string()
|
||||
.trim()
|
||||
.min(8)
|
||||
.custom((value, helpers) => {
|
||||
if (!/[A-Z]/.test(value)) {
|
||||
return helpers.error("uppercase");
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
if (!/[a-z]/.test(value)) {
|
||||
return helpers.error("lowercase");
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
if (!/\d/.test(value)) {
|
||||
return helpers.error("number");
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
if (!/[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/.test(value)) {
|
||||
return helpers.error("special");
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "Password is required",
|
||||
"string.min": "Password must be at least 8 characters long",
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
if (!/[A-Z]/.test(value)) {
|
||||
return helpers.message("Password must contain at least one uppercase letter");
|
||||
}
|
||||
if (!/[a-z]/.test(value)) {
|
||||
return helpers.message("Password must contain at least one lowercase letter");
|
||||
}
|
||||
if (!/\d/.test(value)) {
|
||||
return helpers.message("Password must contain at least one number");
|
||||
}
|
||||
if (!/[!@#$%^&*]/.test(value)) {
|
||||
return helpers.message("Password must contain at least one special character");
|
||||
}
|
||||
|
||||
return value;
|
||||
uppercase: "Password must contain at least one uppercase letter",
|
||||
lowercase: "Password must contain at least one lowercase letter",
|
||||
number: "Password must contain at least one number",
|
||||
special: "Password must contain at least one special character",
|
||||
});
|
||||
|
||||
const credentials = joi.object({
|
||||
@ -60,15 +74,16 @@ const credentials = joi.object({
|
||||
confirm: joi
|
||||
.string()
|
||||
.trim()
|
||||
.messages({
|
||||
"string.empty": "Password confirmation is required",
|
||||
})
|
||||
.custom((value, helpers) => {
|
||||
const { password } = helpers.prefs.context;
|
||||
if (value !== password) {
|
||||
return helpers.message("Passwords do not match");
|
||||
return helpers.error("different");
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "This field can't be empty",
|
||||
different: "Passwords do not match",
|
||||
}),
|
||||
role: joi.array(),
|
||||
teamId: joi.string().allow("").optional(),
|
||||
@ -160,11 +175,49 @@ const advancedSettingsValidation = joi.object({
|
||||
pagespeedApiKey: joi.string().allow(""),
|
||||
});
|
||||
|
||||
const infrastructureMonitorValidation = joi.object({
|
||||
url: joi.string().uri({ allowRelative: true }).trim().messages({
|
||||
"string.empty": "This field is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
}),
|
||||
name: joi.string().trim().max(50).allow("").messages({
|
||||
"string.max": "This field should not exceed the 50 characters limit.",
|
||||
}),
|
||||
secret: joi.string().trim().messages({ "string.empty": "This field is required." }),
|
||||
usage_cpu: joi.number().messages({
|
||||
"number.base": THRESHOLD_COMMON_BASE_MSG,
|
||||
}),
|
||||
cpu: joi.boolean(),
|
||||
memory: joi.boolean(),
|
||||
disk: joi.boolean(),
|
||||
temperature: joi.boolean(),
|
||||
usage_memory: joi.number().messages({
|
||||
"number.base": THRESHOLD_COMMON_BASE_MSG,
|
||||
}),
|
||||
usage_disk: joi.number().messages({
|
||||
"number.base": THRESHOLD_COMMON_BASE_MSG,
|
||||
}),
|
||||
usage_temperature: joi.number().messages({
|
||||
"number.base": "Temperature must be a number.",
|
||||
}),
|
||||
// usage_system: joi.number().messages({
|
||||
// "number.base": "System load must be a number.",
|
||||
// }),
|
||||
// usage_swap: joi.number().messages({
|
||||
// "number.base": "Swap used must be a number.",
|
||||
// }),
|
||||
interval: joi.number().messages({
|
||||
"number.base": "Frequency must be a number.",
|
||||
"any.required": "Frequency is required.",
|
||||
}),
|
||||
});
|
||||
|
||||
export {
|
||||
credentials,
|
||||
imageValidation,
|
||||
monitorValidation,
|
||||
settingsValidation,
|
||||
maintenanceWindowValidation,
|
||||
advancedSettingsValidation
|
||||
advancedSettingsValidation,
|
||||
infrastructureMonitorValidation,
|
||||
};
|
||||
|
3
Client/src/assets/icons/cpu-chip.svg
Normal file
3
Client/src/assets/icons/cpu-chip.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 2V4M15 2V4M9 20V22M15 20V22M20 9H22M20 14H22M2 9H4M2 14H4M8.8 20H15.2C16.8802 20 17.7202 20 18.362 19.673C18.9265 19.3854 19.3854 18.9265 19.673 18.362C20 17.7202 20 16.8802 20 15.2V8.8C20 7.11984 20 6.27976 19.673 5.63803C19.3854 5.07354 18.9265 4.6146 18.362 4.32698C17.7202 4 16.8802 4 15.2 4H8.8C7.11984 4 6.27976 4 5.63803 4.32698C5.07354 4.6146 4.6146 5.07354 4.32698 5.63803C4 6.27976 4 7.11984 4 8.8V15.2C4 16.8802 4 17.7202 4.32698 18.362C4.6146 18.9265 5.07354 19.3854 5.63803 19.673C6.27976 20 7.11984 20 8.8 20ZM10.6 15H13.4C13.9601 15 14.2401 15 14.454 14.891C14.6422 14.7951 14.7951 14.6422 14.891 14.454C15 14.2401 15 13.9601 15 13.4V10.6C15 10.0399 15 9.75992 14.891 9.54601C14.7951 9.35785 14.6422 9.20487 14.454 9.10899C14.2401 9 13.9601 9 13.4 9H10.6C10.0399 9 9.75992 9 9.54601 9.10899C9.35785 9.20487 9.20487 9.35785 9.10899 9.54601C9 9.75992 9 10.0399 9 10.6V13.4C9 13.9601 9 14.2401 9.10899 14.454C9.20487 14.6422 9.35785 14.7951 9.54601 14.891C9.75992 15 10.0399 15 10.6 15Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -20,6 +20,8 @@ html {
|
||||
|
||||
--env-var-width-1: 100vw;
|
||||
--env-var-width-2: 360px;
|
||||
--env-var-width-3: 250px;
|
||||
--env-var-width-4: 100px;
|
||||
|
||||
--env-var-height-1: 100vh;
|
||||
--env-var-height-2: 34px;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { configureStore, combineReducers } from "@reduxjs/toolkit";
|
||||
|
||||
import uptimeMonitorsReducer from "./Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import infrastructureMonitorsReducer from "./Features/InfrastructureMonitors/infrastructureMonitorsSlice";
|
||||
import pageSpeedMonitorReducer from "./Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import authReducer from "./Features/Auth/authSlice";
|
||||
import uiReducer from "./Features/UI/uiSlice";
|
||||
@ -28,6 +29,7 @@ const persistConfig = {
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
uptimeMonitors: uptimeMonitorsReducer,
|
||||
infrastructureMonitors: infrastructureMonitorsReducer,
|
||||
auth: authReducer,
|
||||
pageSpeedMonitors: pageSpeedMonitorReducer,
|
||||
ui: uiReducer,
|
||||
|
2
Docker/dist/docker-compose.yaml
vendored
2
Docker/dist/docker-compose.yaml
vendored
@ -18,6 +18,8 @@ services:
|
||||
environment:
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
|
||||
- REDIS_HOST=redis
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
redis:
|
||||
image: bluewaveuptime/uptime_redis:latest
|
||||
ports:
|
||||
|
19
Docker/prod/certbot-compose.yaml
Normal file
19
Docker/prod/certbot-compose.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
webserver:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
restart: always
|
||||
volumes:
|
||||
- ./nginx/conf.d/:/etc/nginx/conf.d/:ro
|
||||
- ./certbot/www/:/var/www/certbot/:ro
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
volumes:
|
||||
- ./certbot/www/:/var/www/certbot/:rw
|
||||
- ./certbot/conf/:/etc/letsencrypt/:rw
|
||||
depends_on:
|
||||
- webserver
|
15
Docker/prod/nginx/conf.d/cerbot
Normal file
15
Docker/prod/nginx/conf.d/cerbot
Normal file
@ -0,0 +1,15 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name checkmate-demo.bluewavelabs.ca www.checkmate-demo.bluewavelabs.ca;
|
||||
server_tokens off;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://[domain-name]$request_uri;
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name uptime-demo.bluewavelabs.ca;
|
||||
server_name checkmate-demo.bluewavelabs.ca;
|
||||
server_tokens off;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
@ -38,10 +38,10 @@ server {
|
||||
listen 443 default_server ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
server_name uptime-demo.bluewavelabs.ca;
|
||||
server_name checkmate-demo.bluewavelabs.ca;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/privkey.pem;
|
||||
ssl_certificate /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/privkey.pem;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
|
26
Docker/test/build_images.sh
Executable file
26
Docker/test/build_images.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../..
|
||||
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["uptime_client"]="./Docker/prod/client.Dockerfile"
|
||||
["uptime_database_mongo"]="./Docker/prod/mongoDB.Dockerfile"
|
||||
["uptime_redis"]="./Docker/prod/redis.Dockerfile"
|
||||
["uptime_server"]="./Docker/prod/server.Dockerfile"
|
||||
)
|
||||
|
||||
# Loop through each service and build the corresponding image
|
||||
for service in "${!services[@]}"; do
|
||||
docker build -f "${services[$service]}" -t "$service" .
|
||||
|
||||
# Check if the build succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building $service image. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All images built successfully"
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user